内存不是黑盒。
你把它想成一排“字节格子”。
从地址 0 排到地址 N-1。
每一格。
正好 1 个字节。
很多问题就不再神秘。
比特。
字节。
地址。
sizeof。
会在同一张图上对齐。
很多教材也会用一句话开头。
然后就结束了。
bit 是比特,byte 是字节,1 byte = 8 bit。
这句话没有错。
但它太像“背诵题”。
你背下来。
写 C/C++ 的时候。
还是会在调试器里盯着一排十六进制发呆。
更要命的是。
“字节”这东西。
历史上还真不是一开始就统一成 8 位。
早年机器的“字符宽度”。
有过 6 位。
也有过 9 位。
大家各有各的算盘。
直到通信、ASCII、生态、硬件工业化。
把 8 位的 byte(更严谨叫 octet)推成了事实标准。
于是我们今天才敢把 1 byte = 8 bit 写得这么理直气壮。
但对程序员来说。
历史的结论并不重要。
重要的是。
你脑子里要有一幅画。
这幅画不是“抽象的内存”。
就是一排一排的字节格子。
你写下 sizeof(int) 的那一刻。
到底是在数什么。
这一篇我不打算列公式。
我们就用你最熟的 C 风格代码。
把“bit / byte / 内存”这三件事。
钉在同一幅图上。
1. 一行简单的 C 代码背后发生了什么?
先从你肯定写过的一行代码开始:
char c = 'A';
编译器会在内存里给 c 找一个位置。
为了方便想象。
我们先假装内存长这样:
内存(只画出很小一段):
地址: 100 101 102 103 104 105 ...
+-----+-----+-----+-----+-----+-----+
| | | | | | |
+-----+-----+-----+-----+-----+-----+
你可以把这条横线当作一个巨大的数组。
每一个小格子。
正好是 1 个字节。
上面标的 100、101、102...。
就是这些格子的“下标”。
在 C 里。
我们把它叫做“地址”。
当执行到 char c = 'A'; 这一句时,编译器会挑出其中一个格子,比方说地址是 100 的那一格,把字符 'A' 的编码写进去,并且在符号表里记一笔:
变量名
c——> 使用地址为 100 的这个字节格子。
以后你在代码里写 c。
就相当于告诉机器。
“去地址 100 那一格。
把里面那个字节。
按字符的方式读出来”。
到这里你已经有了第一幅图:
内存是一长条由“字节格子”构成的数组。
变量就是在这条数组上。
“占了几个格子”的名字。
2. 一个字节里面到底放了什么?
接下来,把刚才那个地址为 100 的格子放大来看:
地址 100 这一格:
+---+---+---+---+---+---+---+---+
| b7| b6| b5| b4| b3| b2| b1| b0|
+---+---+---+---+---+---+---+---+
这里的 b0 ~ b7。
就是这一字节里的 8 个 bit(比特)。
你可以把它们想象成 8 个只能开 / 关的小开关。
开代表 1。
关代表 0。
如果 c 存的是字符 'A',在 ASCII 编码下,它对应的十六进制数是 0x41,也就是二进制的:
0x41 = 0b0100 0001
对应到上面的 8 个小格子,就是:
+---+---+---+---+---+---+---+---+
| 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
+---+---+---+---+---+---+---+---+
同样的 8 个比特。
如果你把它当整数来解释。
它就是 65。
如果你把它当字符来解释。
它就是 'A'。
这给我们第二幅图:
一个 字节。
可以理解成“装了 8 个比特的小盒子”。
盒子里装的是 0/1 的组合。
至于把这 8 个 0/1 当成什么。
要看你用什么类型去解释。
3. 从一个字节到一串字节:短字符串的例子
只看一个字节有点抽象,我们来看一段你更熟悉的代码:
char s[] = "Hi"; // 包含结尾的 '\0'
这个数组里一共有三个字符。
'H'。
'i'。
还有结尾的 "\0"。
编译器可能会把它们。
放在地址 200 开始的三个格子里:
地址: 200 201 202
+------+------+------+
内存: | H | i | '\0'|
+------+------+------+
如果再把每一个字节展开,就会看到三组 8 个比特:
'H' -> 0x48 -> 0b0100 1000
'i' -> 0x69 -> 0b0110 1001
'\0'-> 0x00 -> 0b0000 0000
和前面的 char c = 'A'; 对照着看。
你会发现内存还是那条字节数组。
只不过这次。
我们连续用了 3 个格子。
s[0] 就是地址 200 的那一格。
s[1] 是 201。
s[2] 是 202。
所以,当我们说“字符串是以 \0 结尾的一串字符”时,在内存里的样子其实非常朴素:
从某个起点地址开始。
一格一格连续往后放
char。遇到那一格全为 0 的字节。
就当作结束。
以后你在调试器里看到一段内存。
既能被显示为十六进制字节。
又能被显示为 ASCII 字符。
其实就是在同一块“字节数组”上。
换了一副眼镜在看。
4. 多字节的类型:数“格子”的方式理解 sizeof
现在把视角从 char 换到你更关心的 int 上。
假设在某个平台上。
sizeof(int) 恰好是 4。
你写了这样一段代码:
int a = 1;
int b = 2;
编译器可能会这样安排它们。
在那条“内存数组”上的位置:
地址: 300 301 302 303 304 305 306 307 ...
+-----+-----+-----+-----+-----+-----+-----+-----+
内存: | a 的四个字节 | b 的四个字节 | ...
+-----+-----+-----+-----+-----+-----+-----+-----+
这时再来看 sizeof 的输出:
std::cout << sizeof(a) << "\n"; // 打印 4
你就可以把它直接翻译成一句中文:
a这个变量在那条内存数组上。一共占了 4 个连续的字节格子。
同理。
之前的 char c。
sizeof(c) 就是 1。
表示只占了 1 个字节格子。
这里我们暂时不去展开。
“a 的四个字节内部。
是高位在前。
还是低位在前”。
那是大小端的问题。
会在后面的文章里讲。
我们先把一个直觉钉死:
sizeof告诉你的。就是“这个类型的一个对象。
需要在那条内存数组上。
占多少格子”。
有了这个直觉。
结构体。
数组。
指针运算。
都会变得好理解很多。
5. 再看一眼 bit 和 byte 的关系
回过头来。
总结一下 bit 和 byte 在这幅图里的位置。
整块内存是一条巨大的“字节数组”。
每个“数组元素”。
就是 1 个字节。
每个字节内部。
藏着 8 个 bit。
是更细一层的 0/1。
而 sizeof 数出来的。
是“有多少个字节元素”。
不是“有多少个 bit”。
如果你愿意再往下拆,可以把整个画面想象成一串嵌套的数组:
内存: byte MEMORY[N]; // 从地址 0 到地址 N-1,每格 1 字节
字节: bit BYTE[8]; // 每个字节里有 8 个 bit
变量: 占用 MEMORY 里的若干连续格子
这当然不是 C++ 的真实语法。
但作为心智模型。
已经足够用了。
6. 小结:以后脑子里该出现什么画面?
以后你再看到。
“比特 / 字节 / 地址 / sizeof”。
我希望你脑子里能自然浮现几幅画面。
你写 char c = 'A'; 的时候。
某个地址上的一格字节。
被填上了 0x41。
它内部的八个 bit。
变成了 0100 0001。
你写 char s[] = "Hi"; 的时候。
从某个起点开始。
连续三个字节格子里。
分别放进了 'H'、'i'。
以及结尾的 "\0"。
你写 sizeof(s) 或 sizeof(int) 的时候。
其实是在问。
“这个东西在那条内存数组上。
一共占了多少个字节格子?”
有了这几幅画面。
后面我们再去谈。
位宽。
补码。
整数溢出。
浮点编码。
结构体对齐。
大小端。
就不再是空洞的名词。
它们都会在这条“内存数组”上。
找到自己的位置。