1Computer Science from the Bottom Up

二进制与数字表示

计算的基础

二进制布尔运算补码浮点数字节序IEEE 754

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第1章 二进制与数字表示

计算的基础

章节导读

在开始阅读本章之前,请先思考一个问题:为什么计算机使用二进制,而不是我们人类习以为常的十进制?这个问题的答案不在数学里,而在物理学里。计算机的基本构成单元是晶体管,晶体管本质上是一个电子开关:它要么导通(高电平),要么截止(低电平)。让电路可靠地区分两种状态非常容易——只要电压高于某个阈值就是 1,低于另一个阈值就是 0,中间留出足够的噪声容限。但如果要让电路区分十种不同的电压水平来表示十进制的 0 到 9,那么每一档之间的间隔就会变得非常小,任何微小的电磁干扰、温度漂移或元件老化都可能让电路把 5 误读成 6。工程上的可靠性要求决定了:状态越少,抗噪声能力越强。于是二进制成为了几乎所有现代计算机的基础。

表示方案编码方式零的表示加减法统一范围 (8-bit)缺点
原码符号位+绝对值两种 (+0, -0)需判断符号-127 ~ +127零不唯一,加减复杂
反码按位取反两种 (+0, -0)需末端进位-127 ~ +127零不唯一
补码取反+1唯一 (全0)天然统一-128 ~ +127不对称范围
IEEE 754 单精度1+8+23 位唯一不适用±3.4x10^38精度有限
IEEE 754 双精度1+11+52 位唯一不适用±1.8x10^30864 位开销

二进制的数学基础是布尔代数。1847 年,英国数学家 George Boole 发表了《逻辑的数学分析》,提出了一套用代数方法处理逻辑命题的体系。在当时,这纯粹是一种数学游戏,没有人想到它会有什么实际用途。将近一百年后,1937 年,麻省理工学院的硕士生 Claude Shannon 发表了后来被誉为「数字电路圣经」的硕士论文《继电器与开关电路的符号分析》。Shannon 敏锐地发现,Boole 代数中的真与假,恰好对应开关电路中的闭合与断开:AND 对应串联电路,OR 对应并联电路,NOT 对应继电器常闭触点。这篇论文完成了从抽象数学到工程实现的关键一跃,奠定了整个数字时代的基础。可以说,你今天使用的每一台计算机,都是 Boole 和 Shannon 两人跨越时空合作的产物。

有了布尔运算,我们就可以搭建加法器、寄存器、存储器,进而搭建完整的计算机。但新的问题接踵而至:如何用有限的二进制位表示现实世界中无限的数字?负数怎么表示?小数怎么表示?非常大的数和非常小的数怎么办?这些问题催生了原码、反码、补码的演进,催生了 IEEE 754 浮点数标准,也催生了无数程序员的深夜调试。1996 年,阿丽亚娜 5 号火箭在首飞 37 秒后爆炸,直接原因是一行看似无害的类型转换代码把 64 位浮点数转成了 16 位有符号整数并发生了溢出——这次价值 3.7 亿美元的事故让全世界认识到,数字表示不是教科书上的抽象概念,而是实实在在的工程生命线。2000 年问题(Y2K)同样源于数字表示:早期程序员为了节省昂贵的存储空间,用两位数字表示年份,结果在世纪之交引发了全球范围的系统恐慌。

本章将带你从最底层的比特出发,理解计算机如何表示整数、负数、小数和实数。你会学到补码为什么能让加法和减法共用同一套电路,会亲眼看到 0.1 + 0.2 != 0.3 背后的二进制真相,会理解大端序和小端序这场持续了四十年的「圣战」,还会掌握一系列实用的位运算技巧。这些知识是后续所有章节——CPU 架构、操作系统、编译工具链——的共同地基。Ian Wienand 在书中强调「from the bottom up」的方法论:只有真正理解了最底层的表示方式,你才能理解上层软件的行为,才能在遇到诡异 bug 时知道问题出在哪里。让我们从第一个比特开始。

核心概念

  • 二进制
  • 布尔运算
  • 补码
  • 浮点数
  • 字节序
  • IEEE 754

要点精析

为什么计算机选择二进制——电子电路的物理约束。数字电路的基本元件是晶体管,它工作在开关状态:截止时输出低电平(逻辑 0),饱和导通时输出高电平(逻辑 1)。以典型的 3.3V CMOS 电路为例,0V 到 0.8V 被识别为逻辑 0,2.0V 到 3.3V 被识别为逻辑 1,中间的 0.8V 到 2.0V 是禁止的过渡区。这种设计提供了超过 1V 的噪声容限,即使信号受到相当程度的干扰,接收端仍能正确判断。如果改用十电平表示十进制数字,3.3V 要分成十档,每档只有 0.33V,再留出噪声容限后几乎无法可靠工作。历史上确实存在过三进制计算机——1958 年苏联制造的 Setun 就使用 -1、0、+1 三态,理论上三进制在「基数经济性」上甚至优于二进制(e≈2.718 是最优基数),但因为二进制元件的制造成本和可靠性优势,二进制最终胜出。此外,二进制与布尔逻辑天然同构:0 对应假,1 对应真,逻辑运算可以直接用电路实现,这是十进制做不到的。

布尔代数的数学基础——George Boole 的贡献。布尔代数是一个定义在集合 {0, 1} 上的代数系统,包含三种基本运算:与(AND,记作 ∧ 或 ·)、或(OR,记作 ∨)、非(NOT,记作 ¬ 或上划线)。它满足一系列公理:交换律(a∧b = b∧a)、结合律、分配律(a∧(b∨c) = (a∧b)∨(a∧c),注意 AND 对 OR 和 OR 对 AND 都满足分配律,这与普通代数不同)、同一律(a∧1 = a,a∨0 = a)、互补律(a∧¬a = 0,a∨¬a = 1)。1854 年 Boole 在《思维规律的研究》中系统化这套理论时,目标是给亚里士多德的形式逻辑建立数学模型。De Morgan 定律(¬(a∧b) = ¬a∨¬b,¬(a∨b) = ¬a∧¬b)是布尔代数中最常用的化简工具,它告诉我们「与非」和「或非」可以表达一切布尔函数。事实上,单独的 NAND 门就是功能完备的——任何布尔函数都能只用 NAND 门搭建,这也是为什么 NAND 闪存和 NAND 门电路在工业中如此重要:只需要生产一种门电路就能构建任意复杂的逻辑。

基本布尔运算的真值表与电路实现。NOT 门(反相器)有一个输入一个输出,晶体管级实现只需一个 PMOS 和一个 NMOS 组成的 CMOS 反相器。AND 门在 CMOS 中由两个串联的 NMOS 加两个并联的 PMOS 构成,只有两个输入都为高时输出才为高。OR 门结构对偶。XOR(异或)门的输出在两个输入不同时为 1,其实现需要 4 个晶体管(传输管结构)或 6 个晶体管(标准 CMOS),它是加法器的核心:半加器的和输出就是 A XOR B,进位输出是 A AND B。把 8 个全加器串起来就得到 8 位行波进位加法器,但进位信号要逐级传播,延迟是 O(n) 的。现代 CPU 使用超前进位加法器(Carry-Lookahead Adder)或 Kogge-Stone 加法器,通过并行计算「生成」和「传播」信号把延迟降到 O(log n)。一个 64 位加法器在现代工艺下大约需要几千个晶体管,延迟不到 100 皮秒。理解这些电路让你明白:CPU 的「计算」不是什么魔法,就是布尔门电路的组合。

数字表示系统的演进——原码、反码、补码。用二进制表示有符号数经历了三代方案。原码(Sign-Magnitude):最高位作符号位,其余位表示绝对值。它直观但有两个致命缺陷:一是存在 +0(00000000)和 -0(10000000)两种零的表示,浪费编码空间且比较逻辑复杂;二是加法和减法需要完全不同的电路,做加法前必须先比较两数符号和大小,硬件成本高。反码(Ones' Complement):负数按位取反。它解决了部分问题,但仍有 ±0 两种表示(00000000 和 11111111),而且加法结果可能产生「循环进位」,需要把溢出的进位再加回低位。早期计算机如 CDC 6600 使用反码。补码(Two's Complement):负数等于按位取反再加一。它完美解决了所有问题:零只有一种表示,加减法统一,n 位补码能表示 -2^(n-1) 到 2^(n-1)-1 共 2^n 个数(注意负数比正数多一个)。1960 年代的 IBM System/360 确立了补码的统治地位,此后所有主流计算机都采用补码表示有符号整数。

补码的数学原理——为什么加减法可以统一。补码的本质是模运算(Modular Arithmetic)。n 位二进制数的运算天然以 2^n 为模:任何超过 n 位的结果都会「绕回」。在模 2^n 的世界里,-x 与 2^n - x 是同一个数,而 2^n - x 恰好等于「按位取反加一」(因为按位取反得到 2^n - 1 - x,加一即 2^n - x)。于是减法 a - b 变成加法 a + (-b) = a + (2^n - b),结果模 2^n 后与真正的差完全一致。这意味着 CPU 只需要一个加法器就能同时完成加法和减法——做减法时只需把减数取反加一(进位输入置 1 即可完成「加一」)再相加。举例:8 位下计算 5 - 3,即 00000101 + 11111101(-3 的补码)= 100000010,丢弃溢出的第 9 位得 00000010 = 2,完全正确。补码还有一个优雅的性质:符号位参与运算而无需特殊处理,且比较大小可以直接用减法看符号。这就是为什么补码是「让硬件最简单」的表示法——计算机体系结构的核心设计哲学就是用数学技巧换取硬件简洁。

溢出问题——阿丽亚娜 5 号火箭爆炸的教训。补码运算的取值范围有限,超出范围就会发生溢出(Overflow),而 C/C++ 中有符号整数溢出是未定义行为,编译器可以做出任何假设。1996 年 6 月 4 日,欧洲航天局的阿丽亚娜 5 号火箭首飞,升空 37 秒后偏离轨道自毁,损失约 3.7 亿美元。事故调查表明:惯性参考系统的一段代码将 64 位浮点数(水平速度值)转换为 16 位有符号整数,5 号火箭的水平速度比 4 号大得多,转换时发生溢出,导致主备两套惯性导航系统同时失效,火箭姿态数据错乱触发自毁。这段代码是从阿丽亚娜 4 号直接复用的,在 4 号上从未出问题。这个案例的教训是多方面的:第一,类型转换必须检查范围;第二,代码复用要重新验证假设条件;第三,冗余设计如果复制了同一个 bug 就不是冗余;第四,测试必须覆盖真实运行环境。类似的溢出事故还有 2015 年波音 787 的发电机控制器连续运行 248 天后整数溢出导致全机断电的适航指令。溢出不是理论问题,而是价值数亿美元的工程现实。

IEEE 754 浮点数标准——符号位、指数位、尾数位的设计权衡。1976 年 Intel 聘请加州大学伯克利分校的 William Kahan 设计 8087 数学协处理器的浮点单元,Kahan 的工作最终演化为 1985 年发布的 IEEE 754 标准。标准规定浮点数的形式为 (-1)^s × 1.M × 2^(E-bias):s 是符号位,M 是尾数(Mantissa,也叫 significand),E 是指数域的编码值,bias 是偏置值。单精度(float):1 位符号 + 8 位指数(bias=127)+ 23 位尾数;双精度(double):1 位符号 + 11 位指数(bias=1023)+ 52 位尾数。三个关键设计:其一,隐含前导 1——规格化数的尾数总是 1.xxx 形式,这个 1 不存储,白赚一位精度;其二,偏置指数——用无符号数加偏置表示指数,使得浮点数比较大小可以像整数一样按位比较(同号时);其三,特殊编码——指数全 0 表示非规格化数(Denormal,实现渐进下溢)和零,指数全 1 表示无穷大(尾数为 0)和 NaN(尾数非 0)。Kahan 坚持的渐进下溢设计让接近零的数逐渐损失精度而不是突然跳变,这一设计在当时引起激烈争论,如今被证明极其重要。IEEE 754 还严格定义了四种舍入模式,默认「向最近偶数舍入」避免了统计偏差。

浮点数精度问题——0.1 + 0.2 != 0.3 的深层原因。在几乎所有现代编程语言中,0.1 + 0.2 == 0.3 都返回 false,实际结果是 0.30000000000000004。原因在于:0.1 的二进制是 0.0001100110011...(0011 无限循环),就像十进制无法精确表示 1/3 一样,二进制无法精确表示 1/10。双精度只有 52 位尾数,存储时必须截断舍入,所以内存中的「0.1」其实是一个与 0.1 相差约 10^-17 的近似值。两个近似值相加,误差累积后无法恰好等于 0.3 的近似值。这不是 bug,而是有限位二进制表示实数的固有局限:n 位浮点数只能表示 2^n 个不同的值,而实数有无穷多个。工程对策包括:比较浮点数用 fabs(a-b) < epsilon 而非 ==;货币计算用整数分或十进制类型(如 Java 的 BigDecimal);求和大量浮点数时用 Kahan 补偿求和算法减少误差累积;理解机器精度(double 约 2.2×10^-16)是数值算法稳定性的基础。浮点数还不满足结合律:(3.14+1e10)-1e10 = 0 而 3.14+(1e10-1e10) = 3.14,这意味着编译器不能随意重排浮点运算,也是并行归约求和结果不确定的根源。

字节序——大端与小端的历史争论和网络字节序。多字节整数在内存中的存放顺序有两种约定:大端序(Big-Endian)把最高有效字节放在低地址,符合人类书写习惯,打印内存时直观;小端序(Little-Endian)把最低有效字节放在低地址。这个命名来自 Jonathan Swift 的《格列佛游记》——小人国因为该从大端还是小端敲鸡蛋而爆发战争,1980 年代 Danny Cohen 借用这个典故形容计算机界的字节序之争。大端阵营:IBM 主机、SPARC、PowerPC、网络协议(TCP/IP 规定网络字节序为大端,因此有 htons/ntohl 等转换函数);小端阵营:Intel x86/x64、ARM(默认小端模式)、RISC-V。小端的实际优势:取低字节无需地址偏移(把 32 位指针转成 8 位指针直接解引用即可得到最低字节),且类型截断自然。大端的实际优势:内存转储可读性好,判断符号位直接看第一个字节。跨平台二进制协议必须显式规定字节序,否则同一个文件在不同机器上解析出完全不同的数值——这是网络编程和文件格式设计中最经典的坑之一。

位运算技巧——从掩码到 Brian Kernighan 算法。位运算是直接操作二进制位的运算,CPU 单周期即可完成,是性能敏感代码的常用工具。常用技巧包括:x & (x-1) 清除最低位的 1(Brian Kernighan 算法,用于统计 1 的个数,循环次数等于 1 的个数而非位数);x & (-x) 提取最低位的 1(lowbit,树状数组的核心操作);x ^ x = 0 和 x ^ 0 = x 用于成对消除(找出数组中唯一不重复的数);(x >> k) & 1 提取第 k 位;x |= (1 << k) 置位,x &= ~(1 << k) 清位;用位掩码(bitmask)表示集合,交并差补对应 AND、OR、AND NOT、XOR,一个 64 位整数就能表示 64 个元素的集合,比布尔数组快一个数量级。2 的幂判断:x > 0 && (x & (x-1)) == 0。交换两数不用临时变量:a ^= b; b ^= a; a ^= b(但现代编译器下不如临时变量快,且有 a==b 时清零的陷阱)。无符号数右移是逻辑右移补 0,有符号数右移通常是算术右移补符号位(但 C 标准中是实现定义行为)。注意:有符号数左移溢出是未定义行为,位运算应尽量作用于无符号类型。

代码示例

/* 观察补码表示:同一个位模式,不同的解释 */
#include <stdio.h>
#include <string.h>

void show_bits(void *p, size_t n) {
    unsigned char *bytes = (unsigned char *)p;
    for (size_t i = n; i > 0; i--) {
        unsigned char b = bytes[i - 1];
        for (int j = 7; j >= 0; j--)
            putchar((b >> j) & 1 ? '1' : '0');
        putchar(' ');
    }
    putchar('\n');
}

int main(void) {
    int a = -5;
    unsigned int u;
    memcpy(&u, &a, sizeof(u));
    show_bits(&a, sizeof(a));
    printf("signed:   %d\n", a);
    printf("unsigned: %u\n", u);
    printf("2^32 - 5 = %u\n", 4294967296u - 5u);
    return 0;
}

/* 这段代码演示了补码的核心思想:位模式与解释分离。
   -5 在 32 位补码下的位模式是 11111111 11111111 11111111 11111011,
   用 memcpy 把同样的 32 个比特按无符号数解释,得到 4294967291,
   恰好等于 2^32 - 5,验证了「补码负数 x 等于模 2^n 下的 2^n + x」。
   注意我们刻意使用 memcpy 而不是指针强转 (unsigned*)&a,
   因为后者违反 C 的严格别名规则(strict aliasing),
   在开启 -O2 优化时编译器可能生成错误代码。
   show_bits 从高位字节向低位打印,在 x86(小端)机器上需要
   逆序遍历字节数组才能得到人类习惯的书写顺序。
   这个实验说明:内存中只有比特,「类型」只是解释比特的方式,
   这也是 C 语言类型系统与底层硬件关系的本质。 */
/* 浮点数精度:亲眼看到 0.1 + 0.2 != 0.3 */
#include <stdio.h>
#include <math.h>

int main(void) {
    double a = 0.1, b = 0.2, c = a + b;
    printf("0.1 + 0.2 = %.20f\n", c);
    printf("0.3       = %.20f\n", 0.3);
    printf("equal?    %s\n", (c == 0.3) ? "yes" : "no");
    printf("diff      = %.20e\n", c - 0.3);
    printf("epsilon   = %.20e\n", DBL_EPSILON);

    double sum = 0.0;
    for (int i = 0; i < 10; i++) sum += 0.1;
    printf("10 x 0.1  = %.20f, equal 1.0? %s\n",
           sum, (sum == 1.0) ? "yes" : "no");

    printf("correct compare: %s\n",
           fabs(c - 0.3) < 1e-9 ? "yes" : "no");
    return 0;
}

/* 运行结果会显示 0.1 + 0.2 = 0.30000000000000004441,
   与 0.3 的存储值相差约 5.55e-17,正好在双精度机器精度
   (DBL_EPSILON ≈ 2.22e-16)的量级内。十次累加 0.1 同样
   得不到精确的 1.0,因为每次加法都引入舍入误差并且误差会累积。
   这个例子给出三条工程准则:
   第一,永远不要用 == 比较两个经过运算的浮点数,
   应当比较差值的绝对值是否小于容差 epsilon;
   第二,容差的选取要与数据的量级匹配,
   绝对容差适合 O(1) 的量,大数值应使用相对容差;
   第三,打印浮点数时 %.20f 能暴露真实存储值,
   默认的 %f 只打印 6 位小数,会掩盖精度问题。
   如果业务涉及货币,正确做法是用整数「分」运算,
   或使用十进制浮点类型(C 的 _Decimal64、Java 的 BigDecimal),
   而不是试图用二进制浮点表示十进制小数。 */
/* 检测字节序:运行时判断大端还是小端 */
#include <stdio.h>

int main(void) {
    unsigned int x = 0x01020304;
    unsigned char *p = (unsigned char *)&x;

    printf("value: 0x%08x\n", x);
    printf("bytes at addresses low->high: ");
    for (size_t i = 0; i < sizeof(x); i++)
        printf("%02x ", p[i]);
    printf("\n");

    if (p[0] == 0x04)
        printf("little-endian\n");
    else if (p[0] == 0x01)
        printf("big-endian\n");

    union { unsigned int i; char c[4]; } u = { .i = 0x01020304 };
    printf("union check: %s\n", u.c[0] == 0x04 ? "LE" : "BE");
    return 0;
}

/* 在 x86 机器上输出 bytes 为 04 03 02 01——最低有效字节 0x04
   存放在最低地址,这就是小端序。字节序只有在把多字节整数
   当作字节序列观察时才有意义:单个字节的读写、以及同一台机器上
   的整数运算都感知不到字节序。字节序问题在三种场景下爆发:
   第一,网络传输——TCP/IP 规定网络字节序为大端,
   发送前要用 htonl/htons 转换,接收后用 ntohl/ntohs 还原;
   第二,二进制文件格式——跨平台文件必须显式规定字节序,
   例如 PNG 规定大端,BMP 规定小端;
   第三,指针类型双关——把 int* 强转为 char* 逐字节访问是合法的
   (char 类型豁免严格别名规则),本例正是利用这一点探测字节序。
   编译期探测可用 __BYTE_ORDER__ 宏(GCC/Clang 提供),
   C++20 则有 std::endian 枚举。理解字节序是理解
   「数据在内存中到底是什么样子」的关键一步。 */
/* 位运算实战:Brian Kernighan 算法与集合掩码 */
#include <stdio.h>

int popcount(unsigned int x) {
    int count = 0;
    while (x) {
        x &= (x - 1);
        count++;
    }
    return count;
}

int main(void) {
    unsigned int n = 0b10110100u;
    printf("popcount(%u) = %d\n", n, popcount(n));

    unsigned int lowbit = n & (unsigned int)(-(int)n);
    printf("lowest set bit = 0x%x\n", lowbit);

    printf("is power of two: %s\n",
           (n && !(n & (n - 1))) ? "yes" : "no");

    unsigned int setA = (1u << 0) | (1u << 2) | (1u << 5);
    unsigned int setB = (1u << 2) | (1u << 3) | (1u << 5);
    printf("union   = 0x%02x\n", setA | setB);
    printf("intersect = 0x%02x\n", setA & setB);
    printf("diff A-B  = 0x%02x\n", setA & ~setB);
    printf("symdiff   = 0x%02x\n", setA ^ setB);
    return 0;
}

/* popcount 使用 Brian Kernighan 算法:x &= (x-1) 每次恰好
   清除最低位的 1。原理是 x-1 会把最低位的 1 变成 0、
   其后的所有 0 变成 1,AND 之后最低位的 1 及其低位全部清零。
   循环次数等于 1 的个数,对稀疏位模式远快于逐位检查的 32 次循环。
   现代 CPU 有专门的 POPCNT 指令(x86 SSE4.2),
   GCC 的 __builtin_popcount 会自动使用它,单周期完成。
   lowbit 运算 x & (-x) 提取最低位的 1,是树状数组(Fenwick Tree)
   的核心操作。集合掩码部分演示了用一个无符号整数的每一位
   表示集合中的一个元素:并集是 OR,交集是 AND,
   差集是 AND NOT,对称差是 XOR——这些操作都是单指令完成的,
   比 std::set 或布尔数组快一到两个数量级,
   是状态压缩动态规划和权限位设计的标准手法。
   注意所有位运算都作用于 unsigned 类型,
   避免有符号数移位和取负的未定义行为。 */

实验笔记

本章实验建议完成以下四个动手任务,全部只需要一台 Linux 机器和 gcc。实验一「比特显微镜」:编写程序接受命令行输入的任意整数,分别按有符号、无符号、单精度浮点、双精度浮点四种方式打印其位模式和解释值。使用 memcpy 在类型间搬运比特(不要用指针强转),对照 IEEE 754 的公式手工验证几个特殊值:打印 0.0 和 -0.0 的位模式(注意它们的符号位不同但 == 比较为真),构造指数全 1 尾数非 0 的位模式验证它是 NaN,并确认 NaN != NaN 返回真。实验二「溢出侦探」:编写程序演示 8 位、16 位、32 位有符号与无符号整数在边界值(如 INT_MAX + 1、INT_MIN - 1、0u - 1)时的回绕行为,用 gcc 的 -fsanitize=undefined 编译并运行,观察 UBSan 如何捕获有符号溢出;再写一个模拟阿丽亚娜 5 事故的函数,把 double 强转为 short,喂入 3.27e4 以上的值观察崩溃或错误结果,体会为什么类型转换必须做范围检查。实验三「精度实验室」:计算 0.1+0.2 与 0.3 的差值;用朴素求和与 Kahan 补偿求和分别累加一千万个 0.1,对比两者与 1000000.0 的误差,验证补偿求和的误差小几个数量级;用 %.20f 打印 1.0/3.0、FLT_MAX、DBL_MIN 等值,理解规格化数与非规格化数的分界。实验四「字节序与对齐」:探测本机字节序;构造结构体 struct { char c; int i; char d; } 用 sizeof 和 offsetof 观察内存对齐产生的填充字节,再调整成员顺序对比 sizeof 的变化,理解为什么网络协议和文件格式都要显式规定字段顺序与对齐。每个实验要求写一页实验报告,记录预测值、实际值和差异原因——「先预测再验证」是 CSAPP 式学习方法的核心。

延伸阅读

  • 《深入理解计算机系统》(CSAPP)第 2 章「信息的表示和处理」——与本章主题高度重合但更加深入,配有著名的 Data Lab 实验,是理解位级表示的最佳补充读物,建议配合阅读。
  • Claude Shannon 的硕士论文《A Symbolic Analysis of Relay and Switching Circuits》(1937)——数字电路理论的开山之作,篇幅不长,原文可在 MIT 档案馆找到,读一遍能体会理论如何点燃工程革命。
  • IEEE 754-2019 标准原文——浮点算术的权威定义,重点阅读第 3、4 章的格式定义和第 7 章的舍入规则;William Kahan 的讲义《IEEE Standard 754 for Binary Floating-Point Arithmetic》是更友好的导读。
  • What Every Computer Scientist Should Know About Floating-Point Arithmetic(David Goldberg, 1991)——ACM 计算综述经典论文,系统讲解浮点误差的来源与数值稳定性,几乎所有数值计算课程的第一篇必读文献。
  • Bit Twiddling Hacks(Sean Anderson)——在线位运算技巧大全,收录上百个精妙的位操作算法及其正确性证明,是竞赛程序员和底层开发者的案头参考。
  • 阿丽亚娜 5 号事故调查报告(ARIANE 5 Flight 501 Failure, Inquiry Board Report)——官方事故分析原文,详细还原了溢出如何一步步导致火箭自毁,是软件工程史上最重要的案例研究之一。
  • 《编码:隐匿在计算机软硬件背后的语言》(Charles Petzold)——从手电筒和继电器讲起,用故事的方式带你亲手「搭建」一台计算机,适合在学完本章后作为趣味延伸。
  • Ones' and Twos' Complement 相关维基百科条目及 Computer History Museum 关于早期算术单元的资料——了解反码计算机(如 CDC 6600)的真实设计,理解补码胜出前的技术路线竞争。
1/5二进制与
0%