有符号与无符号
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 ~ 32767 | 0 ~ 65535 |
| 32 位 | -2147483648 ~ 2147483647 | 0 ~ 4294967295 |
| 64 位 | -9223372036854775808 ~ 9223372036854775807 | 0 ~ 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 */