编程语言研究书库

数据存储格式 — 从开关到「123abc」的两种存法

数据存储格式 — 从开关到「123abc」的两种存法

这一章讲什么: 计算机里发生的一切都由双态开关控制;人类为了书写开关状态发明了 位和十六进制;开关的集合在内存里变成「有地址的字节」;字符、字符串、整数各有自己的 编码方式;最后用一个 C 程序和调试器,亲眼看见同一个输入按不同格式存放时内存里的差别。 读完你会真正理解「内存里存的不是数字,是编码」——这是后面所有章节的暗线。

1. 顶层全景:一台计算机就是一堆开关

这一章的视角切换是全书第一个观念升级。第 1 章把计算机看成 CPU + 内存 + I/O; 这一章把它看成数十亿个双态开关,外加一两个能检测和改变开关状态的控制单元1。 你写的代码、处理的数据、屏幕上的图像——全部由开关组合的状态表示。

问题马上来了:怎么描述一台计算机的状态?「第 1 个开关开,第 2 个开,第 3 个关……」 写到第一千个开关时没人读得下去。所以人类做了三层压缩:

  物理层:数十亿个双态开关(通/断)
     │  一层开关 = 一个二进制数码(位,0/1)
     ▼
  记号层:位太长不好读 → 4 位并成一个十六进制数码,8 位并成一个字节
     │  内存给每个字节编一个固定地址
     ▼
  编码层:程序员决定「这一串位代表什么」——字符?整数?字符串?
         同一种位模式,按不同编码读,得到完全不同的东西

  图说:三层每一层都在解决上一层的「不好用」,编码层是全章的落点。

「计算机由开关组成」这句话的威力在于:它把「数据」从抽象物变成了物理状态的描述。 后面第 3 章的算术、第 4 章的逻辑、第 5 章的晶体管,全是从这里长出来的。

这一章在全书链条里的位置:第 1 章搭了「三个子系统」的空架子,这一章往架子里放 第一样东西——数据。放什么(编码)、放多少位(容量)、放在哪(地址)、 怎么验货(gdb),四个问题各有一节;而「算术怎么算」被刻意留到第 3 章, 「谁来存」留到第 8 章——本章末尾你会带着「位模式」这个共同语言进入硬件部分。

2. 位、字节与十六进制:开关的人类记号

2.1 一位一个开关

二进制数码(binary digit,简称位(bit))表示一个开关的状态:0 表示关,1 表示开2。 0 和 1 对应哪边其实无所谓——「重要的是保持一致」。原书举例:某台计算机第 1 个开关开、 第 2 个开、第 3 个关、第 4 个开,用二进制写作 1101。

2.2 十六进制:每 4 位换一个好写的符号

位数一多,二进制的长串还是难读。于是每 4 位并为一个十六进制数码(09 加 af 共 16 个),查一张固定的对照表3:1101 对应 d,0101 对应 5,等等。8 位(一字节)恰好是 两个十六进制数码,所以 1101 0010 写作 d2。

十六进制与二进制的换算规则简单到可以心算:把十六进制数逐位展开成 4 位二进制即可。 原书给了两个例子:6c2a₁₆ = 0110110000101010₂、0123abcd₁₆ 逐位展开成 32 位4。 把 6c2a 走一遍,体会「逐位展开」是什么意思——每一位数码独立翻译,互不干扰:

  6      c      2      a
  0110   1100   0010   1010      ← 查表 2-1,一位换四位
  → 0110 1100 0010 1010

  反过来,把二进制 0110110000101010 从右往左每 4 位切一刀(最左边不足 4 位补 0),
  每刀查表换回一个数码,就回到 6c2a。

「一位十六进制 = 四位二进制」这个 4:1 的关系,是 16 = 2⁴ 的直接推论; 八进制是 3:1(8 = 2³),同理3。记住比例,遇到任何「几位一组」的进制都不慌。

记法约定:上下文不清楚时,用下标标明基数——100₁₀ 是十进制,100₁₆ 是十六进制, 100₂ 是二进制。C 语言里 0x 前缀表示十六进制,0 前缀表示八进制,0b 前缀表示二进制 (最后这个是 gcc 的扩展,不是标准 C)5:

  100 = 0x64 = 0144 = 0b01100100     // 四个写法是同一个值

顺带一提八进制:每个八进制数码代表 3 位(而不是十六进制的 4 位),今天不常见, 但你在 Unix 文件权限里会撞见它(补充,不在书里)。

2.3 字节:计算机一次能访问的最小单位

单个位通常没有用武之地。计算机一次能访问的最小位数被定义为字节(byte), 现代计算机一字节 = 8 位6。

「8 位」不是天经地义:原书特意举例,CDC 6000 系列大型机的字节是 6 位6。 这个提醒值得记住:很多我们以为是「定律」的东西,其实是历史选择。

2.3 八进制:3 位一组的老三进制皮肤

八进制系统每个数码代表 3 位(07 对应 000111),今天不常见, 但 Unix 系统的文件权限(读 4 / 写 2 / 执行 1)用的就是八进制思维,你迟早会撞见7。

记法上有个陷阱必须记住:C 语言里数字前只加一个 0 表示八进制—— 写 0144 是八进制的一百四十四(= 十进制 100),不是一百四十四5。 初学时在这里栽跟头的方式是:想写 10 个什么,写成 010,结果程序按 8 个算。 这是「记号约定咬人」的第一个实例,后面汇编的语法前缀(第 10 章)还有第二轮。

3. 位置记数法:三种进制是同一套乘法

现在回答一个根本问题:二进制数到底「等于」十进制的几?答案靠位置记数法 (positional notation):符号的值取决于它在数字中的位置8。

在位置记数法之前,人类用的是「计数系统」:罗马数字就是代表,I 是 1、V 是 5、 X 是 10,数值靠符号的加减堆砌——IV 表示 4(小符号在大符号前就做减法)。 罗马数字没有 0:不是古人想不到,而是计数系统里根本不需要—— 没有「位置」这个概念,就不存在「占位」的需求。位置记数法必须有一个符号表示 「这一位没有值,但位置本身有意义」:500 里的两个 0 说的正是这件事。 原书给了一个让人会心一笑的检验:试试用罗马数字算 LX ÷ III(答案 XX), 你立刻会明白为什么位置记数法「催生了我们今天所知的数学」9。

十进制 123 的意思是:

  123 = 1×10² + 2×10¹ + 3×10⁰

同一个符号 5,在 50 里值 50,在 500 里值 500——样子没变,位置变了。 最右边的数码叫最低有效数码(贡献最小),最左边叫最高有效数码(贡献最多)8。

把底数 10 换成任意 r,就得到通用形式:

  d(n-1)×r^(n-1) + d(n-2)×r^(n-2) + … + d1×r¹ + d0×r⁰

二进制就是 r = 2 的特例:只有 0、1 两个符号,向左挪一位值翻一倍10。

这个展开式是双向的,它同时给出两条转换算法。原书把两条都写成了可以照抄的算法11:

  二进制 → 十进制:
    Let result = 0
    Repeat for each i = 0,...,(n - 1)
        add di × 2^i to result          // 该位的比特值 × 2 的「位置次幂」

  十进制 → 二进制:
    quotient = N
    di = quotient % 2                   // % 是取余数
    quotient = quotient / 2             // / 是整数除法
    While quotient != 0
        i = i + 1
        di = quotient % 2
        quotient = quotient / 2

注意一个写法惯例:这两条算法里的 / 是整数除法(丢掉余数),% 是取余数。 这两个运算符后面会原样出现在 C 和汇编里(第 16 章的除法指令一节会正式重逢), 符号先在这里认个脸熟。

二进制 → 十进制:逐位乘 2 的幂再相加。 原书的例子:

  10010101₂ = 1×2⁷ + 0×2⁶ + 0×2⁵ + 1×2⁴ + 0×2³ + 1×2² + 0×2¹ + 1×2⁰
            = 128 + 16 + 4 + 1
            = 149₁₀

十进制 → 二进制:反复除以 2,余数从下往上(从右往左)排。 原理一句话: 等式两边同除以 2,右边所有含 2 的幂的项都还能被整除,只有最低位 d₀ 孤零零地落在余数里—— 所以第一次除法的余数就是最低位,再除一次余数是次低位,以此类推11:

  以 N = 149 为例:
  149 ÷ 2 = 74 余 1   ← d₀(个位)
   74 ÷ 2 = 37 余 0   ← d₁
   37 ÷ 2 = 18 余 1   ← d₂
   18 ÷ 2 =  9 余 0   ← d₃
    9 ÷ 2 =  4 余 1   ← d₄
    4 ÷ 2 =  2 余 0   ← d₅
    2 ÷ 2 =  1 余 0   ← d₆
    1 ÷ 2 =  0 余 1   ← d₇(最高位)
  读余数从下往上:10010101₂ = 149₁₀ ✓

为什么要费这个劲,而不是直接用十进制?因为计算机由双态开关构成,以 2 为基数来考虑 存储在计算机中的数值是有意义的12。十六进制和十进制只是人类读写的两种皮肤。

4. 内存:地址固定,内容可变

有了「字节」这个单位,就能精确描述存储器了。计算机配两种存储器13:

存储器全称断电后控制单元能做什么
RAM随机存取存储器数据丢失读、写任意位的状态
ROM只读存储器(非易失)数据保留只能读(特殊硬件可重编程)

「随机存取」这个名字容易误读:它的意思是「访问任何字节花的时间一样长」, 不是「乱取」。反例是磁带那样的顺序存取存储器:想听一首歌,得先倒到它的位置, 花费取决于它离磁头多远14。

4.1 地址:字节的房间号

内存中的每个字节都有一个地址,像办公楼的房间号。地址永不改变——从内存开头数 第 957 字节,永远是第 957 字节;但该字节里的 8 位状态可以随时改写15。 这个「地址固定、内容可变」的对偶,就是后面一切编程的地基:变量名、指针、数组, 全都是在这对概念上搭的。

惯例:地址用十六进制从 0 开始书写。内存的前 16 字节,地址就是 0, 1, 2, …, 9, a, b, c, d, e, f—— 第 957 字节的地址写作 0x3bc(原书特意玩了个文字游戏:0x3bc 的十进制是 956, 因为地址从 0 起数)15。内存的样貌用这种记法:

  <地址>: <内容>
  0x00000000: 0x6a
  0x00000001: 0xf0
  …

4.2 程序员存数据前必须回答两个问题

原书把「往内存存东西」提炼成两个决定16:

  1. 需要多少位? n 位能表示 2ⁿ 个不同的值。一字节:256 种;二字节:65 536 种。 而且你不一定用完分配到的所有位模式——这是编码冗余的根源。
  2. 编码是什么? 日常数据(字母、金额)不是现成的 0/1,程序员必须决定怎么编码。

把第一个问题算成一张速查表(原书环境的无符号整数)17:

分配的位数字节数不同值的数量无符号整数范围
81256(2⁸)0 ~ 255
16265 536(2¹⁶)0 ~ 65 535
3244 294 967 296(2³²)0 ~ 4 294 967 295
648约 1.8×10¹⁹(2⁶⁴)0 ~ 约 1.8×10¹⁹

注意范围的「差一」:8 位有 256 种位模式,能表示的却是 0~255——最大值永远是 2ⁿ − 1,因为 0 也占一个名额。这个差一后面还会出现在数组下标、循环边界上, 值得现在就记牢。64 位一行是补充(不在书里,来自通用知识):x86-64 的通用寄存器 就是 64 位宽,第 9 章会见到。

这两个问题贯穿全章:字符一节回答「字母怎么编码」,整数一节回答「数字怎么编码」, BCD 一节展示「另一种回答的代价」。

5. 字符:码点、字形与 NUL 结尾的字符串

5.1 码点 ≠ 字形

最常见的字符编码是 Unicode UTF-8。它用 1~4 字节存储一个数,这个数叫码点 (code point),写作 U+ 加十六进制数码——比如 U+0041 是拉丁大写字母 A18。

这里有一对必须分开的概念:

  • 码点:字符的编号,是存进内存的那个数;
  • 字形(glyph):你在屏幕或纸上看到的字符外观。同样的码点 U+0041,在宋体里是 A, 在黑体里也是 A,长得不一样,码点一样。

UTF-8 向后兼容旧标准 ASCII:ASCII 用 7 位给 128 个字符编号——英文字母大小写、数字、 标点、控制字符。本书编程只用 UTF-8 的 ASCII 子集(U+0000 至 U+007F),每个字符恰好 1 字节19。

一个展示「ASCII 子集之外」的例子:码点 U+00B5(微符号 µ)在内存里以 16 位模式 0xc2b5 存储——两字节,不再是「码点数字 = 字节值」的简单对应20。这就是 UTF-8 「1~4 字节变长」的含义:码点越大,占的字节越多,而且头几个字节里藏着 「后面还跟了几个字节」的标记位。变长的完整规则本章不展开,记住结论即可: ASCII 范围(0~127)内的字符,UTF-8 与 ASCII 一模一样;超出范围的,一律多字节。

一个对调试极有用的事实:在 ASCII 子集里,字符的码点几乎等于它的「形状直觉」—— 0 是 0x30,9 是 0x39(连续);a 是 0x61,f 是 0x66(连续);A 是 0x41。 位 6 和位 5 两个开关的组合把字符分成四组:控制字符 / 数字和符号 / 大写字母 / 小写字母21。

关键区分:'0'(字符零,0x30)和 0(数字零)是两回事。 字符 '5' 的码点是 0x35; 数字 5 的二进制是 0x05。把文本数字变成算术数字,正是 scanf/printf 这类函数干的事 (下一节展开)。

两个顺手的工具:Linux 终端里 man ascii 能打印完整的码点表(内容很多,不必背, 但「粗略了解其组织形式」值得)22;想深入了解 Unicode 的来龙去脉,原书推荐 Joel Spolsky 的文章《The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)》22—— 这篇 2003 年的文章至今仍是程序员 Unicode 入门的标准读物(补充,不在书里:该文 可在 Joel on Software 网站上找到)。

5.2 C 字符串:末尾藏着一个 NUL

C 风格的字符串用码点 U+0000(NUL)作为哨兵值标记结尾23。以 "Hello, World!\n" 为例:

  地址        内容      字符      地址        内容
  0x4004a1:   0x48      H         0x4004a9:   0x6f  o
  0x4004a2:   0x65      e         0x4004aa:   0x72  r
  0x4004a3:   0x6c      l         0x4004ab:   0x6c  l
  0x4004a4:   0x6c      l         0x4004ac:   0x64  d
  0x4004a5:   0x6f      o         0x4004ad:   0x21  !
  0x4004a6:   0x2c      ,         0x4004ae:   0x0a  \n(换行)
  0x4004a7:   0x20      (空格)    0x4004af:   0x00  NUL(哨兵)
  0x4004a8:   0x57      W

  图说:13 个可见字符 + 1 个换行 + 1 个 NUL = 15 字节。
  「Hello, World!」看得见的内容只占 13 字节,内存却为它分配了 15 字节——
  最后那个 0x00 平时完全不可见。

两个细节值得记住:

  • C 源码里写 \n 两个字符,内存里只存一个字节 0x0a(LF)23;
  • 对比另一种设计:Pascal 字符串把长度放在第一字节(所以最长 256 个字符), C++ 的字符串类外壳之下,实际存的还是 C 风格 NUL 结尾字符串24。 「怎么标记结束」是字符串设计里最重要的一次分叉,两个决定延续至今。

把两种设计并排,各自的账单很清楚:

C 风格(NUL 结尾)Pascal 风格(长度前缀)
长度从哪来遍历到 NUL,数出来读第一字节,一步到位
额外开销永远 1 字节1 字节,但限制了最长 256
翻车方式忘记留 NUL 的位置 / 缓冲区溢出时读飞长度字节被写坏
今天的遗存C 及其后裔(包括 C++ 的 .c_str())Pascal、部分网络协议的设计思路

这张对照表是我们自己的整理,两边的具体事实(NUL 哨兵、256 上限、C++ 内部仍是 C 字符串)都来自原书2324。

6. 无符号整数与 BCD:两种「存数字」的答案

6.1 直接存二进制

无符号整数最直白的存法:就存它的二进制展开。123₁₀ = 7b₁₆,一字节状态就是 01111011。 一字节能存 0255;书里的编程环境默认无符号整数 4 字节,范围 04 294 967 29525。

6.2 「123」的字符串与整数值,在内存里是两套字节

这是本章第一个真正有冲击力的对照,也是主线例子的起点: 十进制数 123,按字符串存是 4 个字节 0x31 0x32 0x33 0x00(每个字符一个码点加 NUL); 按无符号整数存是 4 个字节 0x0000007b26。

  输入:123(从键盘敲的三个字符 '1' '2' '3')

  按字符串存:  0x31 0x32 0x33 0x00        ← 是字符的码点,不是「一百二十三」
  按整数存:    0x00 0x00 0x01 0x7b(大端书写)= 二进制 1111011

  图说:同一串键盘输入,解释规则不同,内存里的字节完全不同。
  「数字本身不分进制」——123 只是值;二进制、十进制、十六进制是它的三种写法[^10]。

原书在这里点破了一个结构性的事实:二进制系统的局限之一,是执行算术运算之前, 你得先把字符串形式的十进制数转换成二进制数;反过来,显示时又得转回十进制字符27。 这个「来回转换」的刚需,正是 I/O 库函数存在的理由——你每次 scanf/printf, 都在付这笔转换税。等第 11 章自己用汇编调用 write 时,你会亲手把这笔税算一遍。

于是「执行算术运算前,你得先把字符串的十进制数转成二进制数」这件事,就成了刚需; 反过来,显示时又得从二进制转回十进制字符。这个来回转换就是 I/O 库函数的家常便饭。

6.3 BCD:用空间换转换方便

BCD(二进制编码的十进制)是另一种回答:每个十进制数码独占 4 位,0→0000, 9→100128。

  十进制 1234:
  二进制存法:  0000 0100 1101 0010      (0x04d2,16 位全用上)
  BCD 存法:    0001 0010 0011 0100      (每个数码 4 位,一眼能读)

代价清清楚楚:4 位有 16 种组合,BCD 只用 10 种,6 种浪费掉;16 位二进制能存 065 535, BCD 只能存 09 99929。为什么要忍受浪费?因为「BCD 在字符格式和整数格式之间转换 比较简单」——做财务的系统大量做的是打印数字,不是算数字。COBOL 的商业数据系统 至今用压缩 BCD 格式:一个字节塞两个数码,最后半个字节存符号30。

这条权衡(编码空间 ↔ 转换成本)会在第 19 章小数那里再演一遍,那时换成浮点 vs 定点。

再补一个 BCD 处理符号的例子:压缩 BCD 用最后一个 4 位存符号—— 0001 0010 0011 1010 表示 +123,…1011 表示 −123,…1111 表示无符号 12330。 同样 16 位,符号只是换了个码,整串数码肉眼可读——这正是商业系统要的效果。

7. 主线例子:intAndString —— 同一输入的两种命运

现在把前面所有零件装进一个程序。原书不用「Hello, world」开场,而是用清单 2-1 的 intAndString.c——它读入同一个十六进制值,一次当整数存,一次当字符串存,然后并排显示。 先把整个程序逐行过一遍,每一行都标出它对应的角色:

/* intAndString.c
 * Read and display an integer and a text string.
 */                                    // ① 注释:只给人类读者看,对程序没有任何影响
#include <stdio.h>                     // ② 包含头文件:告诉编译器 printf/scanf 的接口

int main(void)                         // ③ 程序入口:C 运行时搭好环境后调用 main
{
unsigned int anInt;                  // ④ 变量定义:名字 + 类型,编译器据此分配内存
  char aString[10];

  printf("Enter a number in hexadecimal: ");   // ⑤ 调用 printf,实参是一个字符串
  scanf("%x", &anInt);                          // ⑥ %x:把输入字符按十六进制换算成整数
  printf("Enter it again: ");
  scanf("%s", aString);                         // ⑦ %s:把输入字符原样存成字符串
  printf("The integer is %u and the string is %s\n", anInt, aString);

  return 0;                                     // ⑨ 返回 0:向操作系统报告一切正常
}

每一行背后是一条规则,值得停下来认全:

  • 注释(/* … */):只服务于人类读者;原书建议自己写代码时连作者名和日期 一起写上(书里为省纸省略了)31。
  • #include <stdio.h>:编译器必须知道每个函数的进出数据的类型才能检查调用; 头文件提供函数原型(接口声明),stdio.h 是 C 标准库众多函数的接口集合31。
  • int main(void):C 程序按函数组织;执行时操作系统先布置好 C 运行时环境, 由它调用 main,main 再去调别的函数,最后控制权回到运行时环境32。
  • 实参与形参:调用方在调用时给出的输入叫实际参数(argument), 被调函数内部用来接收的名字叫形式参数(parameter)——printf("…") 里那个字符串 是实参,printf 内部接它的名字是形参33。这组术语第 14 章讲参数传递时会正式上岗。
  • char aString[10]:数组定义——类型 char、名字 aString、10 个元素, 于是能存「9 个字符 + 1 个 NUL」的 C 风格字符串34。为什么数组定义的长度比 能存的字符多一,现在有了答案(NUL 也要占房间)。
  • C 的老规矩:变量必须在函数开头定义(现代 C 已放宽,本书从简)—— 像菜谱先列配料再写步骤34。

还要交代这本书的编程范式坐标:C 是过程式编程语言——程序被划分为一组函数, 每个函数是一段具名的操作序列;面向对象那套范式 C 不支持,第 18 章会借 C++ 补上 面向对象在汇编层的样子35。

实际运行(书里的真实输出):输入 123abc 两遍,打印

  The integer is 1194684 and the string is 123abc

同一个 123abc,第一次进来被换算成数 1194684(= 0x123abc),第二次进来原封不动 存成 6 个字符的码点36。scanf 的 %x 和 %s 就是两种编码规则的入口。

7.1 程序是怎么被世界接待的

顺着第 ① 到 ④ 站再看一遍这个小程序的一生,这次带上每个角色的台词:

  1. 操作系统先把 C 运行时环境(C runtime environment)搭好,然后调用 main—— 所以每个 C 程序必须有一个叫 main 的函数32;
  2. main 向 C 运行时返回整数 0,意思是「一切顺利」;非 0 值报告出错37;
  3. printf/scanf 属于 C 标准库。你敲的每个键都是字符(char);scanf 把字符序列 按 %x 的规则换算成 int;printf 反向把 int 按 %u 换算回字符序列。 库函数和操作系统的关系:scanf 底层调用操作系统提供的 read 从键盘取字符, printf 底层调用 write 往屏幕送字符38;
  4. 格式字符串里的 %u、%d、%x、%s 叫转换说明符,告诉函数按哪种规则做 「字符 ↔ 值」的换算39。

顺带一个 C 的怪癖:scanf("%x", &anInt) 里变量名前要加 &(取地址运算符), 因为 scanf 需要知道结果存到哪里;而数组名 aString 本身就会被 C 当成地址, 所以不加40——为什么数组名是地址,第 17 章才彻底展开,这里先记下现象。

还有一个 scanf 的脾气,第一次见必踩:scanf 格式串里除了转换说明符之外的字符, 要求键盘输入严格逐字匹配。原书的例子:

scanf("1 %i and 2 %i", &oneInt, &twoInt);

你必须原样敲 1 123 and 2 456——and 一个字母都不能差41。 这让 scanf 适合书里的教学示例,却让很多人在实际输入时卡住(多敲一个空格, 匹配就失败、输入残留到下一次读取)。这个脾气不属于硬件知识,但属于你「用 C 做实验」 时会立刻撞上的现实。

8. 用 gdb 亲眼检查内存:debugger 是显微镜,不只是捉虫工具

这一章教 gdb(GNU 调试器)的方式值得单独说:作者明确把它定位成学习工具, 而不是等出了 bug 才用的救火队——「对于学习使用调试器这件事,简单的例子绝对比 复杂的例子效果好得多」42。

调试器是什么:一个能在另一个程序运行的同时观察和控制它的程序。主要手段是断点—— 程序跑到断点处暂停,把控制权交还给你,这时可以查看变量的值43。

上手命令一张表(原书给的清单)44:

命令作用
b 文件名:行号在指定行设断点
r从头运行被调试的程序
c从断点处继续运行
l 行号显示源代码(按 ENTER 重复)
print 表达式求值并显示(知道变量类型)
printf "格式", 变量…按 C 的格式规则显示
x/nfs 地址检查内存:n 个、格式 f、大小 s 的对象
i r显示 CPU 寄存器内容

编译进调试器的那一步也有讲究。书里的编译命令是:

  $ gcc -g -Wall -masm=intel -o intAndString intAndString.c

四个选项各司其职45:-g 把调试信息加进可执行文件(gdb 靠它认识你的变量名和行号); -Wall 让编译器对「语法正确但可能和你的意图不符」的代码发警告(比如声明了没用的变量, 往往意味着你有遗漏);-masm=intel 指定汇编语法风格——现在还用不上,但后面读汇编时 要和 Intel/AMD 手册的写法对齐,从现在就养成习惯;-o 指定输出文件名。

断点为什么设在第 17 行(最后那个 printf)?作者交代了他的偏好: 「我喜欢将断点设置在一个函数调用另一个函数的地方,以便在将参数传给被调用函数之前 先检查参数的值」46——停在被调函数的门口,看得见双方交接前的那一刻。

完整的会话走一遍,每一步都能看到「控制权在谁手里」:

  $ gdb ./intAndString                 ← 启动调试器,装载程序(还没运行)
  (gdb) l                              ← list:显示源代码,ENTER 重复
  (gdb) b 17                           ← 在第 17 行(最后的 printf)设断点
  Breakpoint 1 at 0x11f6: file intAndString.c, line 17.
  (gdb) r                              ← run:程序真正开始跑
  Enter a hexadecimal value: 123abc    ← 程序自己在跑,和直接运行无异
  Enter it again: 123abc
  Breakpoint 1, main () at intAndString.c:17    ← 撞上断点,控制权交还 gdb
  17    printf("The integer is %u and the string is %s\n", anInt, aString);

  图说:r 之后是「程序的时间」,撞上断点之后是「你的时间」;
  gdb 高亮的那一行(17)是「准备好但还没执行」的下一行。

注意撞线时 gdb 显示的那一行语句还没有执行——它是指令指针的下一站。 这是断点语义里最容易搞反的一点:「停在第 17 行」永远意味着「第 17 行尚未发生」。 想验参数的值,此刻正是时候;c(continue)把控制权还给程序,一路跑到结束或下一个断点。

把断点设在最后一行 printf 之前,程序停住,开始验货:

  (gdb) print anInt
  $1 = 1194684              ← 十进制显示(int 的默认待遇)
  (gdb) printf "anInt = %u = %#x\n", anInt, anInt
  anInt = 1194684 = 0x123abc   ← 同一个值,十六进制皮肤
  (gdb) print &anInt
  $3 = (unsigned int *) 0x7fffffffde88   ← 变量的房间号

x 命令是显微镜的镜头。它的帮助文本把全部语法压缩成一行 x/FMT ADDRESS,FMT = 重复次数 + 格式字母 + 大小字母47:

格式字母按什么显示大小字母一次看多大
o八进制b字节(1 字节)
x十六进制h半字(2 字节)
d有符号十进制w字(4 字节)
u无符号十进制g巨字(8 字节)
t二进制
c字符
s字符串

所以 x/4xb 读作「4 个、十六进制、字节」,x/1dw 读作「1 个、十进制、字」, x/10c 读作「10 个、字符(每个 1 字节)」。同一个地址三种看法:

  (gdb) x/1dw 0x7fffffffde88
  0x7fffffffde88: 1194684
  (gdb) x/1xw 0x7fffffffde88
  0x7fffffffde88: 0x00123abc
  (gdb) x/4xb 0x7fffffffde88
  0x7fffffffde88: 0xbc   0x3a   0x12   0x00     ← 逐字节的真身!

  图说:值 0x00123abc 在内存里按地址递增是 bc 3a 12 00——「反着」存的。
  这就是下一节的主角:小端序。

顺带解释一个当时看 print 输出会困惑的细节:print aString 显示 "123abc\000\177\000"——反斜杠加三个八进制数码是 gdb 表示「没有字形的码点」 的方式(NUL 没有字形,所以显示 \000;0x7f 是删除控制符,显示 \177)48。 对照表 2-2(八进制 ↔ 3 位组)就能手工解码这些转义。

9. 小端序:内存把多字节值「反着放」

逐字节看 0x00123abc,内存里出现的是 bc 3a 12 00——最低有效字节住在最低地址。 这叫小端序(little-endian):「最小」(littlest)的字节排在内存最前面49。

反着来的叫大端序(big-endian):最高有效字节在最低地址,同一值会存成 00 12 3a bc (原书拿 PowerPC 举例)50。

  值 0x00123abc,两种字节序对照(地址从左到右递增):

  小端(x86-64,我们的机器):  bc   3a   12   00
  大端(PowerPC 等):           00   12   3a   bc

  图说:字的「人类书写顺序」和「内存存放顺序」是两回事;gdb 显示整个字时会
  按机器的字节序自动拼回正确值,所以 x/1xw 在两台机器上都显示 0x00123abc,
  只有 x/4xb 逐字节看时才露馅。

什么时候要紧?原书的判断:大多数编程情况下字节序不是问题,但两种场合必须懂它—— 在调试器里逐字节检查内存时(不然你会以为数据存错了),以及两台不同字节序的机器通信时 (网络协议 TCP/IP 规定用大端序,即「网络字节序」;x86-64 是小端,操作系统会替你翻转; 但如果你写操作系统本身,或没有操作系统的嵌入式通信软件,就得自己管)51。

再把 gdb 的「双重视界」走完,这是新手最容易犯迷糊的一幕。逐字节看,小端机器上是:

  0x7fffffffde88: 0xbc
  0x7fffffffde89: 0x3a
  0x7fffffffde8a: 0x12
  0x7fffffffde8b: 0x00

「猛一看,值的顺序似乎存储反了」——原书原文的这句自言自语,正是每个读者的第一反应52。 但同一会话里让 gdb 显示整个 4 字节值,它显示 0x00123abc,完全正确52: gdb 知道当前是小端环境,显示整字时会把字节重新拼回「人类顺序」。 大端机器(PowerPC)上同一变量的两幕对照是:

  (gdb) x/1xw 0x7fffffffde88          [BIG-ENDIAN COMPUTER, NOT OURS!]
  0x7fffffffde88: 0x00123abc          ← 整字显示:两台机器一模一样
  (gdb) x/4xb 0x7fffffffde88
  0x7fffffffde88: 0x00  0x12  0x3a  0xbc   ← 逐字节看:顺序正好反过来

  图说:x/1xw(拼好的值)看不出字节序差别;x/4xb(单字节序列)才暴露它。
  学会问「我现在看的是拼好的值,还是单字节序列」,调试时就不容易被字节序吓到。

「little-endian / big-endian」这两个名字本身是一则计算机科学的著名笑话—— 出自《格列佛游记》里小人国为「从大头剥鸡蛋还是小头剥鸡蛋」开战的桥段, 由 Danny Cohen 1980 年引入计算机网络文献(补充,不在书里,来自通用知识)。 名字的来历不必记,但「端」指什么要记牢:端 = 最低地址那一头放的是值的哪一端。

补一句实用结论:你手上这本书的所有程序都跑在 x86-64 上,而 x86-64 架构用的是 小端序,没有开关、没有选择51。所以在本书范围内,字节序是个「知道就好」的 背景事实;它真正咬人的场景在原书点名的两个地方之外还有一个——读别人写进文件 或网络包的二进制数据时:如果数据来源是一台大端机器(不少网络协议、旧式处理器), 逐字节解析就必须先按大端拼装。这个场景做底层开发迟早会遇见(补充,不在书里, 来自通用知识)。

判断(我们的,不是书里的): 「值的书写顺序 ≠ 值的存储顺序」是学习底层的第一道坎, 而几乎所有教材都默认读者自动迈过去。这本书用 x/4xb 把坎亮出来,是本章写得最好的地方。 记住一个自查口诀:任何时候你在内存转储里看到一个「反着的」数,先问字节序,别急着怀疑数据坏了。 如果错,会错在: 如果查看工具显示时已经帮你按字节序重排(比如 gdb 的按字显示), 口诀就不适用——判据是:你看的是「单字节序列」还是「拼好的多字节值」。

7.2 C 和 C++ 的 I/O:同一件事的两种语法皮肤

书里给的 C 与 C++ 对照,值得逐行看一眼,因为它展示了「库函数负责转换」这个分工:

/* C 版:读一个整数、加 100、显示 */
int x;
scanf("%i", &x);
x += 100;
printf("%i", x);
// C++ 版:同一件事
int x;
cin >> x;
x += 100;
cout << x;

两个版本的工作完全相同:从键盘读字符序列(char),按格式规则换算成 int, 加 100,再换算回字符序列送上屏幕。差别只在语法皮肤:scanf 用格式字符串 ("%i")显式声明转换规则,cin >> 则靠变量的类型隐式决定怎么转换53。 记住这个对照的意义不在 C++ 本身,而在:转换规则既可以写在格式串里(显式), 也可以绑在类型上(隐式)——两种设计后来各成一派。

7.3 I/O 的三层结构:你的程序并不直接碰键盘

图 2-1 给了一个贯穿全书后半部分的分层:

  你的 C 程序
     │ 调用 printf / scanf
     ▼
  C 标准库           ← 负责「值 ↔ 字符」的格式转换
     │ 调用 read / write
     ▼
  操作系统           ← 真正拥有键盘和屏幕,通过系统调用开放给程序

  图说:程序从来不直接读写硬件;每次 printf 都是两次委托。
  直接调用 read/write 绕过库的写法,第 11 章就会用到。

原书预告:应用程序可以直接调用 read/write 读写字符,「这个话题留待第 16 章讨论」 (中文版把章节号标成 16,实际内容在第 11 章——原书此处指的是「自己编写转换函数」, 见第 11 章的 write/read 一节)54。自己动手写转换函数比用现成的库更能暴露 「数据在内存里到底长什么样」——这是作者反复用的教学策略:把抽象层剥掉一层再看。

顺带一个工程建议:原书强烈推荐用 make 构建程序,哪怕程序只有一行—— 「目前看来似乎有些大材小用,但使用简单的程序会更容易上手」55。 make 的语句本身就是「声明式编程」的好例子:你声明目标,make 决定执行顺序 (第 13 章开头会正式定义这个术语)。

7.4 主线例子继续走:字符串在内存里的真身与「垃圾值」

回到断点处,现在验字符串 aString。先拿到地址,再用两种镜头各看一遍:

  (gdb) print &aString
  $4 = (char (*)[50]) 0x7fffffffde8e
  (gdb) x/10c 0x7fffffffde8e
  0x7fffffffde8e: 49 '1'   50 '2'  51 '3'  97 'a'  98 'b' 99 'c' 0 '\000' 127 '\177'
  0x7fffffffde96: 0 '\000' 0 '\000'
  (gdb) x/10xb 0x7fffffffde8e
  0x7fffffffde8e: 0x31  0x32  0x33  0x61  0x62  0x63 0x00 0x7f
  0x7fffffffde96: 0x00 0x00

三个看点,每一个都对应本章的一个概念:

  1. x/10c 把每个字节翻译成「码点 + 字形」:49 是字符 '1' 的十进制码点, 97 是 'a' 的56。这就是第 5 节 ASCII 表在调试器里的样子。
  2. NUL 出现了:第 7 个字节 0x00 就是哨兵,字符串的有效内容到此为止56。
  3. 剩下的 3 个字节(0x7f 0x00 0x00)是「垃圾值」——那是数组 aString 分配了 10 字节、而这次输入只占 7 字节留下的旧内容57。内存不会因为你没写过就清零; 这个「垃圾值」现象在后面的栈讨论(第 14 章)还会回来。

把两次 x 并排,本章的对照就齐了:

  输入 123abc,在 0x7fffffffde88 处按整数存:  bc 3a 12 00(小端!)
  输入 123abc,在 0x7fffffffde8e 处按字符串存: 31 32 33 61 62 63 00

  图说:同样六个键,一个被换算成了数(还要按小端倒着放),
  一个被原样记成码点序列。编码决定字节,字节决定含义。

7.5 动手实践:作者布置的练习在练什么

本章的习题不是点缀,每一组都针对一个具体的卡点:

习题组位置练什么
十六进制 ↔ 二进制互转、位数估算2.1 节后把表 2-1 从「查表」练成「条件反射」,这是后面读内存转储的基本功
二进制 ↔ 十进制互转(8 位、16 位)2.2 节后手算转换算法;答案可以拿 gdb 的 x 命令反向验证
设计大小写评级的编码2.2 节后第 3 题亲手做一次「要多少位 + 什么编码」的两个决定
进制转换器程序(0x7b = 123)2.4 节后把 %x/%u 的转换方向摸熟
%u 改 %i 输入 ffffffff 会怎样2.4 节后第 3 题预习第 3 章的有符号数:同一串位,按无符号读是 4294967295,按有符号读是 −1
用 gdb 跟踪 intAndString2.5 节后复现第 7、8、9 节的全部观察

最后一题(换 %i)尤其值得做:它会让你提前撞上「位模式不变、解释规则变、值就变」 这个现象——第 3 章的补码就是把这个现象正式化。

10. 作者的判断与证据

判断一:用「双态开关」而不是「数字机器」开场。 这是刻意的视角选择:先建立物理直觉 (一切都是开关状态),再引入记号(位、十六进制),最后才谈编码(字符、整数)。 证据是本章的组织本身——2.1 开关、2.2 记数法、2.3 存储,一层只解决一层的问题。

判断二:第一个程序选 intAndString 而不是 Hello World。 理由在效果里:它第一次运行时 就逼出「同一输入两种存法」的对照,这个概念靠讲定义讲不出来,必须看见36。

判断三:现在就教调试器。 作者预判读者的反应(「程序这么简单,用不着调试」)并直接回应: 简单例子学调试器效果最好;而且 gdb 是演示内存概念的工具,不靠 bug 也存在价值42。 这是「工具即教学手段」的主张,后面第 9、10、11、14、15 章会反复使用。

判断四(写在章节组织里的):字符先于整数,字符串先于算术。 对照传统教材先讲「数制与算术」的顺序,这本书把 ASCII 和字符串放在无符号整数之前、 把算术整体推到第 3 章。理由从效果上看得出来:程序员日常最先碰到的数据是字符 (第一个程序就和 printf 打交道),而「字符也是编码出来的数」这个认知一旦建立, 「整数也是编码」就顺理成章。教学的顺序迁就认知的顺序,而不是迁就数学的 elegance。

11. 边界与局限

  • 只覆盖 ASCII 子集。 UTF-8 的 1~4 字节变长机制、非 ASCII 字符(比如中文)的编码, 本章只给了入口(码点 U+00B5 用两字节 0xc2b5 存的例子),不展开18。
  • 只讲无符号整数。 负数怎么办——留给第 3 章;小数——留给第 19 章。
  • gdb 会话里的地址(0x7fffffffde88 等)是作者机器上的实际值,你在自己机器上跑, 地址几乎必然不同;原书也提醒了这一点58。这是读所有底层书籍的常识: 地址是会变的,结构是不变的。
  • 「垃圾值」会一直在。 数组没写满的尾巴、栈上局部变量的初值,都是内存里残留的旧内容; 指望它们自动清零是新手最常见的错觉之一,第 14 章讲栈时这条会回来收账。
  • 「字(word)= 4 字节」是本书环境的约定,不是 x86-64 的定律(历史上「字」指 16 位, 今天文档里 word/dword/qword 各指 2/4/8 字节)59。
  • 动手实践的习题(进制转换练习、ASCII 表制作)是本章的重要组成部分,拆解无法替代动手。
  • 「编码」这一章是全书的暗线。 字符是编码(ASCII/UTF-8)、整数是编码(二进制/BCD)、 小数是编码(第 19 章的定点/浮点)、指令也是编码(第 12 章的机器码)—— 第 4 章还会看到,连「逻辑」都能被编码成代数。读后面章节时,不断回来问一句: 「这一串位,按什么规则读?」 能回答这个问题,内存转储在你面前就从天书变回数据。

12. 可带走的

全章主线例子,一行写完: 键盘敲入 123abc → scanf("%x") 把它换算成整数 0x00123abc 存进 anInt(小端字节:bc 3a 12 00)→ scanf("%s") 把它原样存成字符串 31 32 33 61 62 63 00 → printf 再各自换算回字符显示。 同一串键击,两种编码,两套字节;内存里没有「数字」,只有按规则读的位。

  1. 计算机 = 数十亿双态开关;位是开关的记号,十六进制是位的压缩记号;
  2. n 位能表示 2ⁿ 个值;字节 = 8 位,是计算机一次访问的最小单位;
  3. 十进制 → 二进制:反复除 2 取余数,从下往上读;二进制 → 十进制:逐位乘 2 的幂求和;
  4. 内存 = 固定地址 + 可变内容;地址用十六进制从 0 编起;
  5. 存数据前回答两个问题:要多少位、用什么编码;
  6. 字符的码点不等于数字的值:'5' 是 0x35,5 是 0x05;scanf/printf 是两种方向 的转换器;
  7. C 字符串以 NUL(0x00)结尾,看不见的第 N+1 个字节标记结束;
  8. BCD 用 6/16 的编码冗余换「数码一眼可读」——财务系统的选择;
  9. 同一个输入(123abc)按整数存(0x00123abc)和按字符串存(31 32 33 61 62 63 00) 内存里完全不同——「内存里存的是编码,不是事物本身」;
  10. x86-64 是小端序:多字节值的最低字节在最低地址;调试器逐字节查看和跨机器通信时 必须记得这件事;
  11. 「字」= 4 字节是本书环境的约定,读到旧文档里的「字」要警惕它可能指 2 字节; gdb 的 x 命令自己有一套字母(o/x/d/u/t/c/s × b/h/w/g),用之前 h x 查一下。

13. 原文地图

主题原书章原文位置
计算机 = 双态开关第2 章 数据存储格式text/06-ch02.txt:9(搜「双态开关」)
位表示开关第2 章 数据存储格式text/06-ch02.txt:32(搜「二进制数码」)
十六进制与 4 位组、字节第2 章 数据存储格式text/06-ch02.txt:44(搜「十六进制数码」) · text/06-ch02.txt:101(搜「字节」)
0x/0/0b 前缀第2 章 数据存储格式text/06-ch02.txt:105(搜「0x」)
CDC 6000 的 6 位字节第2 章 数据存储格式text/06-ch02.txt:104(搜「6 位」)
位置记数法第2 章 数据存储格式text/06-ch02.txt:147(搜「位置记数法」)
二进制→十进制例子第2 章 数据存储格式text/06-ch02.txt:195(搜「10010101」)
十进制→二进制(除 2 取余)第2 章 数据存储格式text/06-ch02.txt:267(搜「反复除以 2」)
0x55555555 位模式第2 章 数据存储格式text/06-ch02.txt:284(搜「0x55555555」)
RAM/ROM、随机存取真义第2 章 数据存储格式text/06-ch02.txt:316(搜「随机存取存储器」) · text/06-ch02.txt:322(搜「磁带」)
地址固定、内容可变第2 章 数据存储格式text/06-ch02.txt:334(搜「房间号」)
存数据两问第2 章 数据存储格式text/06-ch02.txt:363(搜「多少个不同的值」)
码点与字形、UTF-8/ASCII第2 章 数据存储格式text/06-ch02.txt:390(搜「码点」) · text/06-ch02.txt:394(搜「ASCII」)
ASCII 分组(位 6、位 5)第2 章 数据存储格式text/06-ch02.txt:430(搜「位 6 和位 5」)
NUL 哨兵、Hello World 内存布局第2 章 数据存储格式text/06-ch02.txt:464(搜「哨兵值」) · text/06-ch02.txt:472(搜「0x4004a1」)
Pascal 长度前缀对比第2 章 数据存储格式text/06-ch02.txt:484(搜「Pascal」)
123 的两种存法第2 章 数据存储格式text/06-ch02.txt:499(搜「0x0000007b」)
BCD 与压缩 BCD第2 章 数据存储格式text/06-ch02.txt:501(搜「BCD」) · text/06-ch02.txt:526(搜「压缩 BCD」)
intAndString 程序与输出第2 章 数据存储格式text/06-ch02.txt:610(搜「intAndString.c」) · text/06-ch02.txt:720(搜「1194684」)
C 运行时调用 main、返回 0第2 章 数据存储格式text/06-ch02.txt:646(搜「运行时环境」) · text/06-ch02.txt:664(搜「一切顺利」)
scanf/printf 与 read/write第2 章 数据存储格式text/06-ch02.txt:587(搜「read 系统函数」)
转换说明符表第2 章 数据存储格式text/06-ch02.txt:681(搜「转换说明符」)
& 与数组名第2 章 数据存储格式text/06-ch02.txt:697(搜「取值操作符」)
gdb 命令清单、断点第2 章 数据存储格式text/06-ch02.txt:739(搜「木偶」) · text/06-ch02.txt:740(搜「断点」)
x/4xb 与小端发现第2 章 数据存储格式text/06-ch02.txt:872(搜「x/1dw」) · text/06-ch02.txt:877(搜「0xbc」)
小端/大端定义、网络字节序第2 章 数据存储格式text/06-ch02.txt:927(搜「最低有效字节位于最低编号的地址」) · text/06-ch02.txt:968(搜「网络字节序」)
地址因机器而异第2 章 数据存储格式text/06-ch02.txt:768(搜「可能与本例不同」)

Footnotes

  1. 出处:「第2 章 数据存储格式」第 9 段(text/06-ch02.txt:9,搜「双态开关」)。原文:计算机「不再将其视为程序、文件和图形的集合,而是将其视为数十亿个双态开关和一个或多个能够检测和改变这些开关状态的控制单元」。 ↩

  2. 出处:「第2 章 数据存储格式」第 32 段(text/06-ch02.txt:32,搜「二进制数码」)。原文:「我们使用一个二进制数码(binary digit),通常简称为位(bit),来表示开关的状态。一个位有两个取值——0 和 1」。 ↩

  3. 出处:「第2 章 数据存储格式」第 44 段(text/06-ch02.txt:44,搜「十六进制数码」)。原文:「我们使用十六进制数码(hexadecimal digit)来指定位模式(bit pattern)。十六进制系统的数码共有 16 种取值,分别表示某个 4 位组。」 ↩ ↩2

  4. 出处:「第2 章 数据存储格式」第 98 段(text/06-ch02.txt:98,搜「6c2a」)。原文给出 6c2a16 = 01101100001010102 与 0123abcd16 逐位展开的例子。 ↩

  5. 出处:「第2 章 数据存储格式」第 105 段(text/06-ch02.txt:105,搜「0x」)。原文:「在 C 和 C++编程语言中,在数字前加上 0x(即数字 0 和小写字母 x)表示该数字以十六进制表示,如果在数字前仅加上 0,则表示八进制。」 ↩ ↩2

  6. 出处:「第2 章 数据存储格式」第 101 段(text/06-ch02.txt:101,搜「字节」)与第 104 段(text/06-ch02.txt:104,搜「6 位」)。原文:「单个位对于数据存储通常没什么用处。计算机一次可以访问的最小位数被定义为字节(byte)」;「例如,在 CDC 6000 系列科学大型计算机中,一字节由 6 位组成。」 ↩ ↩2

  7. 出处:「第2 章 数据存储格式」第 82 段(text/06-ch02.txt:82,搜「八进制」)。原文:「八进制系统(基于 8)不太常见,不过你偶尔也会遇到。八进制数码的取值为 0~7,一个数码代表 3 位组。」Unix 文件权限用八进制是补充(不在书里,来自通用知识)。 ↩

  8. 出处:「第2 章 数据存储格式」第 147 段(text/06-ch02.txt:147,搜「位置记数法」)与第 157 段(text/06-ch02.txt:157,搜「最低有效数码」)。原文:「这意味着符号的值取决于其在一组符号中的位置」;并定义最低有效数码与最高有效数码。 ↩ ↩2

  9. 出处:「第2 章 数据存储格式」第 163 段(text/06-ch02.txt:163,搜「罗马数字系统」)与第 172 段(text/06-ch02.txt:172,搜「催生了我们今天所知的数学」)。原文:「位置记数法的发明极大地简化了算术,并催生了我们今天所知的数学。如果你不信,试试用罗马数字系统中的 60(LX)除以 3(III)。(答案:XX。)」 ↩

  10. 出处:「第2 章 数据存储格式」第 175 段(text/06-ch02.txt:175,搜「基数或底数」)与第 184 段(text/06-ch02.txt:184,搜「基数为 2」)。原文:「将数码向左移动一位会使其值扩大 10 倍」可推广到任意基数;二进制「表示数码的只有两个符号」。 ↩

  11. 出处:「第2 章 数据存储格式」第 267 段(text/06-ch02.txt:267,搜「反复除以 2」)。原文:「我们可以通过从右到左反复除以 2,并使用余数作为相应位的值,以此产生一个数的二进制表示。」 ↩ ↩2

  12. 出处:「第2 章 数据存储格式」第 724 段(text/06-ch02.txt:724,搜「数字本身不分二进制」)。原文:「数字本身不分二进制、十进制或十六进制。数字只是一个值而已。任何值都能以这 3 种数制(基数为 2、10、16)等价表示。」 ↩

  13. 出处:「第2 章 数据存储格式」第 316 段(text/06-ch02.txt:316,搜「随机存取存储器」)与第 326 段(text/06-ch02.txt:326,搜「只读存储器」)。 ↩

  14. 出处:「第2 章 数据存储格式」第 322 段(text/06-ch02.txt:322,搜「磁带」)。原文:「不妨将 RAM 与顺序访问存储器(Sequential Access Memory,SAM)作一个对比……你可以将 SAM 看作磁带。」 ↩

  15. 出处:「第2 章 数据存储格式」第 334 段(text/06-ch02.txt:334,搜「房间号」)。原文:「内存中的每个字节都有一个位置或地址,这很像办公楼的房间号。特定字节的地址永远不会改变……但是,特定字节的各个位的状态(内容)(0 或 1)都是可以改变的。」 ↩ ↩2

  16. 出处:「第2 章 数据存储格式」第 363 段(text/06-ch02.txt:363,搜「多少个不同的值」)。原文:「存储数据需要多少位?要回答这个问题,我们需要知道特定数据项允许多少个不同的值」;「数据的编码是什么?……程序员必须决定如何用 0 和 1 编码数据。」 ↩

  17. 出处:「第2 章 数据存储格式」第 493 段(text/06-ch02.txt:493,搜「0~255」)与第 495 段(text/06-ch02.txt:495,搜「4 294 967 295」)。原文给出 1 字节 0255、默认 4 字节 04 294 967 295 的范围。16 位与 64 行的数值按 n 位 2ⁿ 个值(原书第 365 段的规则)推算。 ↩

  18. 出处:「第2 章 数据存储格式」第 390 段(text/06-ch02.txt:390,搜「码点」)。原文:「最常见的字符编码标准是 Unicode UTF-8。它使用 1~4 字节存储代表某个字符的数字,我们将这个数字称为码点(code point)」;「字形是我们在屏幕或纸张上看到的字符外观」。 ↩ ↩2

  19. 出处:「第2 章 数据存储格式」第 394 段(text/06-ch02.txt:394,搜「ASCII」)。原文:「ASCII 使用 7 位指定大小为 128 的字符集合中每个字符的码点」;「我们在编程时只使用 UTF-8 的 ASCII 子集(U+0000 至 U+007F)」。 ↩

  20. 出处:「第2 章 数据存储格式」第 365 段(text/06-ch02.txt:365,搜「n 位能够表示多达 2n」)。原文:「例如,U+00B5 是“微”词头符号(micro sign)的码点,它以 16 位模式 0xc2b5 存储在内存中。」 ↩

  21. 出处:「第2 章 数据存储格式」第 430 段(text/06-ch02.txt:430,搜「位 6 和位 5」)。原文:「字节中的位 6 和位 5(位从右到左编号,以 0 为起始)指定了该字符属于四个分组中的哪一个」:控制字符 / 数字和特殊字符 / 大写字母和特殊字符 / 小写字母和特殊字符。 ↩

  22. 出处:「第2 章 数据存储格式」第 442 段(text/06-ch02.txt:442,搜「man ascii」)与第 446 段(text/06-ch02.txt:446,搜「Joel Spolsky」)。原文:「你可以在 Linux 终端窗口中输入命令 man ascii 来生成与 ASCII 字符一致的码点表」;并推荐 Joel Spolsky 的 Unicode 文章。 ↩ ↩2

  23. 出处:「第2 章 数据存储格式」第 464 段(text/06-ch02.txt:464,搜「哨兵值」)与第 481 段(text/06-ch02.txt:481,搜「LF」)。原文:「C 风格的字符串在末尾使用码点 U+0000(ASCII NUL)作为哨兵值(sentinel value)」;「C 语言使用 U+000A(ASCII LF)作为单个换行符,即便 C 语法需要程序员书写两个字符 \n。」 ↩ ↩2 ↩3

  24. 出处:「第2 章 数据存储格式」第 484 段(text/06-ch02.txt:484,搜「Pascal」)。原文:「在 Pascal 中……字符串的长度由该字符串的第一字节指定,其中的值被视为 8 位无符号整数(这正是 Pascal 的字符串最长只能有 256 个字符的原因)。」 ↩ ↩2

  25. 出处:「第2 章 数据存储格式」第 493 段(text/06-ch02.txt:493,搜「0~255」)与第 495 段(text/06-ch02.txt:495,搜「4 294 967 295」)。原文:「如果只使用一字节,那么无符号整数的取值范围就被限制在了 0~255」;「无符号整数的默认大小是 4 字节,允许的取值范围为 0~4 294 967 295」。 ↩

  26. 出处:「第2 章 数据存储格式」第 499 段(text/06-ch02.txt:499,搜「0x0000007b」)。原文:「十进制数 123 以字符串格式存储为 4 字节(0x31、0x32、0x33、0x00),而按照无符号整数格式,则存储为 0x0000007b。」 ↩

  27. 出处:「第2 章 数据存储格式」第 496 段(text/06-ch02.txt:496,搜「二进制系统的局限之一」)。原文:「二进制系统的局限之一是在执行算术运算之前,你需要将字符串的十进制数转换为二进制数……相反,为了满足现实世界的大部分显示需要,二进制数需要转换为十进制字符表示形式。」 ↩

  28. 出处:「第2 章 数据存储格式」第 501 段(text/06-ch02.txt:501,搜「BCD」)。原文:「BCD(Binary Coded Decimal,二进制编码的十进制)是用于存储整数的另一种编码。每个十进制数码占用 4 位。」 ↩

  29. 出处:「第2 章 数据存储格式」第 522 段(text/06-ch02.txt:522,搜「浪费了 6 种」)。原文:「16 种可能的组合形式只用到了其中 10 种,浪费了 6 种」;16 位 BCD 范围 09 999,二进制 065 535。 ↩

  30. 出处:「第2 章 数据存储格式」第 526 段(text/06-ch02.txt:526,搜「压缩 BCD」)。原文:「COBOL 就是一门商业应用编程语言,支持压缩 BCD 格式(packed BCD format),其中使用 8 位字节保存 2 个数码。最后一个(4 位)数码用于存储数字符号。」 ↩ ↩2

  31. 出处:「第2 章 数据存储格式」第 1 段(text/06-ch02.txt:1,搜「6 数据存储格式」)与第 635 段(text/06-ch02.txt:635,搜「头文件用于提供每个函数的原型」)。原文:「/和/之间的所有内容都是注释。注释仅服务于人类读者,对程序本身没有任何影响」;「头文件用于提供每个函数的原型,后者指定了编译器所需的数据类型。」 ↩ ↩2

  32. 出处:「第2 章 数据存储格式」第 646 段(text/06-ch02.txt:646,搜「运行时环境」)。原文:「当执行 C 程序时,操作系统会先设置 C 运行时环境(C runtime environment)……然后 C 运行时环境调用 main 函数。」 ↩ ↩2

  33. 出处:「第2 章 数据存储格式」第 653 段(text/06-ch02.txt:653,搜「实际参数」)。原文:「调用函数(calling function)可以在调用时提供一系列实际参数(argument,简称"实参")作为被调用函数(called function)的输入。这些输入对应于被调用函数所执行操作中用到的形式参数(parameter,简称"形参")。」 ↩

  34. 出处:「第2 章 数据存储格式」第 668 段(text/06-ch02.txt:668,搜「烹饪配方」)。原文:「语法[10]告诉编译器为 aString 变量分配能容纳 10 个 char 类型的存储空间,这允许我们存储一个长度为 9 个字符的 C 风格字符串(第 10 个字符将是终止字符 NUL)」;把在函数开头定义变量比作「在给出操作说明之前要先列出烹饪配方的配料」。 ↩ ↩2

  35. 出处:「第2 章 数据存储格式」第 550 段(text/06-ch02.txt:550,搜「过程式编程语言」)。原文:「C 语言不支持面向对象的范式,它是一种过程式编程语言。C 程序被划分为多个函数。」 ↩

  36. 出处:「第2 章 数据存储格式」第 610 段(text/06-ch02.txt:610,搜「intAndString.c」)与第 720 段(text/06-ch02.txt:720,搜「1194684」)。原文运行输出:「The integer is 1194684 and the string is 123abc」。 ↩ ↩2

  37. 出处:「第2 章 数据存储格式」第 664 段(text/06-ch02.txt:664,搜「一切顺利」)。原文:「main 函数向 C 运行时环境返回整数 0,后者将该值传给操作系统。值 0 告诉操作系统一切顺利。」 ↩

  38. 出处:「第2 章 数据存储格式」第 587 段(text/06-ch02.txt:587,搜「read 系统函数」)。原文:「scanf 库函数首先调用 read 系统函数(操作系统提供的一个函数)从键盘读取」;「printf 库函数将 int 类型转换为相应的 char 类型字符串,并调用 write 系统函数将每个字符写入屏幕。」 ↩

  39. 出处:「第2 章 数据存储格式」第 681 段(text/06-ch02.txt:681,搜「转换说明符」)。原书表 2-9 列出 %u 无符号十进制、%d/%i 有符号十进制、%f 浮点、%x 十六进制、%s 字符串。 ↩

  40. 出处:「第2 章 数据存储格式」第 697 段(text/06-ch02.txt:697,搜「取值操作符」)。原文:「我们通过对变量使用取值操作符(&anInt),告诉 scanf 将输入的整数存储在哪里。将数组名传入函数时,C 会直接使用该数组的地址,所以在调用 scanf 从键盘读取文本时不需要使用 & 操作符。」 ↩

  41. 出处:「第2 章 数据存储格式」第 700 段(text/06-ch02.txt:700,搜「严格匹配」)。原文:「除了这些转换说明符,scanf 格式字符串中出现的其他字符必须与键盘输入严格匹配。」例子输入为「1 123 and 2 456」。 ↩

  42. 出处:「第2 章 数据存储格式」第 743 段(text/06-ch02.txt:743,搜「为时过早」)。原文:「如果你觉得现在就谈论调试器似乎为时过早……我保证,对于学习使用调试器这件事,简单的例子绝对比复杂的例子效果好得多。」 ↩ ↩2

  43. 出处:「第2 章 数据存储格式」第 739 段(text/06-ch02.txt:739,搜「木偶」)。原文:「在使用调试器时,你有点像是一位木偶师,所调试的程序就是被精心控制的木偶。你的主要控制手段是断点。」 ↩

  44. 出处:「第2 章 数据存储格式」第 740 段(text/06-ch02.txt:740,搜「断点」)至第 761 段(text/06-ch02.txt:761,搜「memory_address」)。命令清单见原书 2.5 节列表。 ↩

  45. 出处:「第2 章 数据存储格式」第 773 段(text/06-ch02.txt:773,搜「-g 选项」)。原文:「-g 选项告诉编译器在可执行文件中加入调试器信息。-Wall 选项告诉编译器对语法正确但可能与你的意图不符的代码发出警告。」 ↩

  46. 出处:「第2 章 数据存储格式」第 808 段(text/06-ch02.txt:808,搜「我喜欢将断点设置在」)。原文:「我喜欢将断点设置在一个函数调用另一个函数的地方,以便在将参数传给被调用函数之前先检查参数的值。」 ↩

  47. 出处:「第2 章 数据存储格式」第 851 段(text/06-ch02.txt:851,搜「Examine memory」)。原文引用 gdb 帮助:「FMT is a repeat count followed by a format letter and a size letter. Format letters are o(octal), x(hex), d(decimal), u(unsigned decimal), t(binary), f(float), a(address), i(instruction), c(char) and s(string). Size letters are b(byte), h(halfword), w(word), g(giant, 8 bytes).」 ↩

  48. 出处:「第2 章 数据存储格式」第 837 段(text/06-ch02.txt:837,搜「八进制数码」)。原文:「如果没有对应的字符字形,则显示码点(\之后跟 3 个八进制数码,参见表 2-2)。例如,因为 NUL 没有字符字形,所以 gdb 在字符串末尾显示\000。」 ↩

  49. 出处:「第2 章 数据存储格式」第 927 段(text/06-ch02.txt:927,搜「最低有效字节位于最低编号的地址」)。原文:「小端(little-endian):对于存储在内存中的数据,多字节值中的最低有效字节位于最低编号的地址。」 ↩

  50. 出处:「第2 章 数据存储格式」第 943 段(text/06-ch02.txt:943,搜「大端」)与第 949 段(text/06-ch02.txt:949,搜「PowerPC」)。原文:「大端(big-endian):……最高有效字节位于最低编号的地址」;大端例子来自「采用了大端存储(比如 PowerPC 架构)的计算机」。 ↩

  51. 出处:「第2 章 数据存储格式」第 968 段(text/06-ch02.txt:968,搜「网络字节序」)。原文:「传输控制协议/互联网协议(TCP/IP)使用大端序,有时也称为网络字节序。x86-64 架构使用的是小端序。操作系统会为网络通信调整字节序。」 ↩ ↩2

  52. 出处:「第2 章 数据存储格式」第 938 段(text/06-ch02.txt:938,搜「存储反了」)与第 939 段(text/06-ch02.txt:939,搜「正确的顺序重新安排显示字节」)。原文:「猛一看,值的顺序似乎存储反了,因为最低有效字节(“小端”)最先存储在内存中」「当我们命令 gdb 显示整个 4 字节值时,gdb 知道当前是小端序环境,会以正确的顺序重新安排显示字节」;大端对照输出见第 955 段(text/06-ch02.txt:955,搜「BIG-ENDIAN」)。 ↩ ↩2

  53. 出处:「第2 章 数据存储格式」第 575 段(text/06-ch02.txt:575,搜「cin」)。原文 C++ 对照例:cin >> x;、cout << x;。转换规则由 x 的类型(int)决定,与 C 的显式格式串相对照。 ↩

  54. 出处:「第2 章 数据存储格式」第 594 段(text/06-ch02.txt:594,搜「直接调用」)。原文:「应用程序可以直接调用 read 和 write 函数来读写字符。这个话题留待第 16 章讨论,届时我们将编写自己的转换函数。」中文版章节号有标注重排,write/read 的正文实际在第 11 章。 ↩

  55. 出处:「第2 章 数据存储格式」第 601 段(text/06-ch02.txt:601,搜「强烈建议你学习如何使用 make」)。原文:「如果你尚不熟悉 GNU make 程序,强烈建议你学习如何使用 make 来构建程序。」 ↩

  56. 出处:「第2 章 数据存储格式」第 897 段(text/06-ch02.txt:897,搜「49 '1'」)。原文 x/10c 输出「49 '1' 50 '2' 51 '3' 97 'a' 98 'b' 99 'c' 0 '\000'」;第 906 段(text/06-ch02.txt:906,搜「标记了 6 字符长的字符串的结尾」):「NUL 字符都标记了 6 字符长的字符串的结尾。」 ↩ ↩2

  57. 出处:「第2 章 数据存储格式」第 908 段(text/06-ch02.txt:908,搜「垃圾值」)。原文:「剩下的 3 字节都是与字符串无关的随机值,通常称为垃圾值。」 ↩

  58. 出处:「第2 章 数据存储格式」第 768 段(text/06-ch02.txt:768,搜「可能与本例不同」)。原文:「注意,你的计算机上显示的地址可能与本例不同。」 ↩

  59. 出处:「第2 章 数据存储格式」第 879 段(text/06-ch02.txt:879,搜「依赖于所使用的计算机环境」)。原文:「字(word)的大小依赖于所使用的计算机环境。在我们的环境中,一个字是 4 字节。」 ↩

On this page