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 */符号扩展为什么是”复制符号位”而不是别的,用 补码权值解释一步可证:
一个关键的顺序问题: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 按固定顺序找一个公共类型:
- 先各自做整数提升。
- 若符号性相同,转成较宽的那个。
- 若符号性不同:
- 无符号那方更宽或等宽 → 全部转为无符号;
- 有符号那方更宽,且能容纳无符号那方的全部值 → 全部转为有符号;
- 否则 → 转为有符号那方的无符号版本。
第 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 的值 */c整数提升为int(值仍是 3)。a + b:符号性不同、宽度相同 →a转unsigned,值为 4294967295。相加得,结果类型是 unsigned int,值为 1(回绕,定义良好)。1u > 3:符号性不同、宽度相同 →3转unsigned得 3。为假。
注意第 2 步:位串层面 a + b 就是 0xFFFFFFFF + 2 = 0x00000001,与按有符号算
边界
三种不能混叫的”溢出”
| 名称 | 发生在 | 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 short | unsigned short 先提升为 int,结果是有符号 |
char + char | 结果类型是 int |
int vs unsigned long long(64 位) | 转 unsigned long long |
考点
- 扩展补什么由源类型决定,与目标类型无关
- 符号扩展保值的证明:
int → unsigned long是先符号扩展、再重解释,得到全 1 而非高位补 0char、short参与运算前先整数提升为int- 同宽度混合时无符号”传染”;不同宽度时可能被提升化解
unsigned与int比较是最高频陷阱,判据看”提升后符号性是否仍不同”- 回绕(有定义)/有符号溢出(C 中 UB)/转换越界(不置标志),三者分开
(int)f是值转换,*(int*)&f是位型重解释
链接
- 🏠 返回总览:计算机组成原理第 2 章:数据的表示和运算总览
- ⬅️ 上一节:2.1.3 整数的表示
- ➡️ 下一节:2.2.1 基本运算部件
- 📖 名词库:第 2 章名词库