这一章在干嘛?
回答两个根本问题:C 程序在什么「环境」里从文本变成进程?编译器按什么「规则」把字符切成 token?搞懂这两件事,很多玄学错误(多打个分号、注释吞代码)就不再是玄学。
2.1 环境:翻译与执行
翻译环境负责把源文件变成可执行程序,执行环境负责运行它。两套环境不必是同一台机器(交叉编译就是这么来的:在 PC 上编译,在单片机上执行)。
翻译的独立单位是翻译单元:一个 .c 文件 + 它递归包含的所有头文件。每个翻译单元独立编译成目标文件,再由链接器缝合。链接的本质是符号匹配:本单元引用的外部符号(extern 函数/变量)必须在别的目标文件里恰好有一份定义。
执行模型速写:
程序装载后从 main 开始(启动代码先把环境收拾好),main 返回值交给宿主环境表示成功/失败。局部变量住在栈上,static 变量和字符串常量住在静态区,malloc 的内存在堆上——这张地图第 11、18 章会展开。
2.2 词法规则:字符、注释与标识符
编译器看你的代码,第一步是字符集映射(换行符等统一化),第二步是切词(tokenize)。几条硬规则:
- 注释:
/* ... */不允许嵌套(编译器匹配到第一个*/就收工);//行注释到行尾。注释在预处理阶段就被替换成一个空格——它不是字符串里也能随便写的东西; - 标识符:字母/数字/下划线组成,数字不能打头;大小写敏感,
count和Count是两个东西; - 自由形式:语句以分号结束,空格换行只用来分隔 token——于是一个分号写三行也行,一行塞三个分号也行,但风格上别这么干;
- 贪心切词:
a+++b会被切成a ++ + b而不是a + ++b——编译器总是尽量读长的 token。
int x = 1;
int y = x/*ptr; /* 想写 x / (*ptr)?糟糕! */
/* 上面这行 /* 之后全是注释:x/*p 被当成 x 和一个注释的开始,
直到找到下一个 */ 才结束——经典的注释陷阱 */
警惕:
表达式里写
x/*p,除号会被「注释开始」吞掉。写成x / (*p)加上空格和括号,既安全又好读。
2.3 程序风格:写给三个月后的自己
风格不影响编译结果,但影响维护成本。本书的几条约定值得直接抄:
| 约定 | 理由 |
|---|---|
| 一行一条语句,else 单独成行 | 一眼看清控制流,diff 工具也友好 |
| 函数/变量名用小写+下划线或驼峰,宏全大写 | 看到 ALL_CAPS 就知道这是编译期常量 |
| 指针变量名带 p/ptr 后缀或 ptr_ 前缀 | 提醒使用者「这是地址不是值」,解引用前先三思 |
| 常量一律用 define 或 const,不写裸数字 | 改一处生效全局,还能顺手起个自解释的名字 |
一句话收束本章:
编译器按规则切词、按单元编译、按符号链接。理解规则本身,比死记错误信息有效得多。
1. 翻译单元是什么?它和源文件是什么关系?
一个 .c 文件加上它递归 include 的所有头文件,展开后构成一个翻译单元,独立编译成一个目标文件。链接器再把多个目标文件按符号拼接成可执行程序。
2. 为什么 /* */ 注释不能嵌套?
编译器只匹配到第一个 / 就认为注释结束。想注释掉一段本身含 / */ 的代码时会提前截断,剩余文本被当普通代码处理,产生一堆莫名错误。可用 // 或条件编译 if 0 替代。
3. a+++b 会被怎样切词?
按贪心策略切成 a ++ + b,即 (a++)+b,而不是 a+(++b)。切词只看 token 边界,与你的意图无关——所以别写这种需要人肉分析优先级的表达式。