整数的表示

上一节把四种编码都摆了出来,本节只做一件事:把它落到真实机器上。答案非常简洁——现代机器里的有符号整数一律用补码,无符号整数就是纯二进制,原码和反码只活在习题里。

本节真正的重点不是”用什么码”,而是由此导出的一个反直觉事实:有符号数和无符号数在硬件里是同一串位,甚至用的是同一个加法器。“有没有符号”不是数据的属性,是解释的属性。

机制

无符号整数:整个字长都是数值位

无符号整数没有符号位, 位全部参与位权展开:它对应 C 的 unsigned 系列。硬件上它就是一个模 的环: 再加 1 回到 0,0 再减 1 回到 。这个行为叫回绕,是定义良好的,不是错误。

有符号整数:补码,且只有补码

位补码:

选补码的理由已在 2.1.2 给全:减法消失、零唯一、符号位可参与运算。

408 的默认假设:一切有符号整数按补码存储、按补码运算。 题目若要考原码或反码,必然会明说。

同一串位的两种解释

这是本节最该记牢的一张图:同一个 位位串,无符号解释和补码解释的差值恒为 (当最高位为 1 时),否则相等。

补补补补
flowchart TD
    B["位串 <b>1111 1111</b>"]:::bits
    U["按<b>无符号</b>解释<br/>= 255"]:::u
    S["按<b>补码</b>解释<br/>= −1"]:::s
    D["两者相差 <b>2⁸ = 256</b>"]:::note
    H["硬件里<b>只有这一串位</b><br/>加法器对两者<b>完全相同</b>"]:::hw

    B --> U
    B --> S
    U --> D
    S --> D
    B --> H

    classDef bits fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
    classDef u fill:#fef3c7,stroke:#d97706,color:#78350f
    classDef s fill:#dcfce7,stroke:#16a34a,color:#14532d
    classDef note fill:#f1f5f9,stroke:#94a3b8,color:#334155
    classDef hw fill:#ede9fe,stroke:#7c3aed,color:#4c1d95

“加法器对两者完全相同”这句话不是简化说法,而是字面事实。 由 模运算,无符号加法和补码加法在模 下是同一个运算,产生的位串一模一样。

那么硬件靠什么区分有符号与无符号

既然加法器不区分,区别必然出现在别的地方。一共五处,这五处正是所有”有符号/无符号”考题的出处:

区分点无符号有符号(补码)
溢出判据看进位标志 CF看溢出标志 OF
大小比较直接比位串必须先看最高位
右移逻辑右移(补 0)算术右移(补符号位)
除法 / 取模无符号除法指令有符号除法指令
位数扩展零扩展符号扩展

一次加法之后,CF 和 OF 是同时被置位的,机器并不知道该看哪一个——决定看哪一个的是程序(编译器根据变量类型选择跳转指令)。溢出判据在 2.2.3 展开,移位在 2.2.2,扩展在 2.1.4。

这正是”C 的有符号溢出是未定义行为、无符号溢出有定义”的机器层根源:位串行为是一样的,但语言只承诺了其中一种解释。

常用字长与范围

408 的默认机器模型(也是 x86-64 上 C 的实际情况,long 除外):

C 类型位数有符号范围无符号范围
char8
short16
int32
long long64

数量级要有感觉:(约 21 亿),,。

long 的宽度依平台而变(Linux/macOS 64 位上是 8 字节,Windows 上是 4 字节),题目不给宽度就不要假设。

特征位串

考试里反复出现的几个,直接背下来比现场算快:

位串(32 位)有符号无符号身份
0x0000000000零
0x7FFFFFFFINT_MAX
0x80000000INT_MIN;仅最高位为 1
0xFFFFFFFF全 1;UINT_MAX
0xFFFFFFFE

“全 1 就是 “值得单独记住:由权值解释,,且它与字长无关,8 位、32 位、64 位全 1 都是 。

边界

非对称性

补码的正负两侧不对称:,而正数侧最大只到 。于是 的相反数在同字长内不存在。

由此产生一串在真实代码里会出问题的等式:

溢出

前两个从 “连符号位取反加一” 直接可验证:1000…0 取反是 0111…1,加 1 又回到 1000…0。第三个在 x86 上会触发除法异常而不是给出错误值——这是少数几个”溢出会真的中断程序”的情形,见 2.2.4。

回绕不是溢出,溢出不是错误

三个词经常被混着用,在 408 里必须分开:

词含义是不是错误
回绕结果对 取模后落回表示范围不是,无符号运算的定义行为
溢出数学结果超出该编码的表示范围是”超范围”这一事实,由标志位报告
未定义行为C 语言层面对有符号溢出不作任何承诺语言概念,不是硬件概念

硬件永远在做模 运算,从不”出错”,它只是把 CF/OF 置位然后继续。是否算错误,由使用者按类型决定。

无符号数没有负数,但会变成很大的正数

一个在真实代码里出现频率极高的坑:

unsigned int n = 3;
for (unsigned int i = n; i >= 0; i--) { /* 死循环 */ }

i >= 0 对无符号数恒为真,i 减到 0 后再减变成 0xFFFFFFFF,循环永不结束。根源就是无符号是环、不是数轴。

同类的还有 sizeof 的返回类型是无符号的 size_t,所以 if (a - b < 0) 在 a、b 都是 size_t 时永远不成立。相关的类型转换规则在 2.1.4。

整数在内存里的字节顺序是另一个问题

本节只说”一个整数由哪些位组成”,没有说这些字节按什么顺序放进内存。那是 2.3.4 大小端的内容,两者互不决定:大小端只影响多字节对象在内存中的排列,不改变寄存器里的数值。

对照速查

判据结论
有符号整数用什么编码补码(408 默认)
加法器区分有无符号吗不区分,位串完全相同
靠什么区分溢出判据 / 比较 / 右移 / 除法 / 扩展 五处
全 1 位串是多少有符号 (与字长无关),无符号
0x80000000 是多少有符号 ,无符号
等于 自身

考点

  • 有符号数与无符号数在机器里是同一串位,共用一个加法器
  • 区分只发生在溢出判据、比较、右移、除法、扩展五处
  • 全 1 = ,与字长无关;0x80000000 有符号是
  • , 溢出
  • 无符号数 i >= 0 恒真,无符号是环不是数轴
  • 回绕、溢出、未定义行为是三个不同层面的概念
  • long 的宽度依平台而变,题目不给就不假设

链接