87 字节的源文件,预处理后变成 102 万字节——中间发生了什么?
.cpp 到可执行文件要经过预处理 → 编译 → 汇编 → 链接四步,每步的产物是什么;g++ -E / -S / -c 把编译停在任意一步,亲眼看到中间产物;.o 里那 5 个"还没人实现的符号",你就懂了。hello.cpp 逐步编译,逐个产物量出来的。不是示意图里的假数据。点「单步 ▶」逐步推进,看每一步敲什么命令、产出什么文件、文件有多大、内容变成什么样。 每步的数字都是本机真实量出来的。
同样一段代码,四个阶段的产物大小差得离谱。注意横轴是对数刻度——
因为线性画的话,除了 .ii 其他几根都看不见。
#include <iostream> 把整个标准库头文件原样复制进来了。
你以为只写了一行 include,实际编译器要读一百万字节的文本。这就是为什么"头文件别乱 include"。
main 那几行。
std::cout 的实现等一并塞进来了。所以最小的 exe 也有 100 KB 出头。很多人以为"编译第一步就会告诉我语法错了"。其实预处理只做文本替换, 它连 C++ 语法都不看。我写了个满是语法错误的文件实测:
-E 面对三处语法错误照样成功,
还老老实实产出了 100 万字节的 .ii。语法错误是在第二步「编译」才被发现的。
#include 写错文件名会在预处理阶段报错(找不到文件是文本层面的事),
而变量名写错要到编译阶段才报(那是语法/语义层面的事)。hello.o 已经是机器码了,为什么不能直接双击运行?
用 nm -C 看看它里面缺什么:
std::cout << "Hello" 这一行,
背后要调用标准库的 5 个东西,而它们的实现不在你的 .o 里,在 C++ 运行时库里。
链接这一步干的就是这件事:把散落在各个 .o 和库文件里的实现,按符号名对上号、拼成一个完整可执行文件。
hello.exe;对不上 → 就是你在 92.2 里见过的
undefined reference(找不到实现)和 multiple definition(找到两份实现)。.LC0: .ascii "Hello\0";汇编成机器码后,
字符串就以 48 65 6c 6c 6f 00 这串字节躺在 .rdata(只读数据段)里。
源码里的每个字面量,最后都变成了内存里的一段字节——这是 2.3 节"计算机里数是怎么存的"的延续。| 步骤 | 命令 | 产物 | 大小 | 文件头 | 还是文本吗 |
|---|---|---|---|---|---|
| 0 源码 | — | hello.cpp | 87 B | #include | ✅ 文本 |
| 1 预处理 | g++ -E | hello.ii | 1,025,036 B | # 0 "hel | ✅ 文本(仍是 C++) |
| 2 编译 | g++ -S | hello.s | 1,388 B | ⇥.file " | ✅ 文本(汇编) |
| 3 汇编 | g++ -c | hello.o | 1,647 B | 64 86(COFF) | ❌ 二进制 |
| 4 链接 | g++ hello.o | hello.exe | 129,034 B | 4d 5a(MZ/PE) | ❌ 二进制 |
MZ 开头才是 Windows 可执行文件;64 86(COFF)是目标文件,双击跑不起来;
前三个都还是能用记事本打开的文本。
7f 45 4c 46(.ELF),目标文件也是 ELF 但类型不同。-c 出来的 .o 能直接运行:不能。
它的文件头是 64 86(COFF)而不是 MZ,而且里面还挂着 5 个未定义符号。
.o 是半成品,必须经过链接。./hello 报"找不到文件":因为链接产物叫
hello.exe,磁盘上根本不存在名为 hello 的文件(本机实测目录里只有 hello.exe)。
要写 .\hello.exe。Linux 下产物就叫 hello,所以要写 ./hello——照抄教程常在这里翻车。-E 照样通过)。
语法错误在第二步编译才报。所以看到 error: expected ';' 这类,
说明已经过了预处理、正在编译——报错行号指的是展开后的位置,有时和你想的不一样。#include 是"把整个文件原样贴进来",
所以头文件里写变量定义会导致重复定义(贴几次就有几份)——这就是 26.2 和 92.2 的 E2 要讲的事。-S = 编译到汇编为止,产出 .s(文本,能用记事本打开,本机 1388 字节)。-E 只到预处理(.ii);-c 会多做一步汇编、产出二进制 .o;D 是四步全做完。hello.o(1647 字节)为什么不能直接运行?nm -C hello.o 列出 5 个 U(未定义)符号,包括 std::cout、__main——这些实现都在 C++ 运行时库里,得靠链接补上。而且它的文件头是 64 86(COFF 目标文件),不是 MZ(PE 可执行)。D 说反了:.o 是二进制,.ii/.s 才是文本。.cpp,只跑 g++ -E(预处理),结果是?-E 退出码 0,正常产出 1,025,051 字节的 bad.ii,一个错都不报。预处理只做文本替换(展开 include、替换宏、按 #ifdef 裁剪),它根本不解析 C++ 语法。语法错误要到第二步「编译」才报。Q4. 你的 hello.cpp 只有 87 字节,#include <iostream> 之后预处理产物
hello.ii 却有 1,025,036 字节。多出来的一百万字节是什么?
由此说明"头文件"在使用上应该注意什么?
<iostream> 以及它层层 include 的其他头文件的全部内容——
标准库把 cout、ostream、string 等一整套声明都写在里头,
预处理把它们原样复制到你的 .ii 里(本机实测:展开后 #include 剩余 0 行,证明全被替换成了正文)。cout 才要 <iostream>,
只用 vector 就 include <vector>);
② 头文件只放声明、不放定义——因为每个 include 它的 .cpp 都会得到一份副本,
放定义就会在链接期撞上 multiple definition(92.2 的 E2);
③ 编译慢很多时候就是 include 太多,这也是"预编译头 / 模块"想解决的问题。
Q5. 下面是 hello.s 里 main 的真实汇编片段(本机 g++ 15.2 输出):
.LC0 里放的是什么?② _ZStlsISt11char_traitsIcEE...
这一长串是什么,为什么长这样?③ call 的那几个东西,在 .o 里是什么状态?
.LC0 是编译器给字符串字面量起的标号,里面就是 .ascii "Hello\0"
(本机在 .s 里实测到这行)。汇编成机器码后,它以字节 48 65 6c 6c 6f 00 躺在 .rdata 段。
leaq .LC0(%rip), %rdx 就是"把这个字符串的地址装进 rdx 寄存器",准备当参数传出去。std::operator<<<std::char_traits<char>>(std::basic_ostream<char,...>&, char const*)
——也就是你写的 cout << "Hello" 实际调用的函数。
因为 C++ 支持重载和命名空间,同一个函数名可能对应多个不同版本,
编译器必须把参数类型、命名空间全编码进符号名才能唯一区分,所以名字这么长。
nm -C 的 -C 就是把它还原成人能读的样子(demangle)。.o 里它们是 U(Undefined,未定义)状态——本机 nm -C hello.o 实测有 5 个 U 符号,
包括 __main、std::cout 和上面那个 operator<<。
实现都在 C++ 运行时库里,要等链接阶段才补上。补不上就是你熟悉的 undefined reference。
Q6. 同事的工程有两个现象,请用四步模型分别解释根因:
现象一:只改了一个 .cpp,构建系统却把全部 200 个文件重编一遍,每次要 10 分钟。
现象二:代码在 Linux 上编译运行都正常,换到 Windows(MinGW) 上,编译也过了,
但敲 ./app 报"系统找不到指定的文件"。
.ii。
如果有个"万能头"(比如 common.h)被 200 个 .cpp 都 include,
那么改这个头文件 = 200 个 .cpp 的预处理产物全变 → 全部要重编。g++ -M main.cpp 或 g++ -MM 列出真实依赖,看是谁被过度依赖;
② 拆分大头文件,别搞"万能头";③ 能在头文件里用前置声明(class Foo;)就别 include;
④ 把实现从头文件挪到 .cpp(模板除外,模板必须在头文件,见 30.4);
⑤ 构建系统要用 -MD 生成依赖文件,否则连"该重编谁"都算不准。app.exe,磁盘上不存在名为 app 的文件(本机实测:目录里只有 hello.exe),
所以 ./app 当然找不到。Linux 下产物就叫 app,写 ./app 才对。.\app.exe(或 app.exe);
Makefile 里的目标名也要跟着平台走——目标名必须等于真实产物名,
写成 app: 却产出 app.exe,会让 make 每次都以为目标没生成、从而永远重编(这个坑本机也实测复现过,见 92.1)。
.cpp 独立走完前三步,各自产出一个 .o(预制构件);
最后链接把它们组装起来。改了一个 .cpp,只需重编它一个 .o,再重新链接——
这就是 Makefile / CMake 存在的意义(26.4 / 26.5),也是为什么"编译 200 个文件的大工程"改一行不用等 10 分钟。
.cpp 的某一行),
和构件之间对不上(链接期,说某个符号找不到或找到两份)。
分清这两类,是排错的第一课——92.2《编译链接错误对照表》专门讲这个。