这一章在干嘛?

回答两个根本问题:C 程序在什么「环境」里从文本变成进程?编译器按什么「规则」把字符切成 token?搞懂这两件事,很多玄学错误(多打个分号、注释吞代码)就不再是玄学。

2.1 环境:翻译与执行

2.2 词法规则:字符、注释与标识符

2.3 程序风格:写给三个月后的自己

2.1 环境:翻译与执行

翻译环境负责把源文件变成可执行程序,执行环境负责运行它。两套环境不必是同一台机器(交叉编译就是这么来的:在 PC 上编译,在单片机上执行)。

翻译的独立单位是翻译单元:一个 .c 文件 + 它递归包含的所有头文件。每个翻译单元独立编译成目标文件,再由链接器缝合。链接的本质是符号匹配:本单元引用的外部符号(extern 函数/变量)必须在别的目标文件里恰好有一份定义。

执行模型速写:

程序装载后从 main 开始(启动代码先把环境收拾好),main 返回值交给宿主环境表示成功/失败。局部变量住在栈上,static 变量和字符串常量住在静态区,malloc 的内存在堆上——这张地图第 11、18 章会展开。

2.2 词法规则:字符、注释与标识符

编译器看你的代码,第一步是字符集映射(换行符等统一化),第二步是切词(tokenize)。几条硬规则:

  • 注释/* ... */ 不允许嵌套(编译器匹配到第一个 */ 就收工);// 行注释到行尾。注释在预处理阶段就被替换成一个空格——它不是字符串里也能随便写的东西;
  • 标识符:字母/数字/下划线组成,数字不能打头;大小写敏感,countCount 是两个东西;
  • 自由形式:语句以分号结束,空格换行只用来分隔 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,不写裸数字改一处生效全局,还能顺手起个自解释的名字

一句话收束本章:

编译器按规则切词、按单元编译、按符号链接。理解规则本身,比死记错误信息有效得多。