C 语言中的整数类型及类型转换

2.1.3 的结论是”有符号和无符号在硬件里是同一串位”。这一节讲的是:既然硬件不区分,那么区分的责任就落在语言身上——而 C 的区分规则,恰恰是 408 最爱出陷阱的地方。

本节要把三件事分清楚:同宽度的符号性转换(位串不变,值可能变)、窄变宽的扩展(值不变,位串变长)、宽变窄的截断(位串截短,值可能变)。三者的规则各不相同,混起来就会错。

机制

转换的三个正交动作

任何一次整数类型转换,都可以拆成两个独立的问题:宽度变不变、符号性变不变。

符号性不变符号性改变
宽度不变无操作重新解释(位串不动)
变宽扩展(零扩展或符号扩展)先扩展,再重新解释
变窄截断(取低位)先截断,再重新解释

“位串不动”是同宽度符号转换的全部内容,值的变化只是解释变了。这一点在 2.1.3 已经给出:(当 )。

有符号与无符号之间:位串不变,值重新解释

int x = -1;
unsigned int u = (unsigned)x;   /* u == 4294967295,位串仍是 0xFFFFFFFF */
 
unsigned int v = 4294967295u;
int y = (int)v;                 /* y == -1,位串仍是 0xFFFFFFFF */

没有任何位被改动。 (unsigned) 只是告诉编译器”从现在起按无符号解释这串位”。

标准的表述是”对模 取同余的那个值”,与位串不变是等价说法。

扩展:零扩展还是符号扩展,由源类型决定

窄类型转宽类型时,高位要补出来。补什么,只看源类型的符号性,与目标类型无关。

  • 源是无符号 → 零扩展,高位全补 0。
  • 源是有符号 → 符号扩展,高位全补源的符号位。
char  c  = -1;              /* 位串 0xFF */
int   i  = c;               /* 源是有符号 char → 符号扩展 → 0xFFFFFFFF == -1 */
 
unsigned char uc = 255;     /* 位串 0xFF */
int   j  = uc;              /* 源是无符号 → 零扩展 → 0x000000FF == 255 */

符号扩展为什么是”复制符号位”而不是别的,用 补码权值解释一步可证: 位补码的最高位权是 ;扩到 位后,原最高位的权变成 ,新最高位的权是 。若两位都为 1,合计 ,与扩展前完全一致。若都为 0 也显然一致。所以复制一位是唯一能保值的补法,重复多少次都成立。

一个关键的顺序问题:int x = -1; unsigned long u = x; 得到的是 0xFFFFFFFFFFFFFFFF,不是 0x00000000FFFFFFFF。规则是先按源类型(有符号 int)扩到目标宽度,再按目标符号性解释——即先扩展、后重解释。

截断:取低位,然后按目标类型重新解释

宽转窄时,高位直接丢弃,保留低 位,再按目标类型解释这 位。

int  x = 65535;             /* 0x0000FFFF */
short s = (short)x;         /* 取低 16 位 0xFFFF,按有符号 short 解释 → -1 */

数学上等价于对 取模,再映射回目标类型的范围。截断一定不会报错,也不会置任何标志位——它是显式转换的正常语义,不是溢出。

整数提升:小于 int 的类型先变成 int

C 有一条容易被忽略的规则:在表达式求值前,所有窄于 int 的整型(char、short、位域、枚举)都先被提升为 int(若 int 装不下则为 unsigned int)。

这条规则的直接后果是——char + char 的结果类型是 int,不是 char:

char a = 100, b = 100;
int  r = a + b;             /* 200,不是 -56:加法在 int 域中完成 */
char c = a + b;             /* -56:加法仍在 int 域完成,赋值时才截断 */

提升永远是保值的(unsigned char 的 0~255 全部落在 int 范围内,所以走零扩展也不丢值),因此提升本身从不产生意外,意外发生在提升之后的下一步。

通常算术转换:同宽度混合时,无符号”传染”

两个操作数类型不同时,C 按固定顺序找一个公共类型:

  1. 先各自做整数提升。
  2. 若符号性相同,转成较宽的那个。
  3. 若符号性不同:
    • 无符号那方更宽或等宽 → 全部转为无符号;
    • 有符号那方更宽,且能容纳无符号那方的全部值 → 全部转为有符号;
    • 否则 → 转为有符号那方的无符号版本。

第 3 条第一款就是最著名的陷阱:

int i = -1;
unsigned u = 1;
if (i < u)  puts("A");
else        puts("B");      /* 输出 B */

i 被转成 unsigned,值变成 4294967295,于是 变成 ,为假。

边界辨析:同一比较在不同宽度下结论相反

int i = -1; unsigned u = 1; 时 i < u 为假(同宽度,无符号胜出)。 但 int i = -1; unsigned short u = 1; 时 i < u 为真——unsigned short 先被整数提升为 int(int 能装下 0~65535),双方都是有符号, 成立。 判据是”提升之后两边还是不是符号性不同”,而不是字面上有没有 unsigned 这个词。

一次完整的求值:把规则串起来

int  a = -1;
unsigned int b = 2;
short c = 3;
/* 求 (a + b) > c 的值 */
  1. c 整数提升为 int(值仍是 3)。
  2. a + b:符号性不同、宽度相同 → a 转 unsigned,值为 4294967295。相加得 ,结果类型是 unsigned int,值为 1(回绕,定义良好)。
  3. 1u > 3:符号性不同、宽度相同 → 3 转 unsigned 得 3。 为假。

注意第 2 步:位串层面 a + b 就是 0xFFFFFFFF + 2 = 0x00000001,与按有符号算 得到的位串完全一样。位串永远一致,分歧只在类型标签上——这正是 2.1.3 那句”硬件不区分”的语言侧镜像。

边界

三种不能混叫的”溢出”

名称发生在C 的态度机器的行为
无符号回绕无符号运算超出 有定义:对 取模置 CF,结果照常写回
有符号溢出有符号运算超出 未定义行为置 OF,结果照常写回(就是回绕值)
转换越界宽转窄、或符号性转换后值不在目标范围有定义(无符号目标)或实现定义(有符号目标)单纯截断/重解释,不置任何标志

“未定义行为”是语言概念,不是硬件概念。 机器上有符号溢出和无符号回绕产生的位串完全相同;C 之所以把前者定为 UB,是为了给编译器优化留余地(例如允许假定 x + 1 > x 恒真)。408 的题若问”机器结果是多少”,一律按回绕算;若问”C 标准怎么规定”,才答未定义。

强制类型转换不等于位型重解释

同宽度的整数之间,强制转换确实是位型重解释。但这是特例,不是通则。

写法语义
(unsigned)i值转换,恰好等价于位型重解释(同宽度整数)
(short)i值转换(截断),位串变短
(int)f值转换:浮点按向零截断取整,3.9f → 3,位串完全不同
*(int*)&f位型重解释:把 float 的 32 位原样当 int 读

最后两行的对比是高频考点:(int)3.9f 得到 3;把 3.9f 的位串 0x4079999A 当 int 读得到 1081291674。两者毫无关系。详见 2.3.3。

提升是保值的,转换未必

整数提升永远不改变值(因为 int 能容纳所有窄类型的值)。通常算术转换则可能改变值(-1 变成 4294967295)。

考试里问”哪一步改变了数值”时,答案几乎总在通常算术转换或赋值截断那一步,不会在整数提升上。

对照速查

转换位串值由谁决定
同宽度改符号性不变可能变——
变宽(源无符号)高位补 0不变源类型
变宽(源有符号)高位补符号位不变源类型
变窄取低位可能变目标宽度
混合运算结论
int vs unsigned int转 unsigned( 变成最大值)
int vs unsigned shortunsigned short 先提升为 int,结果是有符号
char + char结果类型是 int
int vs unsigned long long(64 位)转 unsigned long long

考点

  • 扩展补什么由源类型决定,与目标类型无关
  • 符号扩展保值的证明:
  • int → unsigned long 是先符号扩展、再重解释,得到全 1 而非高位补 0
  • char、short 参与运算前先整数提升为 int
  • 同宽度混合时无符号”传染”;不同宽度时可能被提升化解
  • unsigned 与 int 比较是最高频陷阱,判据看”提升后符号性是否仍不同”
  • 回绕(有定义)/有符号溢出(C 中 UB)/转换越界(不置标志),三者分开
  • (int)f 是值转换,*(int*)&f 是位型重解释

链接