平时我们在终端里执行一条 g++ 命令,短短一秒钟就能把 .cpp 源代码变成可以直接运行的 .exe 程序。那么在这条命令背后,计算机到底经历了哪几个步骤呢?
由于计算机底层只认识由 0 和 1 组成的机器指令,我们编写的 hello.cpp 只是一份纯文本文件。它必须先经过 “编译” 生成目标文件,再经过 “链接” 组装成可执行程序,最后才能真正运行起来,整体流程如下图所示。

C++ 编译与运行的 4 个步骤
前面配置本地开发环境时,我们已经创建过一份 hello.cpp 代码。为了看清编译命令背后到底发生了什么,下面我们以它为例,把整个加工过程拆成 4 个步骤来一步步看看。
1. 编写源文件(.cpp)
我们亲手创建并保存的 hello.cpp 就是源文件,里面的代码如下:
#include <iostream>
int main() {
std::cout << "Hello, C++!";
return 0;
}源文件里面保存的是纯文本格式的 C++ 代码,不管是使用 VSCode,还是使用系统自带的记事本,都能直接打开查看。需要注意的是,此时的 hello.cpp 仅仅是一份代码草稿,计算机还不能直接运行它。
2. 编译生成目标文件(.o)
写好源文件后,第一步是交给编译器处理。编译器会逐行检查代码的语法和数据类型是否正确。如果检查通过,就会把我们编写的源代码翻译成二进制格式的 “目标文件”。
在 VSCode 终端中,我们可以加上 -c 参数,表示 “只编译、不链接”,单独执行编译这一步。这里的 “c” 就是 “compile(编译)” 的首字母:
g++ -std=c++20 -c hello.cpp -o hello.o执行完上面这条命令后,我们可以看到左侧文件列表里除了原本的 hello.cpp 之外,还多出了一个 hello.o 文件。
这个 hello.o 就是编译生成的目标文件。使用 GCC 编译器时它的后缀是 .o,使用 Visual Studio 的 MSVC 编译器时后缀则是 .obj。它里面存放的已经不再是文本代码,而是机器指令片段,但此时还不能独立运行。
提示 严格来说,编译阶段内部还可以细分为 “预处理”、“编译” 和 “汇编” 3 个小步骤,比如把 #include 引入的头文件内容展开就是在预处理阶段完成的。初学阶段我们先不用深究这些细分过程,抓住 “源文件 → 编译 → 目标文件” 这条主线即可。
3. 链接生成可执行程序(.exe)
为什么 hello.o 已经是机器指令了,却还不能直接运行呢?
回头看 hello.cpp 的代码:我们自己只写了 main() 函数,却直接调用了标准库的 std::cout 来打印文字。编译生成的 hello.o 只是把这几行代码变成了半成品零件,而 std::cout 本身的实现代码存放在 C++ 标准库中,并不在 hello.o 里面。
因此,接下来需要由 “链接器” 去 C++ 标准库里找出 std::cout 的现成实现代码,和目标文件 hello.o 组装到一起,打包成完整的程序。在终端中执行下面这条命令即可完成链接:
g++ hello.o -o hello.exe链接成功之后,文件夹里就会生成最终的 hello.exe。在 Windows 系统中,这就是一个完整的可执行程序了。而在 macOS 和 Linux 系统中,生成的可执行程序通常不带 .exe 后缀,但 “先编译、再链接” 的生成过程是完全一致的。
4. 运行可执行程序
得到了可执行程序 hello.exe 之后,我们才真正进入 “运行” 阶段。在终端中输入以下命令并按下 Enter 键:
.\hello.exe运行结果如下。
Hello, C++!执行 .\hello.exe 时,操作系统会先把程序加载到内存中,然后自动找到程序入口 main() 函数,从上往下执行花括号内部的代码,最终在屏幕上打印出 “Hello, C++!”。
把上面这 4 个步骤串起来,整个 C++ 程序的生成与运行流程如下:
源文件(.cpp) → 编译 → 目标文件(.o) → 链接 → 可执行程序(.exe) → 运行
日常编译的注意事项
在实际编写和调试代码时,有两个常见的编译细节需要小伙伴们特别留意。
1. 一步完成编译与链接
既然生成程序需要先编译成 .o 文件、再链接成 .exe 文件,那为什么平时我们只敲一条 g++ 命令就搞定了呢?
其实,g++ 是一个总控工具。当我们像平时那样直接执行下面这条命令时:
g++ -std=c++20 hello.cpp -o hello.exeg++ 会在后台自动把 “编译” 和 “链接” 两步连续做完,并在生成最终的 hello.exe 之后,自动把中间临时产生的目标文件清理掉。也就是说,平时只写一条命令,并不是省掉了链接阶段,而是 g++ 在背后自动完成了。
前面加上 -c 参数把两步拆开,只是为了带大家看清背后的真实过程。平时编译单文件程序直接执行上面这一条命令即可,刚才实验留下的 hello.o 文件直接删掉也不会有任何影响。
2. 修改代码后重新编译
很多初学的小伙伴在写代码时常会遇到一个奇怪的现象:明明修改了 .cpp 文件里的代码,为什么运行出来的结果还是老样子?
这是因为 hello.cpp 是我们编写的源代码草稿,而 hello.exe 是根据上一次的代码加工出来的成品,它们是两个独立的文件。我们可以按照以下 4 步做一个小实验,亲手验证一下:
- 打开
hello.cpp,把双引号里的"Hello, C++!"修改为"Hello, lvyenet!",然后按下快捷键(Ctrl + S)保存文件。 - 此时先不要执行编译命令,直接在终端里运行原来的
.\hello.exe,可以看到屏幕上输出的依然是旧的 “Hello, C++!”。 - 在终端中重新执行编译命令
g++ -std=c++20 hello.cpp -o hello.exe,重新生成一次hello.exe。 - 再次运行
.\hello.exe,这一次屏幕上才会显示出新的 “Hello, lvyenet!”。
这个实验说明,修改源代码并不会自动更新已经生成的 .exe 程序。日常开发中常把编译加链接生成程序的过程统称为 “构建(Build)”。平时只需要记住一句话:只要修改了源代码,就必须先保存并重新编译,然后再运行才能看到最新效果。
编译错误与链接错误
平时写 C++ 代码遇到报错时,错误信息通常可以分为 “编译错误” 和 “链接错误” 两大类,它们分别对应前面的两个加工阶段。
1. 编译错误
编译错误发生在 “源文件 → 目标文件” 这一步。只要代码违反了 C++ 的语法规则,比如漏写了分号、括号没闭合,或者把标准库里的名字拼错了,编译器在检查时就会直接报错,此时连目标文件(.o)都生成不出来。下面我们来看一个例子。
示例 1:拼写错误导致编译报错
#include <iostream>
int main() {
std::cot << "Hello, C++!"; // 误将 cout 写成 cot
return 0;
}运行结果如下。
(报错)hello.cpp:4:10: error: 'cot' is not a member of 'std'; did you mean 'cout'?分析:
在这个例子中,我们不小心把 std::cout 拼错写成了 std::cot。在编译检查阶段,编译器发现标准库 std 里面根本没有 cot 这个名字,于是直接停止编译并报错。
报错信息里的英文提示意思是 “cot 不是 std 的成员,你想写的是不是 cout?”,并且开头直接标出了具体的文件名与行列位置 hello.cpp:4:10。遇到这类指出具体行号的编译报错,我们直接回到对应行检查拼写或标点符号就可以了。
2. 链接错误
链接错误发生在 “目标文件 → 可执行程序” 这一步。此时代码语法没有问题,已经顺利通过了编译,但在组装程序时,链接器找不到某个被调用函数的具体实现,或者找不到程序入口 main(),就会报出 undefined reference(未定义的引用)错误。
举个初学者很常见的例子:不小心把程序入口 main() 拼错写成了 mian()。请看下面例子。
示例 2:入口写错导致链接报错
#include <iostream>
int mian() { // 误将 main 写成 mian
std::cout << "Hello, C++!";
return 0;
}运行结果如下。
(报错)undefined reference to `WinMain'
collect2.exe: error: ld returned 1 exit status分析:
对比示例 1 可以发现,把 main() 误写成 mian() 之后,这段代码在编译阶段是不会报错的。因为编译器认为我们只是自己定义了一个名叫 mian 的普通函数,单看语法完全合法。
可能有些小伙伴会觉得奇怪:“明明写错的是 main,为什么报错却提示找不到 WinMain 呢?” 这是因为在 Windows 系统下,链接器找不到控制台程序入口 main() 时,会继续去寻找窗口程序入口 WinMain。结果两个入口都没找到,最终就报出了 WinMain 未定义的错误。
报错第二行里的 ld 指的就是 GCC 的链接器程序,返回状态码 1 代表链接失败并退出。除了把 main() 的名字写错之外,以后我们自己编写多个函数时,如果只写了函数声明、却漏写了函数的具体实现代码,同样会在链接阶段触发 undefined reference 报错。
通过上面这两个例子,我们可以总结出以下 2 条实用的报错排查经验:
- 报错指出具体文件名与行号:说明卡在了编译阶段,优先回到对应行检查拼写、分号或括号等语法细节。
- 报错提示 undefined reference:说明语法检查已通过,但卡在了链接阶段,优先检查
main()入口名是否写错,或者被调用的函数是否缺少具体实现。
