CS:APP 读书笔记:从字节序到浮点数,重新理解信息的表示与处理
systems, engineering
systems, engineering
很多“代码看起来没问题,结果却不对”的问题,根子并不在业务逻辑,而在数据是怎么被底层表示和解释的。
同样的一组比特位,可能因为字节序不同、是否带符号、是否发生隐式类型提升,最终得到完全不同的结果。把这些基础概念串起来之后,很多看似诡异的现象都会变得可解释。
当一个对象占多个字节时,内存需要决定“低位字节”和“高位字节”怎么排布:
比如一个 int32 的值是 0x12345678,起始地址是 0x100:
大端序
地址: 0x100 0x101 0x102 0x103
数据: 12 34 56 78
小端序
地址: 0x100 0x101 0x102 0x103
数据: 78 56 34 12
如果你想直接观察一段对象在内存里的字节排布,最简单的方法就是把它按 unsigned char * 来看:
#include <stdio.h>
typedef unsigned char* byte_pointer;
void show_bytes(byte_pointer start, int len) {
for (int i = 0; i < len; i++) {
printf("%02x ", start[i]);
}
printf("\n");
}
void show_int(int x) {
show_bytes((byte_pointer)&x, sizeof(int));
}
void show_float(float x) {
show_bytes((byte_pointer)&x, sizeof(float));
}
void show_pointer(void* x) {
show_bytes((byte_pointer)&x, sizeof(void*));
}
这也是为什么文本数据通常比二进制数据更容易跨平台传输。文本遵循字符编码规则,而不是直接暴露底层字节布局,所以它对字节序没那么敏感。
位移不是简单的“乘 2 / 除 2”语法糖,它首先是按位移动。
0。逻辑右移会在左侧补 0,适合无符号数。算术右移会在左侧补符号位,常用于补码整数。多数机器和编译器对有符号数使用算术右移。
0x8A = 1000 1010
如果按算术右移 3 位:
1111 0001
这里左侧补的是 1,因为原数是负数。也正因为如此,对带符号整数做右移时,最好明确自己依赖的是数值意义,还是位级行为。
无符号整数和有符号整数,看到的是同一组比特,但解释方法不同。
补码有两个特别重要的结论:
例如下面这段代码:
short v = -12345;
unsigned short uv = (unsigned short)v;
printf("v = %d, uv = %u\n", v, uv);
这里并不是先把 -12345 重新算成一个新的正数再存进去,而是保留原来的位模式,然后按无符号整数重新解释。所以最后会看到一个很大的正整数结果。
混合有符号数和无符号数比较时,这个问题尤其容易踩坑:
int b1 = -1 < 0; // 1
int b2 = 2147483647U > -1; // 0
第二个表达式里,-1 会先被提升并转换成无符号值,于是比较结果就和直觉不同了。
整数溢出并不是“机器算错了”,而是位宽固定后必然出现的结果。
对 w 位无符号整数来说,结果本质上是在做模 2^w 运算:
x + y < 2^w,结果正常落在范围内。x + y >= 2^w,高位会被截掉,结果等于 x + y - 2^w。一个常见的小技巧是:无符号加法后,如果结果小于其中一个加数,通常就已经发生了回绕。
补码加法在位级实现上和无符号加法非常像,但解释方式不同。对程序员来说,关键不是死记公式,而是记住一句话:
底层电路只负责按位计算,语言规则才负责解释结果。
很多乘法都可以重写成移位和加减,例如:
x * 8 可以写成 x << 3x * 10 可以写成 (x << 3) + (x << 1)而除以 2^k 时,右移经常能替代除法,但要注意:
所以对于负数,位移结果和真正的整数除法并不总是一致。如果你需要模拟“除以 2^k 再向零截断”,通常需要在右移前先加偏置。
浮点数并不是把十进制小数原样存进内存,而是按照科学计数法的思路编码。可以把它理解成:
V = (-1)^s × M × 2^E
其中:
s 是符号位。E 是指数部分。M 是有效数字部分。以 IEEE 754 为例:
这套表示方式除了普通数值,还定义了几类特殊值:
+∞ 和 -∞NaN+0.0 和 -0.0理解浮点数时最重要的现实感是:很多十进制小数根本不能被二进制精确表示。
这也是为什么像 0.1 + 0.2 这样的表达式,在不同语言里常常会暴露出舍入误差。默认情况下,IEEE 754 通常采用“舍入到最接近值,遇到中点时取偶数”的规则,也就是常说的 round to even。
遇到底层表示相关的问题时,可以按这个顺序检查:
signed / unsigned 时,先检查是否发生了隐式提升。这篇内容真正想建立的,不是公式记忆,而是一种判断习惯:当程序结果和直觉不一致时,先回到底层表示层看一眼。
字节序、补码、溢出、类型转换和浮点舍入看起来分散,但它们其实都在回答同一个问题:同一组比特位,机器到底是怎么理解它的。
日志还会继续,欢迎常来。
返回首页