继续第二章剩下的内容
1.多字节对象的两个基本规则
对于跨多个字节的数据(比如 int、float),必须明确两个规则:
1.这个对象的地址是什么?
在几乎所有机器上,多字节对象都存为连续的字节序列。
对象的地址 = 它所使用的字节中最小的那个地址。
2.在内存中如何排列这些字节?
这就是字节顺序(Endianness)的问题。
两种字节顺序规则
小端法(Little Endian)
规则:最低有效字节 存储在最前面(低地址)。
即:先存低位字节,再存高位字节。
大端法(Big Endian)
规则:最高有效字节 存储在最前面(低地址)。
即:先存高位字节,再存低位字节。
假设变量 x 是 int 类型,地址 0x100,十六进制值为 0x01234567:
高位字节:0x01
低位字节:0x67
地址范围:0x100 ~ 0x103
大端法存储
| 地址 | 0x100 | 0x101 | 0x102 | 0x103 |
|---|---|---|---|---|
| 字节值 | 01 | 23 | 45 | 67 |
特点:和我们平时写数字的顺序一致(从左到右:高位 → 低位)。
小端法存储
| 地址 | 0x100 | 0x101 | 0x102 | 0x103 |
|---|---|---|---|---|
| 字节值 | 67 | 45 | 23 | 01 |
特点:和我们平时写数字的顺序相反(从左到右:低位 → 高位)。
除了字节顺序外,int 类型的字节内容在所有机器上完全一致。
float 类型的字节内容在所有机器上也一致,差异仅在于字节顺序。
指针值与机器 / 操作系统强相关,没有统一规律,只和当前运行环境的内存分配有关。
2.C 语言字符串的基本规则
存储形式:C 语言中字符串被编码为以 null 字符(值为 0)结尾的字符数组。
null 字符:即 '\0',十六进制表示为 0x00,作用是标记字符串结束。
每个字符都由标准编码表示,最常见的是 ASCII 字符码。
示例演示
字符串 "12345":包含 '1'、'2'、'3'、'4'、'5' 共 5 个可见字符,加上末尾的 '\0',总长度为 6 字节。
但是实际都要弄\0是不会打印的
31 32 33 34 35 00
'1' → 0x31
'2' → 0x32
'3' → 0x33
'4' → 0x34
'5' → 0x35
末尾的 00 就是 null 终止符 '\0'
在使用 ASCII 码的任何系统上,字符串的字节表示完全相同,与机器的字节顺序(大端 / 小端)、字长规则无关。
因此:文本数据(字符串)比二进制数据(int、float 等)具有更强的平台独立性。
我们来看下面这段代码
int sum(int x, int y) {
return x + y;
}
当我们在示例机器上编译时,生成如下字节表示的机器代码
们发现指令编码是不同的。不同的机器类型使用不同的且不兼容的指令和编码方 式。
即使是完全一样的进程,运行在不同的操作系统上也会有不同的编码规则,
因此二进 制代码是不兼容的。
二进制代码很少能在不同机器和操作系统组合之间移植。
3.布尔代数
最简单的布尔代数定义在二元集合 {0, 1} 上,包含 4 种核心运算,符号和 C 语言位运算符完全匹配:
| 运算符号 | 逻辑名称 | 命题逻辑符号 | 运算规则(二进制 0/1) | ||
|---|---|---|---|---|---|
~ | 非(NOT) | ¬ | 当 P=0 时,~P=1;当 P=1 时,~P=0(取反) | ||
& | 与(AND) | ∧ | 只有 p=1 且 q=1 时,p&q=1;否则为 0(全 1 才 1) | ||
| | | 或(OR) | v | 只要 p=1或 q=1,否则为 0(有 1 就 1) | ||
| ^ | 异或(EXCLUSIVE-OR) | ⊕ | 当 p 和 q 不同时(1 和 0 或 0 和 1),p^q=1;否则为 0(不同才 1) |
书中扩展部分


整数算术里有乘法对加法的分配律:
a⋅(b+c)=(a⋅b)+(a⋅c)
布尔代数里也有类似的分配律:
& 对 | 的分配律:
a&(b∣c)=(a&b)∣(a&c)
| 对 & 的分配律:
a∣(b&c)=(a∣b)&(a∣c)
注意:整数算术不满足
a+(b⋅c)=(a+b)⋅(a+c)
,但布尔代数的这两个分配律都成立。
布尔环(Boolean ring)
当考虑位向量上的 ^(异或)、&(与)、~(非)运算时,会得到另一种数学结构 ——布尔环。
布尔环与整数运算的相似性:加法逆元
整数运算:每个值 x 都有加法逆元 −x,满足 x+(−x)=0。
布尔环:这里的 “加法” 是 ^(异或),每个元素的加法逆元是它自己。
即:对任意位向量 a,都有a∧a=0(这里的 0表示全 0 的位向量)
验证:对单个位成立
0∧0=0
1∧1=0
扩展到位向量:无论怎么排列组合顺序,
a∧a=0
都成立。这个性质会带来很多有趣的结果和技巧(后续练习题 2.10 会探讨)。
位向量表示有限集合
核心规则:位的位置 = 集合里的元素
我们约定:
位向量长度 = w,对应元素范围是 {0, 1, ..., w-1}
位向量里第 i 位是 1 ⇔ 元素 i 在集合里
位向量里第 i 位是 0 ⇔ 元素 i 不在集合里
注意:位向量最右边是第 0 位,往左依次是第 1 位、第 2 位……
按位与 a & b → 集合交集 A∩B
按位或 a | b → 集合并集 A∪B
按位取反 ~a → 集合补集 A
4.位移运算
左移运算 x << k
规则:x 向左移动 k 位
丢弃最高的 k 位
在右端补 k 个 0
例子(8 位)
x = [01100011],x << 4:
丢弃最高 4 位 0110
右端补 4 个 0
结果:[00110000]
移运算 x >> k
右移行为分两种,这是和左移最大的区别:
1. 逻辑右移
规则:在左端补 k 个 0
2.算术右移
规则:在左端补 k 个 “最高有效位” 的值(即符号位)
如果原数最高位是 1,就补 1
如果原数最高位是 0,就补 0
用途:对有符号整数运算非常有用
左移:右端补 0
逻辑右移:左端补 0
算术右移:
[01100011] 最高位是 0 → 补 0 → [00000110]
[10010101] 最高位是 1 → 补 1 → [11111001]
C 语言标准的模糊性
有符号数:C 标准没有明确定义是用逻辑右移还是算术右移
风险:假设某一种右移的代码,可能在不同编译器 / 机器上有不同行为(可移植性问题)
现实:几乎所有编译器 / 机器组合,对有符号数都使用算术右移,程序员也普遍默认这一点
无符号数:C 标准明确要求必须使用逻辑右移
优先级陷阱
很多人会写类似 1 << 2 + 3 << 4 的表达式,本意是:
(1<<2)+(3<<4)
但在 C 语言中,加法(+、-)的优先级比移位运算(<<、>>)更高,所以实际等价于:
1<<(2+3)<<4
再按移位运算从左到右结合,变成:
((1<<5)<<4)
计算结果:
1 << 5 = 32
32 << 4 = 512
而不是期望的 (1<<2)+(3<<4) = 4 + 48 = 52。
核心结论
优先级:+、- > <<、>>
结合性:移位运算从左到右结合
风险:搞错优先级是 C 语言中非常常见且隐蔽的 bug 来源
✅ 最佳实践
当你拿不准优先级时,一定要加括号!


| 特性 | 无符号数编码 B2Uw | 补码编码 B2Tw |
|---|---|---|
| 符号 | 只能表示非负数 | 可正可负 |
| 最高位权重 | +2^w−1 | −2^w−1(符号位) |
| 最小值 | 0 | −2^w−1 |
| 最大值 | 2^w−1 | [2^(w−1) ]−1 |
| 范围对称性 | 对称(0 到最大值) | 不对称(负数范围比正数多 1) |
| 唯一性 | 双射(一一对应) | 双射(一一对应) |

这张表展示了不同字长 w(8/16/32/64 位)下,几个核心整数的十六进制位模式和十进制数值:
| 数值 | w=8 位 | w=16 位 | w=32 位 | w=64 位 |
|---|---|---|---|---|
| UMaxw | 0xFF / 255 | 0xFFFF / 65535 | 0xFFFFFFFF / 4294967295 | 0xFFFFFFFFFFFFFFFF / 18446744073709551615 |
| TMinw | 0x80 / -128 | 0x8000 / -32768 | 0x80000000 / -2147483648 | 0x8000000000000000 / -9223372036854775808 |
| TMaxw | 0x7F / 127 | 0x7FFF / 32767 | 0x7FFFFFFF / 2147483647 | 0x7FFFFFFFFFFFFFFF / 9223372036854775807 |
| −1 | 0xFF | 0xFFFF | 0xFFFFFFFF | 0xFFFFFFFFFFFFFFFF |
| 0 | 0x00 | 0x0000 | 0x00000000 | 0x0000000000000000 |


原因:
一半位模式(符号位 = 1)表示负数
另一半位模式(符号位 = 0)表示非负数(包含 0)
0 占用了一个非负数编码,导致可表示的负数比正数多 1 个
影响:补码运算会有特殊属性,容易引发细微程序错误
无符号最大值与补码最大值的关系


标准的模糊性
C 语言标准没有强制要求用补码表示有符号整数
但几乎所有现代机器都使用补码表示有符号数
可移植性建议:
不要假设具体数值范围(除了 C 标准保证的最小范围)
不要假设有符号数的具体表示方式
想获得最大可移植性,应依赖 <limits.h> 中定义的常量
C 库头文件 <limits.h>


| 特性 | 补码(Two's complement) | 反码(Ones' complement) | 原码(Sign-Magnitude) |
|---|---|---|---|
| 0 的编码 | 唯一 [00…0] | 两种:+0/-0 | 两种:+0/-0 |
| 负数范围 | 比正数多 1 | 和正数对称 | 和正数对称 |
| 现代机器 | 几乎全部使用 | 几乎淘汰 | 几乎淘汰 |
| 负数计算 | 2w−x | [11…1]−x | 只改符号位 |
补码为什么比原码行,why_补码范围为什么比原码大-CSDN博客

C 语言中,同字长的有符号数 ↔ 无符号数强制转换时,底层二进制位模式完全不变,只是改变了 “如何解读这些位” 的规则:
有符号 → 无符号:用无符号规则(B2U)去解释原来的补码位模式
无符号 → 有符号:用补码规则(B2T)去解释原来的无符号位模式
数值可能变,但位模式本身没有修改



运算中的隐式升级规则
当表达式同时包含有符号数和无符号数时:
C 语言会隐式将有符号数转换为无符号数,并假设两者都是非负数
这会导致非直观的比较结果
5.截断数字

short sx = -12345; // 补码:0xCFC7(16位)
unsigned short usx = sx; // 无符号:53191(0xCFC7)
int x = sx; // 符号扩展 → 0xFFFFCFC7(值仍-12345)
unsigned ux = usx; // 零扩展 → 0x0000CFC7(值仍53191)
输出:
sx = -12345 → 位模式 cf c7(16 位)→ ff ff cf c7(32 位,符号位补 1)
usx = 53191 → 位模式 cf c7(16 位)→ 00 00 cf c7(32 位,补 0)
.符号扩展的数学证明
转换顺序的影响
C 语言规定:先扩展位长度,再转换符号。
short sx = -12345;
unsigned uy = sx; // 等价于 (unsigned)(int)sx,先符号扩展到32位,再转无符号
结果:uy = 4294954951(0xFFFFCFC7),而非 53191。
若先转 unsigned short 再扩展:(unsigned)(unsigned short)sx 结果才是 53191。
截断数字(从大类型 → 小类型)
核心规则
将 w 位截断为 k 位:丢弃高位 w−k 位,数值可能改变(溢出)。

int x = 53191;
short sx = (short)x; // 截断为16位 → -12345
int y = sx; // 符号扩展回32位 → -12345
关于有符号数与无符号数的建议
1. 隐式转换的风险
有符号数 ↔ 无符号数的隐式转换会导致非直观行为,极易引发程序错误(甚至安全漏洞)。
例子:-1 < 0U 被解释为 4294967295 < 0,结果为假(逻辑错误)。
实安全漏洞案例(FreeBSD)
int copy_from_kernel(void *user_dest, int maxlen) {
int len = KSIZE < maxlen ? KSIZE : maxlen;
memcpy(user_dest, kbuf, len); // maxlen 为负数时,len 被转为无符号大整数
}
风险:若 maxlen 为负数,len 被隐式转为无符号数,导致 memcpy 复制大量内核数据到用户缓冲区,泄露敏感信息。
修复:将 maxlen、len 声明为 size_t(无符号类型,与 memcpy 参数一致)。

382

被折叠的 条评论
为什么被折叠?



