Skip to content

第2章 数据编码 ​

🎯 本章学习目标 ​

学完本章,你将能够:

  1. 理解计算机采用二进制存储数据的原因,以及符号位的含义
  2. 区分机器数与真值的概念,避免把符号位当数值位计算
  3. 掌握原码、反码、补码的定义与相互转换方法
  4. 熟练手算给定十进制数的 8 位/16 位补码
  5. 熟练已知补码反求真值(十进制数)
  6. 理解补码的三大优势:减法变加法、零的表示唯一、多一个最小负数
  7. 记住 n 位补码的表示范围:-2^(n-1) ~ 2^(n-1)-1
  8. 掌握溢出的判断方法:同号相加结果变号则溢出
  9. 理解 signed 与 unsigned 的区别,以及 char 类型的符号性问题
  10. 了解用 C++ 的 bitset 验证和输出二进制编码(拓展内容)

🗺️ 本章知识图谱 ​

数据编码(原码、反码、补码)
├── 基本概念
│   ├── 计算机内部是二进制
│   ├── 机器数 vs 真值
│   └── 符号位(0正1负)
├── 原码
│   ├── 定义:符号位 + 绝对值
│   ├── 问题一:0有两种表示(+0和-0)
│   └── 问题二:加减法运算复杂
├── 反码
│   ├── 正数:同原码
│   ├── 负数:符号位不变,数值位取反
│   └── 地位:原码转补码的中间桥梁
├── 补码 ⭐(核心)
│   ├── 正数:三码合一
│   ├── 负数:反码 + 1
│   ├── 三大优势
│   │   ├── 减法变加法(统一用加法器)
│   │   ├── 零的表示唯一
│   │   └── 多一个最小负数(-128)
│   ├── 表示范围:-2^(n-1) ~ 2^(n-1)-1
│   ├── 快速求法:从右数第一个1左边取反
│   └── 已知补码求真值:再求补得原码
├── 溢出判断
│   ├── 同号相加结果变号 → 溢出
│   └── 异号相加永远不溢出
└── C++ 中的编码
    ├── signed vs unsigned
    ├── char 的符号性由平台决定
    └── bitset 查看二进制(拓展)

一、为什么需要数据编码? ​

1.1 计算机只认识 0 和 1 ​

计算机的硬件电路只有"高电平"和"低电平"两种状态,因此所有数据在计算机内部都以二进制存储。数字 5 在 8 位二进制中写作 0000 0101,这很好理解——可负数怎么办?数字前面可没有"负号键"。

科学家们想到一个办法:用最高位充当符号位。

  • 最高位为 0,表示正数
  • 最高位为 1,表示负数

由此引出两个概念:

概念含义举例
机器数一个数在计算机中的二进制表示形式(带符号位)-5 的机器数是 1000 0101
真值机器数真正表示的十进制数值(要考虑符号位)1000 0101 的真值是 -5,不是 133

⚠️ 易错点: 机器数的最高位是符号位,不能当普通数值位算!1000 0101 直接按二进制算是 133,但它的真值是 -5。考试中经常在这里挖坑。

1.2 三种编码方案 ​

为了让负数既能被表示、又能正确参与运算,人们先后设计了三种编码:

  • 原码:最直观,符号位 + 数值的绝对值
  • 反码:原码与补码之间的"桥梁"
  • 补码:现代计算机实际使用的编码 🎯

下面逐一拆解。


二、原码:最直观的表示法 ​

2.1 定义 ​

原码:最高位为符号位(0 正 1 负),其余位表示数值绝对值的二进制。

以 8 位为例:

+5 的原码: 0 000 0101
            ↑
          符号位0,表示正数

-5 的原码: 1 000 0101
            ↑
          符号位1,表示负数

2.2 原码的两个致命问题 ​

问题一:0 有两个原码

+0 的原码:0000 0000
-0 的原码:1000 0000

数学上 0 不分正负,但原码里却有两个"0",既浪费编码又给判断带来麻烦。

问题二:直接做加减法会出错

用原码计算 1 + (-1):

  0000 0001   (+1 的原码)
+ 1000 0001   (-1 的原码)
-----------
  1000 0010   = -2  ❌ 结果居然是 -2!

符号位参与运算导致结果完全错误。若要让原码正确做减法,硬件需要额外判断符号、比较绝对值大小,电路设计十分复杂。

🔥 高频考点: 原码的优点是人眼直观、转换方便;缺点是 0 有两种表示、加减法运算规则复杂。8 位原码的表示范围是 -127 ~ +127(注意没有 -128)。


三、反码:承上启下的过渡编码 ​

3.1 定义 ​

  • 正数的反码 = 原码(不变)
  • 负数的反码:符号位保持 1 不变,其余数值位按位取反(0 变 1,1 变 0)

以 8 位 -5 为例:

-5 的原码: 1 000 0101
              ↓ 数值位逐位取反
-5 的反码: 1 111 1010

正数 +5 的反码则与原码完全相同:0000 0101。

3.2 反码的小练习 ​

十进制原码(8位)反码(8位)
+70000 01110000 0111
-71000 01111111 1000
+10000 00010000 0001
-11000 00011111 1110

反码同样存在 +0(0000 0000)和 -0(1111 1111)两个零,而且用反码做 1 + (-1):

  0000 0001
+ 1111 1110
-----------
  1111 1111   = -0   (结果是个"负零",需要额外处理)

⚠️ 易错点: 反码在现代计算机中并不直接用于存储和运算,它只是原码转补码过程中的中间产物。考试若问"计算机中整数实际采用什么编码",答案永远是补码。


四、补码:计算机真正使用的编码 ⭐⭐⭐⭐⭐ ​

4.1 定义 ​

  • 正数的补码 = 原码(三码合一)
  • 负数的补码 = 反码 + 1(即:符号位不变,数值位取反后再加 1)

以 8 位 -5 为例完整走一遍:

第1步 写原码:  1 000 0101   (-5)
第2步 求反码:  1 111 1010   (数值位取反)
第3步 反码+1:  1 111 1011   (-5 的补码)✅

4.2 补码的三大优势 ​

优势一:加减法统一,减法变加法

用补码计算 1 + (-1):

  0000 0001    (+1 的补码)
+ 1111 1111    (-1 的补码)
-----------
1 0000 0000    (最高位产生进位,超出8位直接丢弃)
= 0000 0000    = 0  ✅ 完美!

CPU 只需一个加法器就能同时处理加法和减法,硬件大大简化。

优势二:0 的表示唯一

-0 的补码怎么求?反码 1111 1111 加 1 得到 1 0000 0000,进位丢弃后就是 0000 0000,和 +0 完全相同!

优势三:多出一个编码 -128

既然 1000 0000 不再浪费在"-0"身上,就用它表示补码中最小的负数 -128(8 位下 -128 没有对应的原码和反码,是补码"专属"的数)。

🔥 高频考点: n 位补码的表示范围是 -2ⁿ⁻¹ ~ 2ⁿ⁻¹ - 1。

  • 8 位补码:-128 ~ 127
  • 16 位补码:-32768 ~ 32767
  • 32 位补码(常见的 int):约 -21 亿 ~ 21 亿

负数比正数多一个,这是补码独有的特点!

4.3 生活中的补码思想:调钟表 ​

为什么"取反加一"这么神奇?可以用钟表理解:钟面一圈 12 个小时(模为 12),从 8 点调到 6 点,既可以逆时针拨 2 小时(8 - 2 = 6),也可以顺时针拨 10 小时(8 + 10 = 18,18 mod 12 = 6)。

也就是说,在模 12 的系统里,-2 和 +10 等价。8 位二进制的模是 2⁸ = 256,所以 -5 和 +251 等价——而 251 的二进制正是 1111 1011,这就是 -5 的补码!

4.4 负数补码的快速求法 ​

除了"取反加一",还有一个更快的手算技巧:

从右往左找到第一个 1,这个 1 及其右边的 0 保持不变,左边的所有数值位逐位取反,符号位始终为 1。

例:求 -5 的补码,原码数值位 000 0101:

1 000 0101
       ↑ 从右数第一个1
1 111 1011   ← 该1及其右侧不变,左侧数值位取反

结果与"取反加一"完全一致。


五、三种编码对比大表 ​

5.1 8 位编码对照表 ​

十进制真值原码反码补码
+70000 01110000 01110000 0111
-71000 01111111 10001111 1001
+50000 01010000 01010000 0101
-51000 01011111 10101111 1011
+10000 00010000 00010000 0001
-11000 00011111 11101111 1111
+00000 00000000 00000000 0000
-01000 00001111 11110000 0000(进位丢弃)
-128❌ 无法表示❌ 无法表示1000 0000(补码专属)

5.2 16 位编码示例 ​

十进制真值原码反码补码
+50000 0000 0000 0101同原码同原码
-51000 0000 0000 01011111 1111 1111 10101111 1111 1111 1011
+327670111 1111 1111 1111同原码同原码
-32768❌❌1000 0000 0000 0000

🔥 记忆口诀: 正数"三码合一";负数"原码看绝对值,反码数值位取反,补码反码再加一"。


六、已知补码,如何求原码和真值? ​

考试常反过来考:给你一个补码,问它表示的十进制数是多少。

6.1 正数:补码即原码 ​

符号位为 0,直接把数值位转成十进制即可。如补码 0000 0111 → 真值 +7。

6.2 负数:两种方法 ​

方法一:逆运算(补码减 1 得反码,再取反得原码)

补码 1111 1011
  减1 → 1111 1010(反码)
  数值位取反 → 1000 0101(原码)= -5

方法二:对补码再求一次补(推荐!)

补码的补码就是原码。 对负数补码的数值位再次"取反加 1",符号位不变,直接得到原码。

补码     1 111 1011
数值位取反 1 000 0100
加1      1 000 0101  = -5 ✅

6.3 特殊值 ​

  • 补码 1000 0000 的真值是 -128(不要按原码规则算成 -0!)
  • 补码 1111 1111 的真值是 -1

七、典型考题类型 ​

7.1 题型一:给定十进制数,求补码 ​

例: 求 -13 的 8 位补码。

13 = 8+4+1 = 0000 1101
原码:1000 1101
反码:1111 0010
补码:1111 0011  ✅

7.2 题型二:补码表示范围 ​

例: 8 位补码能表示多少个不同的整数?

答:-128 ~ 127 共 256 个,正好把 2⁸ 个编码全部用完,没有浪费。

7.3 题型三:溢出判断 ​

🔥 高频考点: 两个同号数相加,若结果符号与它们相反,则发生溢出;异号相加永远不会溢出。

例: 8 位补码下计算 127 + 1:

  0111 1111   (+127)
+ 0000 0001   (+1)
-----------
  1000 0000   (符号位变成1!按补码解释为 -128)

两个正数相加竟得出负数——这就是溢出。同理 -128 - 1 也会溢出得到 +127。


八、C++ 中的编码:signed、unsigned 与 char ​

📚 拓展 · 超纲:本节用到的 bitset 属于 C++ 标准库容器,不在三级考纲范围内。这里仅把它当作"把内存中的二进制打出来看"的演示工具,帮助理解补码;考试不会考查 bitset 的用法,读者了解即可。

C++ 中的整数类型在主流编译器下一律采用补码存储。我们可以用 C++11 的 bitset 把内存中的二进制"打"出来验证。

8.1 打印二进制,验证原反补 ​

cpp
#include <iostream>
#include <bitset>   // bitset 所在头文件
using namespace std;

int main() {
    // 计算机中整数直接以补码存储,bitset<8> 取低8位显示
    int a = 5;
    int b = -5;
    cout << "+5 在内存中的8位二进制: " << bitset<8>(a) << endl;
    // 输出:00000101(正数三码合一)
    cout << "-5 在内存中的8位二进制: " << bitset<8>(b) << endl;
    // 输出:11111011(正是 -5 的补码)

    // 手工演示原码 -> 反码 -> 补码的推导过程
    // 注意:`0b` 前缀是 C++14 引入的特性,
    // 部分老旧环境(甚至 C++98)下 `0b` 写法编译不通过。
    int yuanma = 0b10000101;  // -5 的原码:符号位1 + 数值5
    int fanma  = 0b11111010;  // -5 的反码:符号位不变,数值位取反
    int buma   = 0b11111011;  // -5 的补码:反码 + 1
    cout << "原码: " << bitset<8>(yuanma) << endl;  // 10000101
    cout << "反码: " << bitset<8>(fanma)  << endl;  // 11111010
    cout << "补码: " << bitset<8>(buma)   << endl;  // 11111011

    return 0;
}

8.2 验证补码"减法变加法" ​

cpp
#include <iostream>
#include <bitset>
using namespace std;

int main() {
    // 1 + (-1):补码相加,进位丢弃后结果为 0
    int x = 1, y = -1;
    cout << x << " + (" << y << ") = " << x + y << endl;  // 0
    cout << bitset<8>(x) << " + " << bitset<8>(y)
         << " = " << bitset<8>(x + y) << endl;
    // 00000001 + 11111111 = 00000000(第9位的进位被丢弃)

    // 5 - 3 在硬件中就是 5 + (-3)
    cout << "5 + (-3) = " << 5 + (-3) << endl;  // 2
    return 0;
}

8.3 signed 与 unsigned:同一串 0/1,两种解读 ​

1111 1111 这 8 位,按有符号补码解释是 -1,按无符号解释是 255。类型决定了编码的含义!

cpp
#include <iostream>
#include <climits>
using namespace std;

int main() {
    // 同样的 8 位二进制 11111111
    signed char   sc = (signed char)255;   // 有符号:按补码解释
    unsigned char uc = 255;                // 无符号:全部是数值位
    cout << "signed char 解释 11111111:   " << (int)sc << endl;  // -1
    cout << "unsigned char 解释 11111111: " << (int)uc << endl;  // 255

    // 溢出演示:127 + 1(主流编译器下得到 -128,属于未定义/实现相关行为,考试按补码规则推断)
    // 注意:有符号整数溢出是未定义行为(UB),不同编译器/平台结果可能不同!
    // 以下代码仅为演示"在主流编译器下观察到补码回绕现象",不属于可移植代码。
    signed char c = 127;
    c = c + 1;
    cout << "signed char: 127 + 1 = " << (int)c << endl;  // -128,溢出!

    // char 的符号性由编译器/平台决定,移植代码时要小心
    cout << "当前平台 char 范围: " << CHAR_MIN << " ~ " << CHAR_MAX << endl;
    cout << "signed char 范围:   " << SCHAR_MIN << " ~ " << SCHAR_MAX << endl;  // -128 ~ 127
    cout << "unsigned char 范围: 0 ~ " << UCHAR_MAX << endl;                    // 0 ~ 255
    return 0;
}

⚠️ 易错点: char 到底是有符号还是无符号,由编译器和平台决定(x86 上的 GCC 默认为有符号);而 signed char 一定是 -128~127,unsigned char 一定是 0~255。三级考试中常考"同样的位模式在不同类型下的值"。


九、📝 实战练习 ​

以下 8 道题覆盖数据编码的核心考点。建议先独立完成,再对照解析查漏补缺。

练习题 1(单选题) ​

题目: 十进制数 -13 的 8 位二进制补码是( )

A. 1000 1101  B. 1111 0010  C. 1111 0011  D. 0111 0011

🔑 点击查看答案与解析

答案: C

解析:

13 = 8 + 4 + 1 = 0000 1101
-13 原码:1000 1101(符号位为1)
数值位取反:1111 0010(反码)
反码 +1:  1111 0011(补码)✅
  • A 是 -13 的原码,B 是 -13 的反码,都是经典干扰项;
  • D 符号位为 0,表示正数,直接排除。

选 C。


练习题 2(单选题) ​

题目: 8 位二进制补码 1011 0110 表示的十进制真值是( )

A. -74  B. -54  C. 74  D. 182

🔑 点击查看答案与解析

答案: A

解析: 符号位为 1,是负数补码,需要"再求一次补"得到原码:

补码:     1 011 0110
数值位取反:1 100 1001
加1:      1 100 1010  ← 原码

数值位 100 1010 = 64 + 8 + 2 = 74,加上符号位,真值为 -74。

注意:若把 1011 0110 误当作原码直接读,会得到 -54(即选项 B),这是最容易掉的坑!务必先看清题目问的是原码还是补码。

选 A。


练习题 3(单选题) ​

题目: 下列关于 8 位二进制编码的说法,正确的有( )

① 8 位补码能表示的整数范围是 -128 ~ 127 ② 补码中 0 有 0000 0000 和 1111 1111 两种表示 ③ 在 8 位补码下计算 127 + 1,结果为 -128,发生了溢出 ④ C++ 中 char 类型在所有平台上都一定是有符号的

A. ①②  B. ①③  C. ②④  D. ①③④

🔑 点击查看答案与解析

答案: B

解析:

  • ① 正确 ✅:n 位补码范围 -2ⁿ⁻¹ ~ 2ⁿ⁻¹-1,8 位即 -128 ~ 127。
  • ② 错误 ❌:补码最大的优点就是 0 的表示唯一(0000 0000);1111 1111 是 -1 的补码。有两个 0 的是原码和反码。
  • ③ 正确 ✅:0111 1111 + 0000 0001 = 1000 0000,两个正数相加得出负数(-128),符合同号相加结果变号的溢出判定规则。
  • ④ 错误 ❌:char 的符号性由编译器/平台决定,并不保证所有平台都有符号;想明确范围应使用 signed char 或 unsigned char。

选 B(①③)。


练习题 4(填空题) ​

题目:

(1) 8 位补码能表示的最小整数是 ______,最大整数是 ______,共能表示 ______ 个不同的整数。 (2) 16 位补码能表示的整数范围是 ______ ~ ______。 (3) 补码 1000 0000(8位)的真值是 ______。

🔑 点击查看答案与解析

答案: (1) -128、127、256 (2) -32768 ~ 32767 (3) -128

解析:

  • (1) n 位补码的范围是 -2^(n-1) ~ 2^(n-1)-1。8 位即 -2^7 ~ 2^7-1 = -128 ~ 127。总个数 = 2^8 = 256 个(128 个负数 + 1 个零 + 127 个正数 = 256)。
  • (2) 16 位补码:-2^15 ~ 2^15-1 = -32768 ~ 32767。
  • (3) 1000 0000 是补码中最小负数 -128 的编码,它没有对应的原码和反码,是补码"专属"的数。千万不要按原码理解成 -0!

🔥 记忆口诀:n 位补码,"负半边"比"正半边"多一个——因为 0 占了正半边的一个位置,而负半边没有正零的对应物。


练习题 5(判断题) ​

题目: 判断下列说法是否正确,正确打"√",错误打"×"。

(1) 正数的原码、反码、补码完全相同。( ) (2) 8 位原码能表示的范围是 -128 ~ 127。( ) (3) 补码中 0 的表示是唯一的。( ) (4) 两个负数相加,结果符号位为 0,说明发生了溢出。( ) (5) C++ 中 char 类型在所有平台上都是有符号的。( ) (6) -1 的 8 位补码是 1111 1111。( )

🔑 点击查看答案与解析

答案: (1) √ (2) × (3) √ (4) √ (5) × (6) √

解析:

  • (1) √:正数"三码合一",这是基础结论。
  • (2) ×:-128 ~ 127 是补码的范围。8 位原码的范围是 -127 ~ 127(-0 浪费了一个编码)。
  • (3) √:补码最大的优点之一就是零的表示唯一(0000 0000)。
  • (4) √:两个负数相加应该还是负数(符号位为 1),如果符号位变成 0,说明发生了负溢出(下溢)。
  • (5) ×:char 的符号性由编译器和平台决定,移植代码时要注意。
  • (6) √:-1 的原码 1000 0001 → 反码 1111 1110 → 补码 1111 1111。也可以用公式验证:全 1 的补码 = -1。

练习题 6(单选题) ​

题目: 16 位二进制补码 1111 1111 1111 1100 表示的十进制数是( )

A. -4  B. -3  C. 65532  D. -12

🔑 点击查看答案与解析

答案: A

解析: 符号位为 1,是负数。对补码的数值位"再求补"得到原码:

补码:     1 111 1111 1111 1100
数值位取反:1 000 0000 0000 0011
加1:      1 000 0000 0000 0100  ← 原码

数值位 000 0000 0000 0100 = 4,加上符号位,真值为 -4。

💡 快速技巧:也可以用"从右往左找第一个 1"的方法。补码 ...11111100,从右数第一个 1 在第 2 位(从 0 开始数),该 1 右侧不变(都是 0),左侧数值位全部取反 → ...00000100,即 4,所以真值是 -4。

⚠️ 易错提醒:选项 C(65532)是把它当 unsigned 解读的结果。题目没说无符号,默认按有符号补码解读。


练习题 7(单选题) ​

题目: 在 8 位下,同一二进制 1111 1111 按 signed char 和 unsigned char 解读,值分别是( )

A. -1 和 255  B. -127 和 127  C. 255 和 -1  D. -1 和 127

🔑 点击查看答案与解析

答案: A

解析:

  • signed char(有符号字符):按补码解释,最高位是符号位。1111 1111 符号位为 1,是负数。对数值位再求补 → 000 0001 = 1,所以真值是 -1。
  • unsigned char(无符号字符):所有位都是数值位,没有符号位。1111 1111 = 128+64+32+16+8+4+2+1 = 255。

同一串 0/1,类型不同,解读完全不同!选 A。

🍎 生活比喻:同样是一张写着"110"的纸条,在警察局是报警电话,在医院是急救中心,在学校可能是学号。上下文(类型)决定了含义。


练习题 8(程序阅读题) ​

题目: 阅读以下 C++ 程序,输出结果是( )

cpp
#include <iostream>
using namespace std;

int main() {
    signed char c = 127;
    c = c + 1;
    cout << (int)c << endl;
    return 0;
}

(注:主流编译器下,signed char 溢出按补码回绕处理)

A. 128  B. -128  C. 0  D. -1

🔑 点击查看答案与解析

答案: B

解析:

signed char 的范围是 -128 ~ 127。127 已经是最大值,再加 1 就溢出了。

用补码手算:

  127 = 0111 1111
+   1 = 0000 0001
-----------------
        1000 0000   ← 符号位变成了 1

1000 0000 按 8 位补码解释就是 -128(补码的最小负数)。

两个正数相加结果变成了负数——这就是典型的正溢出(上溢)。

选 B。

⚠️ 注意:严格来说,有符号整数溢出在 C++ 标准中是"未定义行为"(UB),不同编译器结果可能不同。但在 GESP 考试中,通常按"补码回绕"的规则来考,即 127+1 = -128。


十、📊 考点总结表 ​

考点重要程度出题形式易错点
机器数与真值、符号位⭐⭐⭐选择/判断把符号位当数值位直接计算;混淆机器数和真值
原码表示法⭐⭐⭐选择忘记 0 有两个原码;范围记成 -128~127
反码表示法⭐⭐选择/填空负数反码符号位也跟着取反
补码表示法(求补码)⭐⭐⭐⭐⭐选择/程序阅读忘记"+1";符号位参与取反
补码的优点与表示范围⭐⭐⭐⭐⭐选择/判断范围记成 -127~127;不知 -128 无原码/反码
已知补码求真值⭐⭐⭐⭐选择把补码当原码读;不会用"再求补"法
溢出判断⭐⭐⭐⭐选择/判断以为异号相加会溢出;不会判断正负溢出
signed/unsigned、char⭐⭐⭐选择/程序阅读以为 char 处处有符号;同一位模式两种值
补码减法变加法原理⭐⭐⭐⭐选择/判断不理解为什么补码能让减法变成加法
快速求补码技巧⭐⭐⭐手算提速"从右数第一个1"法记不牢

⭐ 必背速记卡 ​

┌──────────────────────────────────────────────────────┐
│              ⭐ 数据编码必背速记卡                       │
├──────────────────────────────────────────────────────┤
│  1. 正数三码合一;负数:原码→反码(数值位取反)→补码(+1) │
│  2. n位补码范围:-2^(n-1) ~ 2^(n-1)-1                 │
│  3. 8位补码:-128 ~ 127;16位:-32768 ~ 32767        │
│  4. -128 的补码是 10000000,没有对应的原码和反码       │
│  5. 补码三大优势:减法变加法、零唯一、多一个最小负数   │
│  6. 已知补码求真值:负数的数值位"再求补"(取反+1)     │
│  7. 快速求补码:从右找第一个1,该1及右侧不变,左侧取反 │
│  8. 同号相加结果变号 → 溢出;异号相加永不溢出          │
│  9. 全1补码 = -1;全0补码 = 0                         │
│ 10. 按补码解释 11111111 = -1,按无符号解释 = 255       │
│ 11. char 的符号性由平台决定,signed char 一定有符号    │
│ 12. 钟表调时理解补码:模12系统中,-2 和 +10 等价        │
└──────────────────────────────────────────────────────┘

百炼成钢,融会贯通