飞翔飞翔
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
  • 数据库

    • SQL教程
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON教程
  • 工具

    • Markdown指南
  • Git

    • GitFlow
  • Quartz

    • Quartz教程
  • Java

    • Java设计模式
  • 缓存

    • Redis教程
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
  • 数据库

    • SQL教程
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON教程
  • 工具

    • Markdown指南
  • Git

    • GitFlow
  • Quartz

    • Quartz教程
  • Java

    • Java设计模式
  • 缓存

    • Redis教程
联系
阿里云
  • 学习路径
  • 第1章 编程基础概念

    • 冯·诺依曼体系结构
    • 数据在计算机中的表示
    • 编程语言的层次
    • C语言的起源与发展
    • C99标准的主要改进
    • 开发环境搭建
    • 第一个C程序
    • 编译与运行流程
    • 可移植性风险的三级体系
  • 第2章 数据类型与运算

    • 字符集与标识符
    • 关键字
    • 注释
    • char 类型
    • short 与 int
    • long 与 long long
    • 有符号与无符号
    • 取值范围与 limits.h
    • float 与 double
    • long double
    • _Bool 类型
    • 变量声明与定义
    • 常量
    • 转义序列
    • 算术运算符
    • 赋值运算符
    • 自增自减运算符
    • 关系与判等运算符
    • 逻辑运算符
    • 位运算符
    • 条件运算符
    • 逗号运算符
    • 运算符优先级
    • 隐式类型转换
    • 显式类型转换
  • 第3章 控制流

    • 表达式语句与空语句
    • 复合语句
    • if 语句
    • switch 语句
    • while 循环
    • do-while 循环
    • for 循环
    • break 与 continue
    • goto 语句
    • return 语句
  • 第4章 函数与模块化编程

    • 函数定义
    • 函数声明与原型
    • main 函数
    • 函数调用机制
    • 传值调用
    • 数组参数
    • 作用域
    • 存储期
    • 链接属性
    • static 与 extern
    • 递归
    • 头文件与源文件
    • 头文件保护
    • include 规则
  • 第5章 数组与字符串

    • 一维数组声明与初始化
    • 数组的存储模型
    • 数组访问与越界
    • 数组操作
    • 二维数组
    • 变长数组 VLA
    • 字符串基础
    • 字符串输入输出
    • 字符串处理函数
    • 字符串与数字转换
  • 第6章 指针

    • 指针的概念
    • 指针的声明与使用
    • 指针运算
    • const 与指针
    • 数组名与指针
    • 指针遍历数组
    • 指针与多维数组
    • 指针作为函数参数
    • 函数返回指针
    • 函数指针
    • 二级指针
    • 复杂声明解析
  • 第7章 结构体、联合体与枚举

    • 结构体定义与声明
    • 结构体初始化
    • 结构体成员访问
    • 结构体嵌套
    • 结构体指针
    • 结构体与函数
    • 联合体
    • 联合体与类型双关
    • 枚举类型
    • 位域
    • 内存对齐与填充
  • 第8章 动态内存管理

    • malloc 与 free
    • calloc 与 realloc
    • 内存泄漏
    • 悬垂指针
    • 内存分配策略
    • 自定义内存池
    • Valgrind 与内存检测
    • 内存碎片
    • 内存对齐分配
    • 常见内存错误
  • 第9章 文件输入输出

    • 文件打开与关闭
    • 文本读写
    • 格式化输入输出
    • 二进制读写
    • 文件定位
    • 错误处理
    • 标准流
    • 临时文件
    • 文件操作示例
  • 第10章 预处理器

    • 预处理器基础
    • 宏定义
    • 带参数的宏
    • 条件编译
    • 头文件包含
    • 预定义宏
    • 宏的高级技巧
    • 预处理器陷阱
    • 编译器特定扩展
  • 第11章 标准库概览

    • 标准库概述
    • assert.h
    • ctype.h
    • errno.h
    • float.h
    • limits.h
    • locale.h
    • math.h
    • setjmp.h
    • signal.h
    • stdarg.h
    • stddef.h
    • stdlib.h
  • 第12章 进阶主题

    • 内联函数
    • 变长数组 VLA
    • 复数类型
    • 布尔类型
    • stdint 与 inttypes
    • 灵活数组成员
    • 匿名结构体与联合体
    • 静态断言
    • 线程支持
    • 原子操作

有符号与无符号

C 语言的整数类型默认是有符号的(signed),可以表示正数、负数和零。加 unsigned 修饰后,只表示非负数,但正数的上限扩大一倍。有符号和无符号的选择不仅影响取值范围,还深刻影响运算行为——混合使用时的隐式转换规则是 C 语言中最常见的陷阱之一。

有符号类型

有符号整数使用补码表示,最高位是符号位:0 表示正,1 表示负。

int temperature = -20;          /* 可以是负数 */
int balance = -1000;            /* 账户余额可能为负 */

有符号类型的范围关于零对称(大致),如 32 位 int:-2147483648 到 2147483647。

无符号类型

无符号整数所有位都参与表示数值,没有符号位。

unsigned int age = 25;          /* 年龄不可能为负 */
size_t array_size = 100;        /* 数组大小、内存大小 */

相同位宽下,无符号类型的最大值是有符号类型的两倍:

类型有符号范围无符号范围
16 位-32768 ~ 327670 ~ 65535
32 位-2147483648 ~ 21474836470 ~ 4294967295
64 位-9223372036854775808 ~ 92233720368547758070 ~ 18446744073709551615

显式声明

可以在任何整数类型前加 signed 或 unsigned:

signed int a = -10;             /* 等价于 int a = -10 */
unsigned int b = 10;            /* 非负 */

signed char c;                  /* 明确有符号字符:-128 ~ 127 */
unsigned char d;                /* 明确无符号字符:0 ~ 255 */

/* signed 和 unsigned 单独使用时,默认修饰 int */
signed x;                       /* 等价于 signed int x */
unsigned y;                     /* 等价于 unsigned int y */

char 类型特殊:不加修饰时,char 可能是有符号也可能是无符号,由编译器决定。

混合运算的陷阱

C 语言的"寻常算术转换"(Usual Arithmetic Conversions)规定:当无符号类型与有符号类型混合运算时,有符号类型会被隐式转换为无符号类型。

unsigned int u = 10;
int s = -5;

if (s < u)
    printf("-5 < 10\n");       /* 不会输出! */

/* 原因:s = -5 被转换为 unsigned int */
/* -5 的补码表示被解释为无符号数:4294967291 */
/* 4294967291 < 10 为假 */

这个规则导致许多看似正确的比较产生意外结果:

size_t len = strlen("hello");   /* size_t 是无符号类型 */
int i = -1;

if (i < len)
    printf("i < len\n");         /* 可能不会输出! */

安全做法:避免有符号和无符号混合比较。如果必须比较,将有符号数显式转换为无符号,或确保有符号数非负:

/* 方案 1:确保有符号数非负 */
if (i >= 0 && (size_t)i < len)
    printf("Safe comparison\n");

/* 方案 2:使用相同类型 */
int len_int = (int)strlen("hello");   /* 确认不会溢出 */
if (i < len_int)
    printf("i < len\n");

无符号类型的回绕

无符号整数溢出不是未定义行为,而是定义的回绕(Wrap-around):

unsigned int u = 0;
u = u - 1;                      /* 回绕到 UINT_MAX */
printf("%u\n", u);              /* 4294967295 */

unsigned int max = UINT_MAX;
max = max + 1;                  /* 回绕到 0 */
printf("%u\n", max);            /* 0 */

回绕在某些场景下是有用的(如循环计数器、哈希计算),但也可能导致意外:

/* 错误的循环:当 i 为 unsigned 时,i >= 0 永远为真 */
for (unsigned int i = n; i >= 0; i--)
    printf("%u\n", i);          /* 无限循环!i 减到 0 后再减回绕到 UINT_MAX */

/* 正确做法 */
for (unsigned int i = n; i > 0; i--)
    printf("%u\n", i);          /* 先打印,再递减 */

选择建议

场景推荐
可能为负的数值(温度、坐标差)有符号
计数、大小、索引size_t(无符号)或确保非负的有符号
位运算、掩码、标志无符号
需要最大正数范围无符号
一般计算有符号(默认)
/* 位运算必须用无符号 */
unsigned int flags = 0x0F;
flags = flags & ~0x02;          /* 清除第 2 位 */

/* 循环索引 */
for (size_t i = 0; i < count; i++)   /* size_t 适合索引 */
    process(data[i]);

常见错误

用无符号表示"可能为负"的概念:

unsigned int debt = -1000;      /* 错误:-1000 被转换为很大的正数 */

有符号与无符号比较:

int array[10];
for (int i = 0; i < 10; i++) { }

/* 如果改用 size_t */
for (size_t i = 0; i < 10; i++) { }    /* 10 被提升为 size_t,安全 */

/* 但如果比较变量 */
int n = -1;
size_t count = 5;
if (n < count)                  /* 危险:n 被转换为很大的无符号数 */
    printf("n < count\n");       /* 不会输出 */

无符号减法的回绕:

unsigned int a = 5, b = 10;
unsigned int diff = a - b;      /* 回绕!结果是 UINT_MAX - 4 */
上一页
long 与 long long
下一页
取值范围与 limits.h