本章目标:理解 .hpp/.cpp 如何变成可执行文件、 什么是翻译单元、链接错误原理、include guard、 内联函数/变量的链接规则、静态库与动态库。 目标是:以后遇到任何编译/链接错误都能自己解决。
hello.cpp ──预处理──▶ hello.i ──编译──▶ hello.s ──汇编──▶ hello.o
│
libmath.a ────────────────────────────────────────────┤ 链接
▼
可执行文件
四个阶段(clang++ 一条命令全做完,但要知道它们在干嘛):
(把头文件内容"粘贴"进来,宏展开)
clang++ -E hello.cpp # 看预处理结果
clang++ -S hello.cpp # 生成 hello.s
clang++ -c hello.cpp # 生成 hello.o
clang++ a.o b.o -o app
人话模型:
只记录了一个"待填的符号名"
一个 .cpp 文件 + 它 #include 的所有头文件(预处理后) = 一个翻译单元。编译器的基本工作单位。
关键推论(无数 bug 的根源):
→ 链接时"重复定义"错误(ODR 违反)
// math.hpp
int add(int a, int b) { return a + b; } // 普通函数定义!
// a.cpp 和 b.cpp 都 #include "math.hpp"
→ 出现两份 add 的定义 → 链接报"redefinition"
解决办法(三选一):
extern int global_counter; // 声明:告诉编译器"有这东西"
int global_counter = 0; // 定义:真正分配存储
void f(); // 函数声明
void f() {} // 函数定义
规则(ODR,One Definition Rule,一个定义原则):
// myheader.hpp
#pragma once // 现代写法(编译器都支持)
// 等价传统写法:
#ifndef MYHEADER_HPP
#define MYHEADER_HPP
...
#endif
作用:同一个翻译单元里,头文件内容只"粘贴"一次。 (防止 a.hpp 和 b.hpp 都 include c.hpp 时出现两份 c 的内容)
// header.hpp
inline int square(int x) { return x * x; } // 头文件里放函数定义
inline constexpr int LIMIT = 100; // C++17 inline 变量
规则:inline 函数/变量允许在多个翻译单元重复定义, 链接器负责"合并成一份"。 代价:内联展开可能增加代码体积(现代编译器自行判断,inline 只是"允许多次定义"的许可证,不一定真内联)。
什么时候需要 inline?
// globals.cpp
int counter = 0; // 定义
// use.cpp
extern int counter; // 声明"用别人的"
void inc() { ++counter; }
注意:全局变量是魔鬼(难以测试、线程不安全、初始化顺序问题)。 尽量别用;非用不可时:
原因:声明了 foo 但没实现 / 忘了链接包含实现的 .o / 库 解决:找到实现所在的 .cpp 加进编译;或 -lxxx 链接库
原因:foo 在头文件里被定义且被多个 .cpp 包含 解决:定义移 .cpp 或用 inline
原因:类声明了虚函数,但某个虚函数没实现 解决:给所有虚函数补实现(或 = 0 / = default)
原因:目标目录不存在 / 输出名冲突(比如 -o 和输入同名) 解决:检查路径
原因:模板实现写在 .cpp 里,但没被实例化 解决:模板实现必须放在头文件(或显式实例化)
把一堆 .o 打包就是库。 静态库 .a:链接时把代码"复制"进可执行文件 动态库 .so:运行时才加载,可共享
# 制作静态库(Termux 有 ar)
clang++ -c a.cpp -o a.o
ar rcs libmy.a a.o
# 使用
clang++ main.cpp libmy.a -o app
# 或 clang++ main.cpp -L. -lmy -o app (-L 找库目录,-l 库名)
# 制作动态库
clang++ -shared -fPIC a.cpp -o libmy.so
# 使用
clang++ main.cpp -L. -lmy -o app
LD_LIBRARY_PATH=. ./app # 告诉运行时去哪找库
namespace mylib {
int version() { return 1; }
namespace detail { int helper() { return 2; } } // 嵌套
}
namespace alias = mylib; // 别名
mylib::version();
using namespace mylib; // 全局引用(少用,会污染)
using mylib::version; // 局部引用(推荐)
匿名命名空间 = 文件内部私有(效果类似 static):
namespace {
int only_this_file() { return 3; }
}
模板是"模具":编译器看到 template 定义时不生成代码, 只有"用到具体类型"时当场实例化。 如果实现放 .cpp:a.cpp 里的模板函数没人用它(没实例化), main.cpp 用它时又看不到实现 → undefined reference。
解决(只有两条路):
template std::vector<int> foo<int>(); // 写明实例化哪些类型
// math.cppm(模块单元)
export module math;
export int add(int a, int b) { return a + b; }
// main.cpp
import math;
int main() { return add(1, 2); }
// 编译(clang 21 支持)
clang++ -std=c++20 --precompile math.cppm -o math.pcm
clang++ -std=c++20 -fmodule-file=math.pcm main.cpp math.pcm -o app
优点:只编译一次、不污染宏、不重复解析头文件。 现状:工具链已可用,但生态仍在迁移,老项目还是 #include。
练习题(配套测试:测试_第20章_编译模型.cpp)
读错误信息并修复。