1 C++ 编译构建过程
C++ 程序从源代码到最终可执行文件的生成,通常经历以下几个阶段:
1.1 概览流程
源码 .cpp / .h
↓ 预处理器(Preprocessing)
预处理代码(宏展开、包含头文件)
↓ 编译器(Compilation)
汇编代码(.s)
↓ 汇编器(Assembly)
目标文件(.o / .obj)
↓ 链接器(Linking)
可执行文件(.exe / .out / .dll / .so)
1.2 预处理(Preprocessing)
处理 #include、#define、#if/#ifdef 等指令,生成纯净的源代码。
- 展开所有宏
- 插入头文件内容
- 条件编译判断(如平台相关代码)
命令示例(GCC):
g++ -E main.cpp -o main.i1.3 编译(Compilation)
将预处理后的 .i 文件翻译为汇编语言代码 .s,同时进行语义分析、类型检查、优化等。
命令示例:
g++ -S main.i -o main.s1.4 汇编(Assembly)
将汇编代码 .s 转换为机器码(目标文件 .o 或 .obj)。
命令示例:
g++ -c main.s -o main.o汇编一般由专门的汇编器完成,但有些编译器(如 clang、gcc)集成了汇编功能,可以一气呵成title: 相关概念
**目标文件**(Object File,通常是 `.o` 或 `.obj`)是编译器和汇编器生成的中间文件,是机器码和元信息的集合,供链接器使用。它不仅包含函数的机器码,还包含丰富的元数据,具体包括:
1. 机器码(Machine Code)
* 函数和变量的机器指令和数据的二进制编码,是可执行程序的核心代码。
2. 符号表(Symbol Table)
* 记录目标文件中定义和引用的符号(函数名、变量名等)及其属性,如地址、大小、类型(函数/变量)、可见性(全局/局部)等。
* 符号分为**定义符号**(提供实现)和**未定义符号**(需要从其他目标文件或库中寻找)。
* 链接器根据符号表进行符号解析和重定位。
3. 重定位信息(Relocation Information)
* 记录目标文件中需要修改的地址信息,因为最终可执行文件的地址在链接时才确定。
* 例如,函数调用的跳转地址、变量的绝对地址等,在链接时会根据符号表调整。
4. 调试信息(Debug Info,可选)
* 包含变量名、类型、源代码行号等调试相关信息,便于调试器定位代码。
* 格式如 DWARF、PDB(Windows)等。
5. 段(Sections)
* 代码段(`.text`):存放机器码
* 数据段(`.data`、`.bss`):存放初始化和未初始化数据
* 其他辅助段:符号表段、重定位段、调试段等
6. 其他元数据
* 目标文件格式头(如 ELF、COFF、Mach-O)描述文件结构
* 目标文件的版本、依赖等信息
1.5 链接(Linking)
将多个目标文件 .o 以及依赖库合并,解析符号地址,生成可执行文件或动态库。
命令示例:
g++ main.o libmath.a -o app可生成:
- 可执行文件
.exe,.out - 静态库
.a,.lib - 动态库
.so,.dll,.dylib
1.5.1 链接方式详解:静态链接 vs 动态链接
静态链接(Static Linking):
- 所有依赖的库代码都打包进可执行文件
- 编译时进行链接,生成的文件体积更大
- 运行时无需外部库,适合嵌入式或便携式部署
动态链接(Dynamic Linking):
- 可执行文件仅保存对动态库的引用
- 程序运行时按需加载
.dll、.so等共享库 - 文件体积小,多个程序可共享同一动态库
| 对比项 | 静态链接 | 动态链接 |
|---|---|---|
| 执行文件大小 | 较大 | 较小 |
| 部署便利性 | 高 | 低(需携带动态库) |
| 运行时加载 | 否 | 是 |
| 多程序共享库 | 否 | 是 |
Q: 静态链接只会将用到的代码包含进去还是会将所有的库代码包含进去?
在静态链接中,**链接器只会将库中实际用到的符号(函数、变量等)所对应的目标代码提取出来**,不会把整个静态库完整打包进最终的可执行文件。
这背后的机制如下:
* 静态库(如 `.a`, `.lib`)实际上是多个目标文件(`.o`, `.obj`)的集合。
* 链接器在扫描可执行文件时,会根据引用的符号去对应的目标文件中查找定义。
* **只有用到的那些目标文件(包含所需符号)才会被包含到最终生成的二进制中**,其余部分会被丢弃。
例如,如果 `libmath.a` 包含了 `add.o`, `sub.o`, `mul.o` 三个模块,而你的程序只调用了 `add()` 函数,那么最终链接的 `.exe` 文件里只会包含 `add.o` 的内容。
1.5.2 CMake 中的链接方式控制
目标: 使用 target_link_libraries 指定链接方式,通常依赖库的构建形式决定最终链接方式。
静态链接:
# 假设 libmath.a 是静态库
add_executable(app main.cpp)
target_link_libraries(app PRIVATE /path/to/libmath.a)或:
# 使用 find_package 静态链接
find_package(MyLib REQUIRED)
target_link_libraries(app PRIVATE MyLib::MyLibStatic)动态链接:
# 链接动态库(如 libmath.so 或 math.dll)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE math)Tip
target_link_libraries可接受目标名、路径、别名等- 链接哪种类型,取决于链接库的文件形式(
.avs.so)或 CMake 提供的 target- 若依赖库使用 CMake
INTERFACE、IMPORTED、ALIAS,可通过MyLib::MyLib精确控制- 可通过
-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDLL等变量控制运行时链接方式(MSVC)- 静态库
.a,.lib- 动态库
.so,.dll,.dylib
Q: 如果lib路径下同时有动态库和静态库,会优先链接哪个?
这个行为依赖于平台、工具链、和 `CMake` + 链接器的具体实现,但可以总结如下:
- 🧭 一般规则(以 Unix/GCC 系为例)
```cmake
target_link_libraries(app PRIVATE math)
```
等效于传递给链接器的:
-lmath
链接器(如 `ld`)会根据搜索路径,在找到第一个满足条件的文件时停止查找,**通常搜索顺序如下**:
libmath.so → libmath.a
也就是说,**动态库 `.so` 优先于静态库 `.a`**。
> 这也是为什么很多项目使用 `libfoo_static.a` 作为静态库名,以避免名称冲突。
- 🪟 Windows / MSVC 下的行为
在 MSVC(`link.exe`)或 MinGW 上,默认是:
math.dll.lib → math.lib
- 如果 `.lib` 是动态库的 import lib,会优先使用它(等效于动态链接)。
- 如果只存在静态 `.lib`,才会静态链接。
1.6 运行时依赖(Runtime)
C++ 编译运行时涉及多种库,这些库为程序提供运行时支持,主要包括以下几类:
| 库类型 | 功能说明 | 常见实现(示例) | CMake 配置与说明 |
|---|---|---|---|
| 线程库 | 支持多线程并发执行,提供线程创建、同步、互斥等基础设施 | POSIX Threads(Linux/macOS),Windows Threads | CMake 中使用 find_package(Threads REQUIRED),通过 ${CMAKE_THREAD_LIBS_INIT} 链接线程库 |
| 异常处理支持 | 提供异常抛出和捕获机制,不同平台有不同实现 | DWARF(Linux),SEH(Windows),SJLJ(MinGW) | 通过编译器选项控制,MSVC 通过 /EHsc 启用异常,GCC 通过 -fexceptions 等 |
| C 标准库 | 提供基础的 C 运行时支持,如内存管理、IO、字符串处理等 | glibc(Linux),msvcrt(Windows),musl, uClibc | CMake 默认使用系统 C 库,交叉编译时需指定工具链和 sysroot 配置 |
| C++ 标准库 | 实现 C++ 标准模板库功能,如容器、算法、字符串、IO 流等 | libstdc++(GCC),libc++(LLVM),MSVC STL | CMake 中通过编译器选择自动链接,或手动指定 target_link_libraries |
| 运行时库类型 | 决定运行时库的链接方式,影响二进制文件大小、性能和部署方式 | MSVC 的 MD(动态)、MT(静态)版本 | MSVC 编译选项 /MD, /MT,在 CMake 中通过 CMAKE_MSVC_RUNTIME_LIBRARY 变量设置 |
1.6.1 线程库
- 线程库负责支持多线程执行环境,常见有 POSIX Threads(Linux/macOS)和 Windows Threads。
- CMake 使用
find_package(Threads)来检测平台线程支持,目标链接${CMAKE_THREAD_LIBS_INIT}。
示例:
find_package(Threads REQUIRED)
target_link_libraries(app PRIVATE Threads::Threads)1.6.2 异常处理支持
- 不同平台/编译器对异常处理实现不同,比如 Windows MSVC 使用 SEH,MinGW GCC 可选 DWARF 或 SJLJ。
- CMake 本身不直接控制异常模型,但通过编译器选项启用,例如:
GCC/Clang:
-fexceptionsMSVC:
/EHsc1.6.3 C 标准库
- 提供基础 C 运行时支持,是所有程序必须依赖的。
- Linux 一般使用
glibc,也有轻量实现如musl、uClibc。 - Windows 使用
msvcrt.dll。 - CMake 默认使用系统库,交叉编译时需配合工具链文件指定。
Note
MSVC 的 C 标准库主要是 MSVCRT(Microsoft Visual C++ Runtime Library),具体说明如下:
- MSVCRT 是微软为 Windows 平台实现的 C 标准库运行时,包含了如 printf、malloc、memcpy 等基础 C 函数。
- MSVCRT 以动态链接库形式存在,常见版本如 msvcrt.dll、msvcr100.dll、msvcr120.dll 等,不同 Visual Studio 版本对应不同 DLL。
什么是 CRT
详见 CRT.
1.6.4 C++ 标准库
- 提供 C++ 标准库功能(STL 容器、算法等)。
- GCC 默认用
libstdc++,LLVM/Clang 推荐使用libc++,MSVC 自带 STL。 - CMake 通过编译器自动处理,或者在特殊需求时手动链接。
1.6.5 运行时库 CRT 类型一致性(主要针对 MSVC)
- MSVC CRT 提供
MD(多线程 DLL)、MT(多线程静态)版本及其调试版本。 - 在 MSVC 工具链下,库的构建类型(静态库
.lib或动态库.dll)与运行时链接方式(/MT,/MD, 及其 Debug 版本/MTd,/MDd)是两个正交维度,理论上可以组合出4 种常见构建方式 - 在 MSVC 工具链下,如果使用了一种链接的运行时(如
/MT选项编译的库),那所有使用该库的代码(包括你的主程序和其他库)也必须统一使用相同的运行时设置,否则会引发严重的问题 - CMake 中可通过设置变量来控制:
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>") # 对应 /MT 和 /MTd为什么 msvc 需要 CRT 一致,其他的工具链不需要指定这个?
这个问题挺关键,原因比较多,也比较复杂,主要和 MSVC 的设计历史、Windows 平台特性和运行时库的多样性有关。总结来说:
1 MSVC 运行时库类型的背景和作用
MSVC 不仅仅是单纯的编译器,还包含了一整套 C/C++ 运行时库(CRT),负责内存管理、异常处理、I/O、线程等功能。
这些运行时库有静态版本和动态版本,比如:
- 静态 CRT(
libcmt.lib):库代码直接编译进程序,生成的可执行文件体积大,但部署简单。- 动态 CRT(
msvcrt.dll及其对应导入库msvcrt.lib):可执行文件较小,运行时加载共享库,多个程序可共享,节省内存。因为这两种方式对程序的链接方式、二进制兼容、运行行为影响很大,MSVC 需要你显式选择(通过编译选项
/MT、/MD等)以明确链接方式。参考:
2 Windows 平台特性
- Windows 平台的 DLL(动态链接库)机制复杂,不同版本的 CRT DLL 之间不兼容,内存分配与释放必须在同一个 CRT 中完成,否则会导致错误。
- 这就要求程序编译时明确指定使用哪种 CRT 版本,确保运行时行为一致。
- 不同程序或模块使用不同 CRT 版本会导致潜在风险,所以 MSVC 要求编译选项明确这一点。
3 其他工具链情况
GCC/Clang 等工具链通常基于 POSIX 平台,使用统一的 libc(如 glibc、musl),静态或动态链接更透明。
这些平台的运行库通常只需指定是否静态或动态链接,且库版本兼容性较好,不需要像 MSVC 那样细分运行时库类型。
GCC/Clang 运行时选项主要关注:
- 标准库(libstdc++ vs libc++)
- 线程模型(pthread vs Windows threads)
- 异常处理(DWARF, SJLJ, SEH)
4 实际影响和配置
- MSVC 的运行时库类型直接影响二进制接口(ABI)、调试信息、异常处理、线程行为等。
- 在 CMake 中,你可以用变量
CMAKE_MSVC_RUNTIME_LIBRARY(CMake 3.15+)来指定运行时库类型,如:set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreadedDLL") # 对应 /MD set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded") # 对应 /MT
- 这让你控制是静态还是动态链接 CRT。
1.7 编译过程可选项
| 阶段 | 常见命令行选项 |
|---|---|
| 预处理 | -E, -I, -D |
| 编译 | -S, -std=c++20, -O2, -Wall |
| 汇编 | -c |
| 链接 | -lxxx, -L, -static, -shared |