Back to Notes

计算机系统 - 数据表达

This post is not yet available in English. Showing the original version.
Table of Contents
Table of Contents

一、整数编码

计算机中整数有两种基本编码方式:无符号编码(只表示非负整数)和补码编码(表示有符号整数,含负数)。两者共享同样的二进制位,但解读规则不同——同一串比特,用不同编码会得到不同的数值。


1.1 无符号数编码(Unsigned)

B2U 函数定义

对一个 ww 位的二进制向量 x=[xw1,xw2,,x0]\vec{x} = [x_{w-1},\, x_{w-2},\, \dots,\, x_0]无符号解码函数 B2U(Binary to Unsigned)定义为:

B2Uw(x)=i=0w1xi2iB2U_w(\vec{x}) = \sum_{i=0}^{w-1} x_i \cdot 2^i

每一位的权重都是正的 2i2^i,所有位做简单的加权求和。

示例(w=4w = 4

二进制计算过程十进制(无符号)
0000000
000120=12^0 = 11
010122+20=52^2 + 2^0 = 55
111123+22+21+20=152^3 + 2^2 + 2^1 + 2^0 = 1515
100023=82^3 = 88

取值范围

0B2Uw(x)2w10 \leq B2U_w(\vec{x}) \leq 2^w - 1
位宽 ww最小值最大值 UMaxwUMax_w
80255
16065 535
3204 294 967 295

直觉:无符号编码就是我们日常做的"二进制转十进制",没有任何特殊处理,ww 位能表示 002w12^w - 12w2^w 个值。


1.2 二进制补码编码(Two's Complement)

补码是现代计算机表示有符号整数的标准方式,也是 CSAPP 的重点。

B2T 函数定义

对同样的 ww 位二进制向量 x=[xw1,xw2,,x0]\vec{x} = [x_{w-1},\, x_{w-2},\, \dots,\, x_0]补码解码函数 B2T(Binary to Two's complement)定义为:

B2Tw(x)=xw12w1+i=0w2xi2iB2T_w(\vec{x}) = -x_{w-1} \cdot 2^{w-1} + \sum_{i=0}^{w-2} x_i \cdot 2^i

和 B2U 唯一的区别:最高位 xw1x_{w-1} 的权重是负的 2w1-2^{w-1},其余低位的权重仍然是正的。

示例(w=4w = 4

二进制计算过程十进制(补码)
0000000
000120=12^0 = 11
010122+20=52^2 + 2^0 = 55
011122+21+20=72^2 + 2^1 + 2^0 = 77TMax4TMax_4
100023=8\mathbf{-2^3} = -8−8TMin4TMin_4
100123+20=7-2^3 + 2^0 = -7−7
111123+22+21+20=1-2^3 + 2^2 + 2^1 + 2^0 = -1−1

注意 10000111 的对比:最高位一翻,数值从 +7+7 变成 8-8,这就是最高位"负权重"的威力。

取值范围

2w1B2Tw(x)2w11-2^{w-1} \leq B2T_w(\vec{x}) \leq 2^{w-1} - 1
位宽 wwTMinwTMin_w(最小值)TMaxwTMax_w(最大值)
8−128127
16−32 76832 767
32−2 147 483 6482 147 483 647

不对称性TMin=TMax+1|TMin| = |TMax| + 1,即负数比正数多一个(因为 00 占了一个正数的编码位置)。这一性质会导致补码取反的陷阱:-TMin 在同位宽下会溢出,仍然等于 TMin


1.3 补码的本质:最高位的负权重

理解补码最关键的一句话:

补码和无符号数的唯一区别,就是最高位(MSB)的权重从 +2w1+2^{w-1} 变成了 2w1-2^{w-1}

把两个公式放在一起对比:

无符号 B2UB2U补码 B2TB2T
最高位权重+2w1+2^{w-1}2w1\mathbf{-2^{w-1}}
其余位权重+2i+2^i(不变)+2i+2^i(不变)
最高位含义单纯的"大数值贡献"符号位:决定正负

直观理解

为什么 1111...1 = −1 而不是最小值?

w=4w = 4 为例:

B2T4(1111)=238+22+21+20+7=1B2T_4(1111) = \underbrace{-2^3}_{-8} + \underbrace{2^2 + 2^1 + 2^0}_{+7} = -1

低位全 1 产生的正权重 (+7+7) 几乎完全抵消了最高位的负权重 (8-8),只差 11。所以1 不是最小值,而是 1-1

相反,1000 才是最小值(TMinTMin),因为只有负权重在起作用,没有任何正位去抵消它。

补码的时钟模型:模运算的圆盘

补码的数值空间本质上是一个——就像时钟盘一样首尾相连。ww 位二进制的所有运算都在 mod2w\mod 2^w 下进行,2w2^w 个编码均匀排列在圆盘上:

flowchart LR
    subgraph 正半边
        direction LR
        n0["0\n0000"] --> n1["+1\n0001"] --> n2["+2\n0010"] --> n3["+3\n0011"]
        n3 --> n4["+4\n0100"] --> n5["+5\n0101"] --> n6["+6\n0110"] --> n7["+7\n0111"]
    end

    subgraph 负半边
        direction RL
        m8["-8\n1000"] --> m7["-7\n1001"] --> m6["-6\n1010"] --> m5["-5\n1011"]
        m5 --> m4["-4\n1100"] --> m3["-3\n1101"] --> m2["-2\n1110"] --> m1["-1\n1111"]
    end

    n7 -->|"⚡ 正溢出\n+7 → −8"| m8
    m1 -->|"−1 + 1 = 0"| n0

    linkStyle 14 stroke:#d62728,stroke-width:3px
    linkStyle 15 stroke:#2ca02c,stroke-width:3px

关键观察:

模运算本质ww 位补码的加减法,其实就是在一个周长为 2w2^w 的圆盘上"顺时针/逆时针走步"。走过边界时不会报错,只会绕回来——这就是整数溢出的几何含义。和 12 小时制的时钟一模一样:1111 点再过 22 小时不是 1313 点,而是 11 点。

与原码、反码的关系

编码方式缺点补码的改进
原码(符号 + 绝对值)+0+00-0 两个零补码只有一个零
反码(符号位不变,其余取反)仍有双零问题补码消除了双零
补码(负权重定义)加法器天然支持,硬件实现最简单

硬件上的优势:补码的加法和无符号加法共用同一个加法器电路——CPU 不需要关心操作数是有符号还是无符号,同样的二进制加法就能得出正确结果(溢出行为除外)。这是补码被选为标准有符号编码的根本原因。


1.4 有符号与无符号的映射关系(T2U / U2T)

同一个 ww 位二进制向量,在两种编码下的数值关系:

B2Uw(x)={B2Tw(x)若 xw1=0(非负数,解读一致)B2Tw(x)+2w若 xw1=1(补码为负,无符号 = 补码值 +2wB2U_w(\vec{x}) = \begin{cases} B2T_w(\vec{x}) & \text{若 } x_{w-1} = 0 \text{(非负数,解读一致)} \\[4pt] B2T_w(\vec{x}) + 2^w & \text{若 } x_{w-1} = 1 \text{(补码为负,无符号 = 补码值 } + 2^w\text{)} \end{cases}

T2U 与 U2T 转换公式

T2U(有符号 → 无符号):

T2Uw(x)={xx0x+2wx<0T2U_w(x) = \begin{cases} x & x \geq 0 \\ x + 2^w & x < 0 \end{cases}

U2T(无符号 → 有符号):

U2Tw(u)={uu2w11(即 uTMaxu2wu2w1(即 u>TMaxU2T_w(u) = \begin{cases} u & u \leq 2^{w-1} - 1 \text{(即 } u \leq TMax\text{)} \\ u - 2^w & u \geq 2^{w-1} \text{(即 } u > TMax\text{)} \end{cases}

两个公式互为逆运算。位模式始终不变,只是在圆盘上换了一套编号。

举例(w=4w = 4):

转换输入输出计算
T2U6-610106+16=10-6 + 16 = 10
T2U5555非负,不变
T2U1-115151+16=15-1 + 16 = 15
U2T10106-61016=610 - 16 = -610>710 > 7
U2T5555575 \leq 7,不变

⚠️ 经典陷阱

陷阱一:有符号 vs 无符号比较

-1 < 0U   // false!

-1 被隐式转为 unsigned:位模式 0xFFFFFFFF → 无符号 42949672954294967295,比 00 大。

陷阱二:unsigned 的循环永远不会 < 0

unsigned i;
for (i = n - 1; i >= 0; i--)  // ⚠️ 死循环!unsigned 永远 >= 0
    // ...

i 减到 0 再减 1,结果不是 1-1 而是 UMaxUMax(圆盘绕回去了),条件永远为真。

陷阱三:size_t(无符号)相减可能绕回

// strlen() 返回 size_t(无符号)
if (strlen(a) - strlen(b) >= 0)  // ⚠️ 永远为真!

无符号减法不会产生负数。如果 strlen(a) < strlen(b),结果会绕回成一个巨大的正数。正确写法:strlen(a) >= strlen(b)

陷阱四:TMinTMin 取负溢出

int x = INT_MIN;     // -2147483648
int y = -x;          // 仍然是 -2147483648!

TMin-TMin 在补码中无法表示(因为 TMin>TMax|TMin| > TMax),溢出后绕回自身。在圆盘上看:TMinTMin 的对称点超出了正半边的最大值,又回到了 TMinTMin

C 语言中的隐式转换规则:当有符号与无符号数混合运算时,C 会将有符号数隐式转为无符号数(位模式不变,解读规则变了)……几乎所有的 unsigned 陷阱都来源于此。


二、位数转换:扩展与截断

当不同位宽的整数需要互相赋值时,位数会发生变化。核心问题:增减比特时,如何保持数值不变(或可预测地变化)?


2.1 符号扩展(Sign Extension)—— 小位宽 → 大位宽(有符号数)

将一个 ww 位补码数扩展到 w>ww' > w 位时,在高位填充符号位(即 MSB 的值)。

规则

[xw1,xw2,,x0]符号扩展[xw1,xw1,,xw1ww 个,xw1,xw2,,x0][x_{w-1},\, x_{w-2},\, \dots,\, x_0] \xrightarrow{\text{符号扩展}} [\underbrace{x_{w-1},\, x_{w-1},\, \dots,\, x_{w-1}}_{w'-w \text{ 个}},\, x_{w-1},\, x_{w-2},\, \dots,\, x_0]

一句话:正数前面补 0,负数前面补 1

示例(4 位 → 8 位)

4 位补码十进制符号扩展到 8 位验证十进制
0101+5+50000 0101+5+5
0111+7+70000 0111+7+7
10106-61111 10106-6
10008-8`1111 1000$8-8
11111-11111 11111-1

注意 6-61010)扩展后变成 1111 1010,看起来多了很多 1,但数值完全不变

为什么符号扩展是正确的?

以负数为例理解(正数补 0 显然正确,略过)。

将 4 位补码 6-61010)扩展到 5 位(11010),验证 B2T 值不变:

原始 4 位

B2T4(1010)=23+21=8+2=6B2T_4(1010) = -2^3 + 2^1 = -8 + 2 = -6

扩展到 5 位(高位补 1):

B2T5(11010)=24+23+21=16+8+2=6B2T_5(11010) = -2^4 + 2^3 + 2^1 = -16 + 8 + 2 = -6 \quad ✓

本质:新增的高位 1 贡献了 24-2^4,但原来的最高位从"负权重 23-2^3"变成了"正权重 +23+2^3",两者之差恰好抵消:

24+23=16+8=8=23(等于原来的负权重)-2^4 + 2^3 = -16 + 8 = -8 = -2^3 \quad \text{(等于原来的负权重)}

每多扩展一位,这个"2k+2k1=2k1-2^k + 2^{k-1} = -2^{k-1}"的抵消关系都成立,所以可以一直往高位补符号位。

数学归纳:每扩展一位,新增一个负权重 2k-2^k,同时原最高位变为正权重 +2k1+2^{k-1},两者之和 =2k1= -2^{k-1},与扩展前的最高位负权重相同。因此,扩展任意多位都不改变数值。


2.2 零扩展(Zero Extension)—— 小位宽 → 大位宽(无符号数)

无符号数的扩展更简单:高位全部补 0

[xw1,xw2,,x0]零扩展[0,0,,0ww 个,xw1,xw2,,x0][x_{w-1},\, x_{w-2},\, \dots,\, x_0] \xrightarrow{\text{零扩展}} [\underbrace{0,\, 0,\, \dots,\, 0}_{w'-w \text{ 个}},\, x_{w-1},\, x_{w-2},\, \dots,\, x_0]

示例(4 位 → 8 位)

4 位无符号十进制零扩展到 8 位验证
010150000 01015 ✓
1010100000 101010 ✓
1111150000 111115 ✓

因为 B2U 公式中每一位的权重都是正的,高位补 0 不增加任何权重值,数值自然不变。

对比:符号扩展 vs 零扩展

符号扩展零扩展
适用类型有符号数(补码)无符号数
填充内容符号位(MSB)的值0
正数时0(和零扩展一样)0
负数时1不适用(无符号没有负数)
x86 指令movsx / movsxd(sign extend)movzx(zero extend)
# x86-64 示例
movsbl  %al, %eax    # 将 al(8位有符号)符号扩展到 eax(32位)
movzbl  %al, %eax    # 将 al(8位无符号)零扩展到 eax(32位)
movslq  %eax, %rax   # 将 eax(32位有符号)符号扩展到 rax(64位)

C 编译器的选择char → int 默认使用符号扩展(因为 char 默认有符号);unsigned char → int 使用零扩展。int → long 使用符号扩展。


2.3 截断(Truncation)—— 大位宽 → 小位宽

ww 位截断为 k<wk < w 位时,直接丢弃高 wkw - k 位,只保留低 kk

[xw1,,xk,xk1,,x0]截断为 k 位[xk1,,x0][x_{w-1},\, \dots,\, x_k,\, x_{k-1},\, \dots,\, x_0] \xrightarrow{\text{截断为 } k \text{ 位}} [x_{k-1},\, \dots,\, x_0]

直觉:十进制的"截断"

先想十进制:把 1234\textbf{1}234 保留后 22 位得 3434。这等价于 1234mod100=341234 \mod 100 = 34——砍掉高位数字,就是10k10^k 取模kk = 保留的位数)。

二进制完全一样:把 0001 01012121)保留低 44 位得 010155),就是 21mod24=21mod16=521 \mod 2^4 = 21 \mod 16 = 5砍掉高位 bit = 对 2k2^k 取模。

所谓"取模",就是用 2k2^k 去除,只留余数。余数恰好就是低 kk 位保存的信息,高位对低 kk 位没有任何贡献(因为高位每一位的权重都是 2k2^k 的整倍数,被模运算整除了)。

无符号截断

B2Uk([xk1,,x0])=B2Uw(x)mod2kB2U_k([x_{k-1},\, \dots,\, x_0]) = B2U_w(\vec{x}) \mod 2^k

补码截断

先按无符号截断(取模),然后将结果转回补码解读:

B2Tk([xk1,,x0])=U2Tk(B2Uw(x)mod2k)B2T_k([x_{k-1},\, \dots,\, x_0]) = U2T_k(B2U_w(\vec{x}) \mod 2^k)

示例(8 位 → 4 位)

8 位二进制无符号值截断后低 4 位无符号结果补码结果
0001 01012101015(21mod1621 \mod 165
1111 1010250101010(250mod16250 \mod 16−6
0000 111115111115(15mod1615 \mod 16−1
0001 00111900113(19mod1619 \mod 163

截断不保值:和扩展不同,截断几乎一定会改变数值(除非高位本来就全是 0 或全是符号位)。截断本质就是强制绕圆盘——用更小的圆盘(2k2^k 个编码)去"套"一个大数,自然只能保留余数。

截断何时"安全"?

只有当原始值恰好在目标位宽的表示范围内时,截断才不改变数值:

int x = 53;
short sx = (short)x;   // 53 在 short 范围内,安全
int y = 100000;
short sy = (short)y;   // 100000 超出 short 范围(-32768~32767),截断后值变了!
// sy = 100000 mod 65536 = 34464 → 补码解读 = -31072

2.4 C 语言中的隐式扩展与截断

C 编译器在赋值和运算时自动处理位宽转换:

C 操作位宽变化编译器行为
char → int8 → 32符号扩展(默认 char 有符号)
unsigned char → int8 → 32零扩展
short → int16 → 32符号扩展
int → long32 → 64符号扩展
int → short32 → 16截断(丢弃高16位)
int → char32 → 8截断(丢弃高24位)

⚠️ 同时涉及扩展 + 类型转换时的顺序

位宽变化有符号/无符号转换同时发生时,C 的规则是先扩展位宽,再转换类型

short sx = -12345;            // 16位补码:0xCFC7
unsigned uy = sx;             // short → unsigned (16位 → 32位 + 有符号→无符号)

// 实际执行顺序:
// ① 先符号扩展:short(-12345) → int(-12345)   位模式:0xFFFFCFC7
// ② 再转 unsigned:int(-12345) → unsigned      位模式不变:0xFFFFCFC7 = 4294954951

如果先截断再扩展,或先转类型再扩展,结果可能完全不同。C 标准规定的顺序是先改大小,再改符号


三、整数运算

硬件加法器的位宽是固定的(ww 位),当运算结果超出 ww 位能表示的范围时,高位被丢弃——这就是溢出。本章讨论在这种有限位宽下,加法、取反、乘法、除法分别如何工作。


3.1 无符号加法(UAdd)

定义

两个 ww 位无符号数相加,真实算术结果可能需要 w+1w+1 位。硬件只保留低 ww 位,等价于 mod2w\mod 2^w

UAddw(u,v)=(u+v)mod2wUAdd_w(u,\, v) = (u + v) \mod 2^w

溢出判断

UAddw(u,v)={u+v若 u+v<2w(正常)u+v2w若 u+v2w(溢出,丢掉进位)UAdd_w(u,\, v) = \begin{cases} u + v & \text{若 } u + v < 2^w \text{(正常)} \\ u + v - 2^w & \text{若 } u + v \geq 2^w \text{(溢出,丢掉进位)} \end{cases}

检测溢出:如果结果 s=UAdd(u,v)s = UAdd(u, v),则溢出     \iff s<us < u(或 s<vs < v)。

直觉:两个正数相加,结果反而比其中一个还小,一定是绕了一圈。

示例(w=4w = 4,范围 0150 \sim 15

uuvv真实和UAdd4UAdd_4溢出?
951414
129212116=521 - 16 = 5
151161616=016 - 16 = 0

圆盘视角:从 uu 出发顺时针走 vv 步,如果越过 00(从 1515 绕回 00),就是溢出。


3.2 补码加法(TAdd)

定义

补码加法的位级运算和无符号加法完全相同(CPU 用同一个加法器),只是对结果按补码解读:

TAddw(u,v)=U2Tw((u+v)mod2w)TAdd_w(u,\, v) = U2T_w\big((u + v) \mod 2^w\big)

展开后:

TAddw(u,v)={u+v2w若 u+v2w1(正溢出:和太大,绕到负数)u+v若 2w1u+v<2w1(正常)u+v+2w若 u+v<2w1(负溢出:和太小,绕到正数)TAdd_w(u,\, v) = \begin{cases} u + v - 2^w & \text{若 } u + v \geq 2^{w-1} \text{(正溢出:和太大,绕到负数)} \\ u + v & \text{若 } -2^{w-1} \leq u + v < 2^{w-1} \text{(正常)} \\ u + v + 2^w & \text{若 } u + v < -2^{w-1} \text{(负溢出:和太小,绕到正数)} \end{cases}

溢出判断

溢出类型条件结果表现
正溢出u>0u > 0v>0v > 0s0s \leq 0两正数之和成了负数
负溢出u<0u < 0v<0v < 0s0s \geq 0两负数之和成了正数
不溢出符号不同,或同号但结果符号一致

关键:正 + 正 = 负 → 正溢出;负 + 负 = 正 → 负溢出。一正一负永远不会溢出(因为结果的绝对值不可能超出范围)。

示例(w=4w = 4,范围 8+7-8 \sim +7

uuvv真实和TAdd4TAdd_4溢出类型
538816=88 - 16 = -8正溢出
−7−3−1010+16=6-10 + 16 = 6负溢出
5−322
−53−2−2

圆盘视角:从 uu 出发走 vv 步,越过 +78+7 \to -8 的边界就是正溢出,越过 8+7-8 \to +7 的边界就是负溢出。

💡 实战:算法题"不开 long long 见祖宗"

写算法题时,两个 int 相加或相乘的结果超过 23112^{31} - 1(约 2.1×1092.1 \times 10^9),就会正溢出变成负数——这就是补码加法/乘法的圆盘绕回:

int a = 1000000000;  // 10^9
int b = 1000000000;
int c = a + b;       // 真实值 2×10^9,超过 TMax(int) = 2147483647
                     // 结果:c = 2000000000 - 2^32 = -294967296(负数!)

原因就在 TAddTAdd:真实和 231\geq 2^{31} 时,减去 2322^{32} 绕到负半边。解决方案:换成 long long(64 位),TMaxTMax 变为 9.2×10189.2 \times 10^{18},溢出的圆盘大了 2322^{32} 倍。


3.3 取反(Negation)

无符号取反(UNeg)

UNegw(u)={0若 u=02wu若 u>0UNeg_w(u) = \begin{cases} 0 & \text{若 } u = 0 \\ 2^w - u & \text{若 } u > 0 \end{cases}

圆盘上 uu 关于 00 的对称点:从 00 往反方向走 uu 步。

补码取反(TNeg)

TNegw(x)={TMinw若 x=TMinw(特殊!取反后溢出,回到自身)x其他情况TNeg_w(x) = \begin{cases} TMin_w & \text{若 } x = TMin_w \text{(特殊!取反后溢出,回到自身)} \\ -x & \text{其他情况} \end{cases}

TMinTMin 的取反是唯一的陷阱:(8)=8-(-8) = 8,但 4 位补码最大只能表示 77,溢出后绕回 8-8

位级操作求补码取反(实用技巧):

x= ⁣x+1(按位取反再加 1)-x = \,\sim\!x + 1 \quad \text{(按位取反再加 1)}
// 例:x = 5 (0101)
// ~x = 1010 (补码 -6)
// ~x + 1 = 1011 (补码 -5) ✓

3.4 无符号乘法(UMult)

两个 ww 位无符号数相乘,真实结果可能需要 2w2w 位。硬件只保留低 ww 位:

UMultw(u,v)=(u×v)mod2wUMult_w(u,\, v) = (u \times v) \mod 2^w

示例(w=4w = 4

uuvv真实积UMult4UMult_4
531515
552525mod16=925 \mod 16 = 9
1515225225mod16=1225 \mod 16 = 1

3.5 补码乘法(TMult)

补码乘法的结果同样截断为低 ww 位,但按补码解读:

TMultw(u,v)=U2Tw((u×v)mod2w)TMult_w(u,\, v) = U2T_w\big((u \times v) \mod 2^w\big)

关键性质:无符号乘法和补码乘法的位级结果完全相同——同样的 ww 位二进制模式,只是解读方式不同。

示例(w=4w = 4

uu(补码)vv(补码)真实积截断后位模式补码解读无符号解读
53151111−115
−3−5151111−115
−17−71001−79

注意 5×3=155 \times 3 = 15,但 4 位补码中 1111 = 1-1,不是 1515乘法非常容易溢出,因为结果增长是平方级的。


3.6 乘以常数:编译器的移位优化

整数乘法指令在 CPU 上通常需要 3~10 个时钟周期,而移位和加法只需 1 个周期。编译器会自动将乘以常数优化为移位 + 加减法的组合。

核心:乘以 2k2^k = 左移 kk

x×2k=xkx \times 2^k = x \ll k

这对无符号和补码都成立(溢出时高位自然丢弃,与截断行为一致)。

分解任意常数

编译器将常数拆成 22 的幂之和(或差),用移位 + 加减组合:

乘法分解移位 + 加减汇编
x * 32+12 + 1(x1)+x(x \ll 1) + xleaq (%rax,%rax,2), %rax
x * 54+14 + 1(x2)+x(x \ll 2) + xleaq (%rax,%rax,4), %rax
x * 64+24 + 2(x2)+(x1)(x \ll 2) + (x \ll 1)移位 + 加法
x * 7818 - 1(x3)x(x \ll 3) - x移位 + 减法
x * 128+48 + 4(x3)+(x2)(x \ll 3) + (x \ll 2)移位 + 加法
x * 1516116 - 1(x4)x(x \ll 4) - x移位 + 减法

lea 指令的再次登场:回忆第一篇笔记中 lea 被编译器借用做乘加优化——leaq (%rax,%rax,2), %rax 就是 rax = rax * 3,这正是上面的常数乘法优化。


3.7 除以 2 的幂:右移

先搞清两种取整

整数除法遇到除不尽时,有两种常见的舍入方式:

取整方式含义7÷27 \div 27÷2-7 \div 2
向下取整(floor,\lfloor \rfloor数轴左边(负无穷方向)取最近整数333.533.5 \to 34-43.54-3.5 \to -4
向零取整(truncate)靠近零的方向取最近整数333.533.5 \to 33-33.53-3.5 \to -3

正数时两者一致(都是砍掉小数部分)。负数时不同3.5-3.5 向下取整是 4-4(更小),向零取整是 3-3(更靠近零)。C 语言的 / 运算符采用向零取整

无符号除法:逻辑右移(补 0

u÷2k=uk(逻辑右移,结果向下取整/截断)u \div 2^k = u \gg k \quad \text{(逻辑右移,结果向下取整/截断)}
unsigned u = 13;   // 1101
u >> 1;            // 0110 = 6  (13/2 = 6.5 → 6)
u >> 2;            // 0011 = 3  (13/4 = 3.25 → 3)

逻辑右移总是向零方向舍入(floor),对非负数来说这就是正常的整除截断。

补码除法:算术右移(补符号位)

对有符号数,右移使用算术右移(高位补符号位):

xk=x/2k(向下取整,即向负无穷方向舍入)x \gg k = \lfloor x / 2^k \rfloor \quad \text{(向下取整,即向负无穷方向舍入)}

问题:对负数来说,"向下取整"≠ "向零取整":

int x = -7;   // 补码 1...11001
x >> 1;       // 算术右移:1...11100 = -4  (-7/2 = -3.5 → 向下取整 = -4)
// 但 C 语言要求整数除法向零取整:-7/2 应该 = -3

偏置修正(Biasing)

为了让算术右移得到向零取整(C 的除法语义),对负数需要在移位前加一个偏置:

对 x<0(x+2k1)/2k=x/2k\text{对 } x < 0:\quad \lfloor (x + 2^k - 1) / 2^k \rfloor = \lceil x / 2^k \rceil

即:先加 (2k1)(2^k - 1),再做算术右移

// 编译器将 x / 4(x 为有符号)翻译为:
// ① 检查 x 是否为负
// ② 若负,加偏置 (4-1)=3
// ③ 算术右移 2 位
# GCC 编译 int y = x / 4; 的典型输出
movl    %edi, %eax
sarl    $31, %eax       # eax = x 的符号位扩展(全0或全1)
shrl    $30, %eax       # eax = 0(正数)或 3(负数)← 偏置值 2^k - 1
addl    %eax, %edi      # x += 偏置(仅对负数有效)
sarl    $2, %edi        # x >>= 2(算术右移,即 ÷4)

对比:右移除法 vs 真正除法

正数负数
逻辑右移 >>等于 x/2k\lfloor x/2^k \rfloor(正确)不适用(无符号无负数)
算术右移 >>等于 x/2k\lfloor x/2^k \rfloor(正确)x/2k\lfloor x/2^k \rfloor(向负无穷取整,不是 C 的 /
算术右移 + 偏置x/2k\lceil x/2^k \rceil(向零取整,等于 C 的 /

为什么编译器不直接用除法指令? 因为 div / idiv 指令通常需要 20~40 个时钟周期,而移位 + 加法只需 1~2 个周期。除以 22 的幂时,性能差距可达 20 倍


3.8 整数运算总览

运算公式溢出行为位级等价
UAddUAdd(u+v)mod2w(u+v) \mod 2^w进位丢失,结果变小与 TAdd 相同
TAddTAddU2T((u+v)mod2w)U2T((u+v) \mod 2^w)正溢出→负,负溢出→正与 UAdd 相同
UMultUMult(u×v)mod2w(u \times v) \mod 2^w高位截断与 TMult 相同
TMultTMultU2T((u×v)mod2w)U2T((u \times v) \mod 2^w)高位截断与 UMult 相同
×2k\times 2^kxkx \ll k同加法溢出无符号/补码通用
÷2k\div 2^k(无符号)xLkx \gg_L k(逻辑右移)向零取整
÷2k\div 2^k(补码)(x+bias)Ak(x + \text{bias}) \gg_A k偏置后向零取整

最重要的一句话:无符号和补码的加法、乘法在位级上完全相同——CPU 用同一组电路完成运算,只在解读结果时才区分有符号/无符号。这就是补码被选为标准编码的根本优势。


四、浮点数(Floating Point)

浮点数用于表示实数,现代计算机统一采用 IEEE 754 标准。在了解标准之前,我们需要先直观地理解,计算机是如何用二进制表示小数的。


4.1 二进制小数(Fractional Binary Numbers)

在十进制中,小数点左边数字的权重是 100,101,10210^0, 10^1, 10^2 \dots,右边数字的权重是 101,10210^{-1}, 10^{-2} \dots。例如:
12.3410=1×101+2×100+3×101+4×10212.34_{10} = 1 \times 10^1 + 2 \times 10^0 + 3 \times 10^{-1} + 4 \times 10^{-2}

二进制小数的原理完全相同,只是权重变成了 22 的幂。小数点左边的权重是 20,21,222^0, 2^1, 2^2 \dots,右边是 21,222^{-1}, 2^{-2} \dots12,14,18\frac{1}{2}, \frac{1}{4}, \frac{1}{8} \dots

直观示例

二进制权重计算式十进制值
101.114+0+1+12+144 + 0 + 1 + \frac{1}{2} + \frac{1}{4}5.755.75
0.112\frac{1}{2}0.50.5
0.0114\frac{1}{4}0.250.25
0.00118\frac{1}{8}0.1250.125

关键认知:二进制小数只能精确表示那些分母是 22 的次方的数(比如 0.5,0.25,0.750.5, 0.25, 0.75 也就是 3/43/4)。如果遇到像 0.20.2(即 15\frac{1}{5})或者 0.10.1110\frac{1}{10})这样的数,无论是用几位二进制去拼凑(如 116+132\frac{1}{16} + \frac{1}{32} \dots),都只能通过无限循环去逼近,永远无法绝对精确地表示!这就是为什么在很多语言中 0.1 + 0.2 != 0.3 的根本原因。


4.2 IEEE 754 的基本结构:二进制的科学记数法

浮点数的核心可以直接对应到科学记数法,只是从十进制平移到了二进制。

回忆一下熟悉的十进制科学记数法表示法:1.23×104-1.23 \times 10^4

IEEE 754 其实就是把上述过程平移到了二进制(逢二进一)

V=(1)s×M×2EV = (-1)^s \times M \times 2^E
  1. 符号(Sign, ss:就像上面的负数标记。
    • 0 代表正数,1 代表负数。这非常稳定地占据了总比特位中最高的 1 位
  2. 阶码(Exponent, EE:就像 10410^4 里面的 44
    • 决定数值在数轴上的大致位置(它是成万还是微米级别)。
    • 在底层的编码里,这段专门划分出来的存放阶码的区域叫 exp,并且占据了 kk 个比特位。
  3. 尾数(Significand/Mantissa, MM:就像 1.231.23 里面的 123123
    • 决定了数值有多精确。你给出的尾数空间越长,你的数值越不易被模糊化。
    • 在底层的编码里,这就叫 frac,即有效位的存储区,占据剩下的 nn 个比特位。

常见精度对应的位宽划分表

面对 32 位或者 64位 的空盘,这三块肉是如何瓜分比特位的?

精度类型符号位 ss阶码 exp尾数 frac总位宽
单精度(float)1 位8 位23 位32 位
双精度(double)1 位11 位52 位64 位

位段填写模板(S / exp / frac)

手动填写位模式时,可以先画固定模板,再往里填:

单精度(32 位)

字段位范围(高位到低位)位数填写内容
Sbit 311正数填 0,负数填 1
expbit 30..238存偏置后的阶码(或全 0 / 全 1 特殊码)
fracbit 22..023小数部分有效位,不含规格化数的隐含前导 1

位布局(从左到右):
[ S ][ exp(8) ][ frac(23) ]

双精度(64 位)

字段位范围(高位到低位)位数填写内容
Sbit 631正数填 0,负数填 1
expbit 62..5211存偏置后的阶码(或全 0 / 全 1 特殊码)
fracbit 51..052小数部分有效位,不含规格化数的隐含前导 1

位布局(从左到右):
[ S ][ exp(11) ][ frac(52) ]

填写顺序(统一流程)

  1. 先写 S:看正负号。
  2. 再写 exp
    • 规格化数:exp = E + Bias(float 的 Bias=127,double 的 Bias=1023)。
    • 非规格化数:exp0
    • 无穷/NaN:exp1
  3. 最后写 frac
    • 规格化数:写 1.xxx... 里的 xxx...
    • 非规格化数:直接写有效小数位。
    • 无穷:frac0;NaN:frac 非全 0

这套模板在单精度和双精度都通用,区别只在 expfrac 的位数。

浮点转化固定流程(避免在 exp / E / frac / M 之间打架)

先固定一条总公式:

V=(1)s×M×2EV = (-1)^s \times M \times 2^E

再记住各符号只负责一件事:

可以按两个方向做题。

A. 数值 \to IEEE 位模式(编码流程)

  1. 定精度(float 或 double),先写好位框:S | exp | frac
  2. S:正数 0,负数 1
  3. 把绝对值写成二进制科学记数法:1.xxx×2E1.xxx\times2^E(若是接近 0 的极小数,要考虑非规格化)。
  4. 判分类并写 exp
    • 规格化:exp = E + Bias
    • 非规格化:exp = 0...0
    • 溢出到无穷:exp = 1...1, frac = 0...0
  5. frac
    • 规格化:写 1.xxxxxx
    • 非规格化:直接写有效小数位
  6. 位数不够就按舍入规则处理,最后拼成二进制/十六进制。

B. IEEE 位模式 \to 数值(解码流程)

  1. 先拆字段:Sexpfrac
  2. 先看 exp 判分类:
    • exp0:零或非规格化
    • exp1:无穷或 NaN
    • 其他:规格化
  3. 分类后再算 EEMM
    • 规格化:E=expBiasE=exp-BiasM=1+fM=1+f
    • 非规格化:E=1BiasE=1-BiasM=fM=f
  4. fracff(例如 010... 表示 222^{-2})。
  5. 代入 V=(1)s×M×2EV = (-1)^s \times M \times 2^E,最后写十进制或分数。

一句话速记:

案例实战:将十进制 15.25 转为 IEEE 754 单精度浮点数

纸上得来终觉浅,我们亲自手动推导一次 15.25 是如何变成机器里那 323201 的:

第一步:转换为二进制
整数部分 15=1111215 = 1111_2
小数部分 0.25=14=0.0120.25 = \frac{1}{4} = 0.01_2
拼起来得到:15.2510=1111.01215.25_{10} = 1111.01_2

第二步:变成"二进制科学记数法"
将小数点向左移动 3 位,直到整数部分只剩下一个 1
1111.0121.111012×231111.01_2 \to 1.11101_2 \times 2^3
(此时,你就已经得到了关键参数:指数 E=3E = 3,而且它是正数,所以符号 s=0s = 0

第三步:填充 32 位的三个字段

  1. 符号位 (1 位):正数,天上掉下个 0
  2. 阶码 (8 位):单精度规定的偏置 Bias 为 127。
    • exp 编码值 = 真正的指数 EE + Bias = 3+127=1303 + 127 = 130
    • 将 130 转为二进制:10000010
  3. 尾数 (23 位):取小数点后面的部分 11101(因为最前面的 1. 是所有数都有的,所以 IEEE 规定将其"白嫖"省略掉,不占存储空间)。
    • 11101 放入 23 位的头部,后面全部补 0 凑满:11101000000000000000000

第四步:拼出最终位模式
把这三块肉串起来:0 + 10000010 + 11101000000000000000000
组合成 32 比特:
0100 0001 0111 0100 0000 0000 0000 0000
转成十六进制,就是在内存中真正存下的字节流:0x41740000


4.3 浮点数的三种分类

根据 exp 字段的值,浮点数被分为三种互相排斥的情况:

1. 规格化值(Normalized)

exp 既不是全 0,也不是全 1 时,表示规格化值。这是最普遍的情况。

为什么要"规格化"?
任何一个浮点数如果在科学记数法下不加限制,会有无数种表现形式。比如 15.2515.25 可以写成 15.25×10015.25 \times 10^0,也能写成 1.525×1011.525 \times 10^1。为了避免同一个数字在底层出现好几种不同的二进制组合(这会导致比较和计算极其复杂),IEEE 754 强制要求:小数点左边必须且只能是一个非零的数字

在十进制里,这意味着首位是 191 \sim 9;而在二进制的世界里,"非零数字"只有 1 这一种可能!这就带来了一个绝妙的设计,既然所有规格化数都是以 1. 开头,那这个 1 就成了废话,根本不需要被专门存进物理磁盘里!这也就是下文中"隐含的 1"的由来,它让我们在同样的物理位宽里多出了一位数据精度。

2. 非规格化值(Denormalized)

exp0 时,表示非规格化值。主要用来表示 00,以及非常接近 00 的极小数值。

3. 特殊值(Special Values)

exp1 时,表示特殊值。

4.3.1 非规格化数的转化示例(单精度)

目标:把最小正非规格化数写成 IEEE 754 单精度位模式。

已知:

最小正非规格化数要求 frac 只有最低位是 1

于是:

f=223,M=223,E=126f = 2^{-23},\quad M = 2^{-23},\quad E = -126 V=(1)0×M×2E=223×2126=2149V = (-1)^0 \times M \times 2^E = 2^{-23} \times 2^{-126} = 2^{-149}

十进制近似值:

V1.40129846×1045V \approx 1.40129846 \times 10^{-45}

同理,如果 frac 末两位为 11(其余位为 0),值就是:

(222+223)×2126=3×2149(2^{-22}+2^{-23})\times2^{-126}=3\times2^{-149}

4.3.2 IEEE 位模式还原为十进制小数 / 分数

按统一步骤还原:先拆 S/exp/frac,再判定分类,最后代入

V=(1)s×M×2EV = (-1)^s \times M \times 2^E

例 1:0x3FC00000(单精度)

V=(+1)×1.5×20=1.5=32V = (+1)\times1.5\times2^0 = 1.5 = \frac{3}{2}

例 2:0x3E200000(单精度)

V=(+1)×54×23=532=0.15625V = (+1)\times\frac{5}{4}\times2^{-3} = \frac{5}{32} = 0.15625

上面两个例子分别对应“还原成十进制小数”和“还原成精确分数”的常用写法。


4.4 舍入(Rounding)

浮点数只能保留有限位,尾部被截掉时就要决定“进不进 1”。

可以把一个数写成:

保留部分 | 被舍弃部分

记号约定:

判定直觉:

IEEE 四种模式可直接记成下表:

模式规则正数效果负数效果
向偶数舍入(Round-to-Even,默认)先选最近值;若 tie,则让结果最低位为偶数有时进位,有时不进位同左
向零舍入(Toward Zero)直接截断变小或不变绝对值变小或不变
向下舍入(Toward -\infty取不大于原值的最大可表示数截断(不进)可能进位(更负)
向上舍入(Toward ++\infty取不小于原值的最小可表示数可能进位(更大)截断(不进)

向偶数舍入的 tie 细则(最常考)

当且仅当 G=1,R=0G=1, R=0(被舍弃部分是 1000...0)时,属于 tie:

这条规则的效果是长期统计更中性,不会像“总是向上”那样积累偏差。

快速例子(按“保留 3 位小数”理解)

十进制类比也一致:


4.5 浮点运算的陷阱

由于精度的固有限制,浮点数运算与数学上的实数运算有本质区别。

1. 缺乏结合律

(3.14 + 1e20) - 1e20  // 结果为 0.0!(3.14 被 1e20 吸收了精度)
3.14 + (1e20 - 1e20)  // 结果为 3.14

启示:编译器通常不敢对浮点运算进行重排序优化,因为顺序会改变结果。

2. 缺乏分配律
a×(b+c)a \times (b + c) 不一定等于 (a×b)+(a×c)(a \times b) + (a \times c)

3. C 语言中的数据类型转换

转换是否丢失精度说明
int \to float可能会丢失精度单精度尾数仅 23 位,表示不完全 32 位的整型信息
int \to double安全且精确双精度尾数有 52 位,足以装下整个 32 位 int
float \to double安全且精确完全被包容
double \to float可能溢出 (±\pm\infty),可能舍入
float / double \to int向零舍入如果浮点数值超过 int 上限,C 语言不保证结果(通常会变成 INT_MIN 作为惩罚)

关键认知:浮点数可以表示极大的范围,但它在连续实数轴上的分布是极度不均匀的。越接近原点(数值越小),刻度越密,精度极高;到了 101010^{10} 以上,相邻两个浮点数之间的跨度可能比 10001000 还要大。实际代码中通常不直接用 == 比较两个浮点数是否相等,而是比较它们的差值绝对值是否小于阈值 ϵ\epsilon


4.6 浮点数表示范围(速查)

exp 位数为 kkfrac 位数为 nn,Bias 为 2k112^{k-1}-1

对应到 IEEE 754 常见类型:

类型Bias指数范围(规格化)最大有限值最小规格化正值最小非规格化正值
float(32 位)127126+127-126 \sim +1273.4028235×10383.4028235\times10^{38}1.17549435×10381.17549435\times10^{-38}1.40129846×10451.40129846\times10^{-45}
double(64 位)10231022+1023-1022 \sim +10231.7976931348623157×103081.7976931348623157\times10^{308}2.2250738585072014×103082.2250738585072014\times10^{-308}4.9406564584124654×103244.9406564584124654\times10^{-324}

补充:


4.7 8bit 课堂模型:非规格化到规格化(1/4/3)

按课堂演示模型,位宽划分为:S=1exp=4frac=3

非规格化区(exp=0000

此时:E=1Bias=6E=1-Bias=-6M=f=frac23M=f=\frac{frac}{2^3},所以

value=f×2E=frac8×26=frac×29value = f\times 2^E = \frac{frac}{8}\times2^{-6}=frac\times2^{-9}
位模式(S exp frac)EEvalue(分数)value(十进制)跨度到下一个
0 0000 0016-6292^{-9}0.001953125292^{-9}
0 0000 0106-62×292\times2^{-9}0.00390625292^{-9}
0 0000 0116-63×293\times2^{-9}0.005859375292^{-9}
0 0000 1006-64×294\times2^{-9}0.0078125292^{-9}
0 0000 1016-65×295\times2^{-9}0.009765625292^{-9}
0 0000 1106-66×296\times2^{-9}0.01171875292^{-9}
0 0000 111(最大非规)6-67×297\times2^{-9}0.013671875292^{-9}(到最小规)

过渡点(非规格化 \to 规格化)

位模式(S exp frac)EEvalue
最大非规格化正数0 0000 1116-67×297\times2^{-9}
最小规格化正数0 0001 0006-61×26=8×291\times2^{-6}=8\times2^{-9}

两者差值:8×297×29=298\times2^{-9}-7\times2^{-9}=2^{-9},所以过渡处没有断层,间距连续。

规格化区(exp=00011110

此时:E=expBiasE=exp-BiasM=1+frac8M=1+\frac{frac}{8},固定某个 EE

value[1.0002×2E, 1.1112×2E]=[2E, (223)2E]value\in[1.000_2\times2^E,\ 1.111_2\times2^E]=[2^E,\ (2-2^{-3})2^E]

且该区间内跨度为:

Δ=2E3\Delta = 2^{E-3}
expEEvalue 范围(正数)跨度 Δ\Delta
00016-6[26, 1.875×26][2^{-6},\ 1.875\times2^{-6}]292^{-9}
00105-5[25, 1.875×25][2^{-5},\ 1.875\times2^{-5}]282^{-8}
00114-4[24, 1.875×24][2^{-4},\ 1.875\times2^{-4}]272^{-7}
01003-3[23, 1.875×23][2^{-3},\ 1.875\times2^{-3}]262^{-6}
01012-2[22, 1.875×22][2^{-2},\ 1.875\times2^{-2}]252^{-5}
01101-1[21, 1.875×21][2^{-1},\ 1.875\times2^{-1}]242^{-4}
011100[1, 1.875][1,\ 1.875]232^{-3}
100011[2, 3.75][2,\ 3.75]222^{-2}
100122[4, 7.5][4,\ 7.5]212^{-1}
101033[8, 15][8,\ 15]202^0
101144[16, 30][16,\ 30]212^1
110055[32, 60][32,\ 60]222^2
110166[64, 120][64,\ 120]232^3
111077[128, 240][128,\ 240]242^4

观察:从 E=6E=-6 往上,每升高 1 档指数,跨度翻倍。这也是“数越大,刻度越稀”的直接来源。


4.8 浮点数加法

4.8.1 固定流程

  1. 拆成 (1)s,M,E(-1)^s, M, E(先判规格化/非规格化)。
  2. 处理特殊值:NaN、±\pm\infty 先行返回。
  3. 对阶:把较小指数的尾数右移,直到两个数指数相同。
  4. 按符号做尾数加减(同号相加,异号相减)。
  5. 结果规格化:
    • 若尾数 2\ge 2,右移一位并 E+1E+1
    • 若尾数 <1<1(且非 0),左移并 E1E-1
  6. 舍入到目标位数(按当前舍入模式,默认向偶数)。
  7. 检查溢出/下溢,最后回填 S | exp | frac

例(8bit 模型:S=1, exp=4, frac=3

计算:0 0101 000 + 0 0100 100

对阶到 E=2E=-2

1.1002×23=0.1102×221.100_2\times2^{-3}=0.110_2\times2^{-2}

尾数相加:

1.0002+0.1102=1.11021.000_2+0.110_2=1.110_2

结果已规格化,不需再移位:

1.1102×22=0.43751.110_2\times2^{-2}=0.4375

回填:S=0,E=2exp=5=0101,frac=110S=0, E=-2\Rightarrow exp=5=0101, frac=110,结果位模式:

0 0101 110\boxed{0\ 0101\ 110}

4.8.2 加法易错点


4.9 浮点数乘法

4.9.1 固定流程

  1. 拆成 (1)s,M,E(-1)^s, M, E
  2. 处理特殊值:NaN、0×0\times\infty×\infty\times 非 0 等。
  3. 符号位:s=s1s2s=s_1\oplus s_2
  4. 指数相加:E=E1+E2E=E_1+E_2
  5. 尾数相乘:M=M1×M2M=M_1\times M_2
  6. 结果规格化(若 M2M\ge2 则右移并 E+1E+1)。
  7. 舍入、检查溢出/下溢、回填 S | exp | frac

例(8bit 模型:S=1, exp=4, frac=3

计算:0 0110 010 × 0 0111 100

符号:00=00\oplus0=0

指数:

E=1+0=1E=-1+0=-1

尾数:

M=1.0102×1.1002=1.1112=1.875M=1.010_2\times1.100_2=1.111_2=1.875

已规格化,回填:exp=E+Bias=1+7=6=0110exp=E+Bias=-1+7=6=0110frac=111frac=111,结果位模式:

0 0110 111\boxed{0\ 0110\ 111}

十进制校验:0.625×1.5=0.93750.625\times1.5=0.9375,与结果一致。

4.9.2 乘法易错点


4.10 浮点加法/乘法的数学特性

设浮点加法、乘法分别记为 \oplus\otimes(表示“先按实数算,再舍入到目标格式”)。

4.10.1 只看浮点加法 \oplus

通常成立:

常失效:

反例(加法结合律):

原因是“对阶 + 舍入”会把小量吞掉,改变中间结果。

4.10.2 只看浮点乘法 \otimes

通常成立:

常失效:

4.10.3 加法与乘法混合

常失效:

x(yz)(xy)(xz)x\otimes(y\oplus z) \neq (x\otimes y)\oplus(x\otimes z)

常见场景:yzy\approx -z 且数量级大时,yzy\oplus z 先发生严重抵消,再乘 xx,结果与“先乘后加”不同。

结论(做题口令):



0 / 2000
Loading comments...