← 目录 / 第二章 · 变量与数据类型 / 2.3 原码、反码与补码

2.3 原码、反码与补码

计算机只认识 0 和 1,那它是如何表示负数的?科学家们为此设计了三种编码方案,依次改进,最终解决了这个问题。

原码、反码与补码

计算机只认识 0 和 1,那它是如何表示负数的?科学家们为此设计了三种编码方案,依次改进,最终解决了这个问题。

符号位约定:最高位(最左边)表示正负,0 代表正数,1 代表负数,其余位存储数值。以 8 位(1 字节)为例:

符号位示例
10 0000101 → +5(符号位为 0,表示正数)
21 0000101 → -5(符号位为 1,表示负数)

2.3.1 三种编码方案

1
原码
最直觉的写法:符号位表示正负,其余位直接写数字的二进制。
+5 → 0 0000101
-5 → 1 0000101
✗ 问题:加减法需要专门区分正负处理,电路复杂。
2
反码
规则:正数不变;负数符号位不变,其余位全部取反。
-5 原码:1 0000101
-5 反码:1 1111010
△ 问题:0 有两种表示(+0 和 -0),需额外处理。
3
补码 ✓
规则:正数不变;负数 = 反码 + 1。现代计算机真正使用的方案。
-5 反码:1 1111010
-5 补码:1 1111011
✓ -0 被消灭,多出的编码用来表示 -128!

2.3.2 验证:5 + (−5) 在三种编码下的结果

二进制加法演示
── 原码(错误)──────────────────
0 0000101 (+5)
+ 1 0000101 (-5)
──────────
1 0001010 = -10,完全不对!✗
── 反码(接近但有瑕疵)───────────
0 0000101 (+5 的反码)
+ 1 1111010 (-5 的反码)
──────────
1 1111111 = -0,还是不对 △
── 补码(正确!)────────────────
0 0000101 (+5 的补码)
+ 1 1111011 (-5 的补码)
──────────
1 0 0000000 最高位进位溢出,自动丢弃
0 0000000 = 0 完全正确!✓

2.3.3 三种编码对照表(8 位)

十进制 原码 反码 补码
+5 0000 0101 0000 0101 0000 0101
-5 1000 0101 1111 1010 1111 1011
+0 0000 0000 0000 0000 0000 0000
-0 1000 0000 1111 1111 (不存在!)
-128 无法表示 无法表示 1000 0000
-1 1000 0001 1111 1110 1111 1111
+127 0111 1111 0111 1111 0111 1111
📖
为什么 signed char 是 −128 到 127?补码消灭了 −0,空出来的编码 1000 0000 被用来表示 −128。8 位补码能表示 256 个数:−128~−1(128 个)加上 0~127(128 个),比原码多表示一个数。
📚
扩展阅读:C++20 标准正式强制要求有符号整数使用补码表示(此前是实现定义的)。补码不再是"可选方案",而是所有 C++ 平台的统一标准。理解三种编码的演进历史,有助于真正体会补码设计的精妙之处。

2.3.4 多字节数据的内存布局:大端与小端

前面讨论的补码都是针对 1 个字节(8 位)展开的,但 int 占 4 个字节、long long 占 8 个字节——这些"大于 1 字节"的数据,在内存里到底是怎么摆放的?这就引出了两个新概念:低位字节高位字节,以及数据摆放顺序的约定——字节序(大端 / 小端)

以十六进制数 0x12345678 这个 int 为例,把它拆成 4 个字节:12345678。数值上越"重要"(越靠左、代表的数值越大)的字节叫高位字节(这里是 12),越靠右、数值贡献越小的叫低位字节(这里是 78)——这和十进制里"千位是高位、个位是低位"是同一个道理。

int n = 0x12345678; 拆分成 4 个字节
字节
12高位字节
34·
56·
78低位字节

问题来了:内存地址是从小到大排列的一条线(见 6.1 节数组的内存布局),这 4 个字节该按什么顺序放进连续的地址里?"高位字节放在前面(小地址)"还是"低位字节放在前面(小地址)",这是完全可以自行约定的两种方案,也确实都有 CPU 在用:

大端(Big-Endian):高位字节放在低地址 —— 符合人类阅读习惯
地址增大 →
120x00
340x01
560x02
780x03
↑ 从左到右读正好是 12 34 56 78,和数字本身的书写顺序一致
小端(Little-Endian):低位字节放在低地址 —— x86 / x64 / 大多数 ARM 采用
地址增大 →
780x00
560x01
340x02
120x03
↑ 从左到右读是 78 56 34 12,正好和数字书写顺序相反
💻
你的电脑用的是哪一种?我们日常使用的 x86、x64(Intel/AMD)以及绝大多数 ARM 设备(手机、树莓派等)默认都采用小端存储;网络传输协议(如 TCP/IP)则统一规定使用大端,因此某些跨网络通信的底层代码需要做字节序转换——这也是 7.2 节 __builtin_bswap32 这个函数真正的用武之地:它能快速把一个数的字节顺序整个翻转过来,实现大小端互转。

记不住哪个是"大端"哪个是"小端"?可以这样理解名字的由来:"端"指的是数值的末端(最低位那一端):

用指针技巧检测本机字节序
1int n = 1; // 内存中只有最低位字节是 1,其余字节是 0
2char* p = (char*)&n; // 取 n 的地址,强制按单字节方式解读
3
4if (*p == 1)
5 cout << "小端:最低地址存的是低位字节 1";
6else
7 cout << "大端:最低地址存的是高位字节 0";
💡
这段代码的原理:n 的值是 1,在内存中只有最低位字节是 0x01,其余 3 个字节都是 0x00。把 int* 强制转换成 char* 再取第一个字节(也就是最低地址那个字节),如果读到的是 1,说明这台机器把"低位字节"放在了"低地址"——也就是小端;如果读到 0,则说明高位字节在最低地址,是大端。这是判断本机字节序最经典的写法。
⚠️
日常写代码基本不用操心字节序:只要老老实实用 cin/cout、变量赋值、算术运算这些正常方式操作数据,字节序完全是透明的,编译器和 CPU 会自动处理好一切。只有当你需要"跳过语言的抽象、直接摸到内存里的每一个字节"时——比如网络编程、跨平台的二进制文件读写、或者上面这种指针强转的技巧——字节序才会成为一个需要认真考虑的问题。