c++程序从main.cpp到输出hello, world!必须经过预处理、编译、汇编、链接四个不可跳过的阶段;g++ hello.cpp -o hello一键执行,实则依次调用cpp、cc1plus、as、ld完成宏展开、语法检查、汇编翻译、符号解析与地址重定位。

直接说结论:C++程序从写完 main.cpp 到屏幕上打出 Hello, World!,必须经过预处理、编译、汇编、链接这四个不可跳过的阶段,缺一不可;用 g++ hello.cpp -o hello 一键完成,只是把这四步“打包”执行了,背后每一步都在干不同的事。
预处理阶段:头文件和宏怎么“消失又出现”的
预处理器(比如 cpp)只做文本替换,不检查语法,也不认识 C++ 是什么语言。它拿到 hello.cpp 后,会:
- 把所有
#include <iostream></iostream>展开成几千行实际头文件内容(你打开.i文件就能看到) - 把
#define PI 3.14替换成字面量,但不会做类型校验——SQUARE(x) x*x这种宏展开后可能算错,就是这阶段埋的坑 - 删掉所有
//和/* */注释,连带行号信息一起塞进输出里,方便后续报错时定位 - 处理
#ifdef DEBUG块:如果没定义DEBUG,对应代码段直接被剔除,不会进入编译阶段
命令:g++ -E hello.cpp -o hello.i。生成的 hello.i 是纯文本,可直接用 less 查看——这是你第一次看到“膨胀”后的完整代码视图。
编译阶段:语法错误和类型错误在这里暴露
这一步才是真正的“翻译”,编译器(如 cc1plus)把 hello.i 转成人类勉强能读的汇编代码(.s 文件)。它会:
- 做词法分析(拆出
std::cout、、字符串字面量等 token) - 构建抽象语法树(AST),检查
std::cout 是否合法,但此时还不知道 <code>std::cout在哪定义 - 发现
int x; std::cout 中未声明的 <code>y,立刻报错‘y’ was not declared in this scope - 对常量表达式做折叠,比如
int a = 2 + 3 * 4;直接变成int a = 14;
命令:g++ -S hello.i -o hello.s。如果这步失败,说明代码有硬伤,得改源码;成功则说明语法/语义过关,可以继续往下走。
汇编阶段:把汇编指令变成机器码
汇编器(如 as)不管 C++、不认 std::,只认 mov、call、ret 这类指令。它把 hello.s 翻译成二进制目标文件 hello.o,特点是:
-
hello.o是二进制格式,不能cat查看,但可用objdump -d hello.o反汇编验证 - 里面包含符号表(symbol table):记录了
main函数起始地址是“暂时未知”,std::cout是“未定义”,留待链接阶段解决 - 每个函数、全局变量都只有相对地址(offset),没有真实内存位置——所以单独的
.o文件无法运行
命令:g++ -c hello.s -o hello.o。注意:这个 -c 参数很关键,它告诉编译器“停在目标文件,别链接”。漏掉它,g++ 就直接走完全部流程了。
链接阶段:多个 .o 怎么拼成一个可执行文件
链接器(如 ld)负责“拼图”:把你的 hello.o、标准库的 libstdc++.a、C 运行时的 crt0.o 等全拉进来,解决“谁在哪”的问题。它会:
- 解析未定义符号:发现
std::cout和std::endl在libstdc++.so里,就把对应机器码拷进来(静态链接)或打个标记(动态链接) - 重定位地址:把
call printf@PLT中的占位地址,替换成libc.so加载后真实的printf入口地址 - 合并段(section):把所有
.text段叠在一起,所有.data段叠在一起,形成最终的hello文件 - 如果用了
add.cpp里的函数但没编译进目标文件,就会报undefined reference to 'add(int, int)'
命令:g++ hello.o -o hello。这步失败几乎全是“找不到东西”,不是代码写错了,而是构建逻辑断了——比如忘了加 -lpthread,或者头文件写了声明却没提供对应 .o。
真正容易被忽略的是:预处理和链接两个阶段都依赖外部输入(头文件路径、库路径、宏定义),而它们不报“语法错误”,只报“找不到”或“重复定义”,排查时容易绕远路。盯住 .i 和链接命令本身,比反复改 main.cpp 更有效。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











