Skip to content

Stage07|Modern C++ Foundation ​

Lesson081|从 .cpp 到可执行文件:预处理、编译与链接 ​

Tags: #C++17 #Compiler #Linker #BuildDifficulty: ⭐⭐⭐☆☆


一、本课从一个错误开始 ​

你在头文件中声明了函数,IDE 也能跳转,编译却在最后失败:

text
undefined reference to `add(int, int)`

这不是语法错误,而是链接器没有在参与链接的目标文件中找到定义。要读懂 C++ 工程错误,必须先把“编译”拆成四步。

二、本课目标 ​

完成本课后,你应该能够:

  1. 区分预处理、编译、汇编和链接。
  2. 解释声明、定义、翻译单元和目标文件。
  3. 分辨编译错误与链接错误。
  4. 解释头文件保护和 ODR 的作用。
  5. 用命令行验证每个产物,而不是只看 IDE 红线。

三、贯穿项目:TaskSystem 的第一步 ​

从本课开始,所有 C++ 课程都围绕一个小型任务系统展开。第一版只完成一个任务函数的编译和链接:

text
task-system/
├── include/task_system/task.hpp
├── src/task.cpp
└── app/main.cpp

task.hpp 只声明 runTask(),task.cpp 定义它,main.cpp 调用它。你要能回答:如果只编译 main.cpp,为什么编译通过但链接失败?如果把定义放进头文件,什么时候会出现重复定义?后续课程会在这个项目上继续加入对象、资源和 worker。

四、四阶段模型 ​

text
main.cpp
→ 预处理:展开 include、宏和条件编译
→ 编译:检查类型并生成汇编
→ 汇编:生成 main.o/main.obj
→ 链接:合并目标文件与库,解析符号
→ app.exe / app

#include 不是“导入一个模块”,而是在预处理阶段把文本纳入当前翻译单元。每个 .cpp 通常独立编译,因此修改公共头文件可能让很多 .cpp 重新编译。

五、声明与定义 ​

cpp
// math.hpp:声明
int add(int a, int b);

// math.cpp:定义
#include "math.hpp"
int add(int a, int b) { return a + b; }

声明告诉编译器函数形状;定义提供实体。main.cpp 只看到声明也能通过编译,但链接时必须找到唯一、兼容的定义。

常见错误:

text
未定义符号:声明存在,定义没参与链接或签名不同
重复定义:同一非 inline 实体被多个翻译单元定义
ABI 不匹配:编译器、标准库、架构或编译选项不兼容

六、头文件和 ODR ​

头文件至少应有保护:

cpp
#pragma once

struct Vec2 {
    float x{};
    float y{};
};

ODR(One Definition Rule)要求某些实体在整个程序中只能有一个定义。类定义可以通过相同头文件出现在多个翻译单元;普通非 inline 函数定义若直接放头文件,会产生重复符号风险。

七、静态库与动态库 ​

text
静态库:链接时把需要的目标代码纳入程序
动态库:运行时由加载器装入,共享独立二进制

“链接成功”也不保证动态库在目标设备能被找到。Native 项目还要检查库搜索路径、架构、导出符号和运行时依赖。

八、可编译验证:看见中间产物 ​

创建 math.hpp、math.cpp、main.cpp,内容使用上面的 add。

GCC/Clang 示例:

bash
g++ -std=c++17 -E main.cpp -o main.i
g++ -std=c++17 -c main.cpp -o main.o
g++ -std=c++17 -c math.cpp -o math.o
g++ main.o math.o -o app
./app

MSVC 开发者终端可以使用:

bat
cl /std:c++17 /c main.cpp math.cpp
link main.obj math.obj /OUT:app.exe

故障复现 ​

故意不链接 math.o,确认出现未定义符号;再把 add 的完整定义同时放进两个 .cpp,确认出现重复定义。记录错误发生在哪一步。

九、把错误放回 TaskSystem ​

在 TaskSystem 中,undefined reference 先查 src/task.cpp 是否加入 target;重复符号先查定义是否错误地放在公共头文件;运行时找不到动态库再查部署,而不是修改业务代码掩盖链接问题。

十、诊断路径 ​

text
语法/类型错误 → 当前翻译单元、include、模板实例化
undefined symbol → 定义、目标文件、库顺序、签名、ABI
duplicate symbol → 头文件定义、重复源文件、ODR
运行时找不到 DLL/so → 部署路径、架构、加载器搜索规则

十一、标准与工具边界 ​

课程统一使用 C++17。GCC、Clang、MSVC 的命令和诊断文本不同,但翻译单元、目标文件和链接模型一致。Cocos Creator 2.4.x 的 Native 构建还叠加了生成工程、平台工具链和引擎库,排查时仍应先判断错误属于哪一层。

十二、练习与答案 ​

1. 为什么只修改 .cpp 通常比修改公共 .hpp 重编译更少? ​

因为 .cpp 是独立翻译单元;公共头文件会被多个翻译单元展开,依赖它们的目标文件都可能失效。

2. IDE 能跳到声明,为什么仍会 undefined reference? ​

跳转只证明索引器找到了声明,不证明定义已编译并参与最终链接。

3. 如何证明一个库真的包含目标符号? ​

先确认库架构和版本,再用 nm、objdump、dumpbin /symbols 等工具查看符号,最后核对修饰后的签名和链接命令。

十三、本课总结 ​

text
编译处理一个翻译单元。
链接把目标文件和库组成程序。
声明解决“长什么样”,定义解决“实体在哪里”。
先定位失败阶段,再处理错误文本。

下一课将在同一个 TaskSystem 中加入任务对象,回答它应该放在哪里、谁拥有它、函数返回后哪些地址仍然有效。