Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

本页目录

C++ 项目工作机制全景:从源码到可执行程序 / How C++ Projects Work from Source Code to Executables ​

📅 创建时间:2026-07-13 🏷️ 标签:#C++ #编译 #链接 #项目结构 #入门基础 📚 前置知识:C/C++ 基础语法


📋 文档目标 ​

本文档帮助你建立对 C++ 项目工作机制的完整心智模型。通过本文档,你将:

  • 理解一段 C++ 代码如何从源码一步步变成可运行的程序
  • 掌握编译的四个阶段:预处理 → 编译 → 汇编 → 链接
  • 理解翻译单元(Translation Unit)、目标文件(.o/.obj)、库文件、可执行文件的本质区别
  • 明白链接器的核心工作——符号解析和重定位
  • 知道 "undefined reference" 这种经典错误为什么会发生
  • 理解构建系统(CMake/Make/MSBuild)在整个流程中的角色
  • 熟悉一个典型 C++ 项目的目录结构:src/、include/、lib/、bin/、build/ 各自放什么

第1部分:一个程序是怎么"生"出来的? ​

1.1 从一段最简单的代码说起 ​

假设你写了这样一段代码:

cpp
// main.cpp
#include <iostream>

int main() {
    std::cout << "Hello, World!" << std::endl;
    return 0;
}
1
2
3
4
5
6
7

然后你在终端敲下:

bash
g++ main.cpp -o hello
./hello
# 输出:Hello, World!
1
2
3

这行命令看起来很简单,但背后发生了四个阶段的事情:

┌─────────────────────────────────────────────────────────────────────────────┐
│                    C++ 编译的四个阶段                                         │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  main.cpp                                                                   │
│     │                                                                       │
│     ▼                                                                       │
│  ┌──────────────────┐                                                       │
│  │ 1. 预处理         │  g++ -E main.cpp -o main.i                          │
│  │ Preprocessing    │                                                       │
│  │                  │  • 展开 #include(把头文件内容复制进来)                │
│  │                  │  • 处理 #define 宏替换                                │
│  │                  │  • 处理 #ifdef / #ifndef 条件编译                    │
│  │                  │  • 删除注释                                          │
│  │                  │  • 输出:.i 文件(纯文本,但非常长)                   │
│  └────────┬─────────┘                                                       │
│           ▼                                                                 │
│  ┌──────────────────┐                                                       │
│  │ 2. 编译           │  g++ -S main.i -o main.s                            │
│  │ Compilation      │                                                       │
│  │                  │  • 词法分析:把字符流转成 token 流                    │
│  │                  │  • 语法分析:把 token 组装成 AST(抽象语法树)         │
│  │                  │  • 语义分析:类型检查、重载决议、模板实例化            │
│  │                  │  • 代码生成:AST → 汇编代码                           │
│  │                  │  • 输出:.s 文件(汇编代码)                           │
│  └────────┬─────────┘                                                       │
│           ▼                                                                 │
│  ┌──────────────────┐                                                       │
│  │ 3. 汇编           │  g++ -c main.s -o main.o                            │
│  │ Assembly         │                                                       │
│  │                  │  • 把汇编指令翻译成机器码                              │
│  │                  │  • 生成符号表(哪些符号定义了、哪些需要外部提供)       │
│  │                  │  • 生成重定位表(哪些地址还不知道,需要链接器来填)     │
│  │                  │  • 输出:.o 文件(Linux)或 .obj 文件(Windows)       │
│  └────────┬─────────┘                                                       │
│           ▼                                                                 │
│  ┌──────────────────┐                                                       │
│  │ 4. 链接           │  g++ main.o -o hello                                │
│  │ Linking          │                                                       │
│  │                  │  • 符号解析:把"我需要 printf"和"printf 在这里"对上    │
│  │                  │  • 重定位:填上所有之前不知道的实际地址                 │
│  │                  │  • 合并段:把多个 .o 的 .text 段拼在一起               │
│  │                  │  • 输出:可执行文件(Linux ELF / Windows PE)          │
│  └──────────────────┘                                                       │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

1.2 四个阶段的深入理解 ​

预处理——不只是复制粘贴 ​

预处理是整个流程中最被低估的一步。做一个实验:

cpp
// before.cpp
#define MAX 100
#define ADD(a, b) ((a) + (b))

int arr[MAX];
int x = ADD(3, 5);

#ifdef DEBUG
    #include <debug_tools.h>
#endif
1
2
3
4
5
6
7
8
9
10
bash
g++ -E before.cpp -o after.i
1

预处理后,MAX 变成 100,ADD(3, 5) 变成 ((3) + (5)),条件编译的块被决定保留或删除。一个 #include <iostream> 展开后可能有几万行——这就是为什么 iostream 比较"重"的原因之一。

编译——核心智力劳动 ​

这是四个阶段中最复杂的一步。编译器做的事分解如下:

┌─────────────────────────────────────────────────────────────────────────────┐
│                    编译阶段的内部流程                                        │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  预处理后的 .i 文件                                                          │
│       │                                                                     │
│       ▼                                                                     │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐              │
│  │ 词法分析  │ →  │ 语法分析  │ →  │ 语义分析  │ →  │ 代码生成  │              │
│  │ Lexer    │    │ Parser   │    │ Semantic │    │ Code Gen │              │
│  └──────────┘    └──────────┘    └──────────┘    └──────────┘              │
│       │               │               │               │                    │
│       ▼               ▼               ▼               ▼                    │
│  字符流→token     token→AST       AST类型检查    AST→汇编代码               │
│                                                                             │
│  例如:int x = a + 1;                                                       │
│  词法:int / x / = / a / + / 1 / ;                                         │
│  语法:Declaration(Variable(x), Add(Identifier(a), Literal(1)))             │
│  语义:检查 a 是否声明过、int + int 是否合法                                │
│  代码:mov eax, [a]; add eax, 1; mov [x], eax                              │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

关键认知:编译阶段的输出是 .o 文件,但 .o 文件不等于可执行程序。 一个 .o 里的"函数调用"还不知道被调用方在哪里。

汇编——机械翻译 ​

汇编阶段做的事情最纯粹:把汇编指令一对一翻译成机器码(操作码 + 操作数)。但这里还有一件重要的事——符号表和重定位表的生成。

bash
# 查看 .o 文件的符号表
nm main.o
# 输出示例:
# 0000000000000000 T main           ← T = 在本文件中定义的符号(Text段)
#                  U _Z4putsPKc     ← U = 未定义,需要链接器来找
#                  U std::cout
1
2
3
4
5
6

U 代表 "Undefined"——这意味着 "我用了这个符号,但我不知道它在哪里,麻烦链接器帮我找"。

链接——最终组装 ​

链接器的工作分两步:

┌─────────────────────────────────────────────────────────────────────────────┐
│                        链接器的工作                                          │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  步骤1:符号解析 (Symbol Resolution)                                         │
│                                                                             │
│  ┌──────────┐   ┌──────────┐   ┌──────────┐                               │
│  │ main.o   │   │ foo.o    │   │ libc.so  │                               │
│  │          │   │          │   │          │                               │
│  │ 需要:    │   │ 提供:    │   │ 提供:    │                               │
│  │ foo()    │ → │ foo()    │   │ printf() │                               │
│  │ printf() │   │          │ → │          │                               │
│  └──────────┘   └──────────┘   └──────────┘                               │
│                                                                             │
│  链接器遍历所有 .o 和 .a/.so,为每一个 "U(未定义)" 符号                     │
│  找到对应的 "T(已定义)" 符号。找不到 → "undefined reference" 错误。         │
│                                                                             │
│  步骤2:重定位 (Relocation)                                                  │
│                                                                             │
│  链接前:main.o 里的 call 指令目标是 0x00000000(占位符)                    │
│  链接后:call 指令目标更新为 foo() 实际在可执行文件中的地址 0x401050          │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

第2部分:翻译单元——理解 C++ 编译的基本单位 ​

2.1 什么是翻译单元? ​

这个概念极其重要但经常被忽略。

翻译单元 = 一个 .cpp 文件 + 它递归 #include 的所有头文件(预处理展开后)
1

一个项目有多少个 .cpp 文件,就有多少个翻译单元,每个编译成独立的 .o 文件。

┌─────────────────────────────────────────────────────────────────────────────┐
│                    翻译单元与编译的关系                                       │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  main.cpp                math.cpp              utils.cpp                    │
│  ┌─────────────┐        ┌─────────────┐       ┌─────────────┐              │
│  │ #include    │        │ #include    │       │ #include    │              │
│  │   "math.h"  │        │   "math.h"  │       │   "utils.h" │              │
│  │ #include    │        │             │       │ #include    │              │
│  │   "utils.h" │        │ int add(    │       │   "math.h"  │              │
│  │             │        │   int a,    │       │             │              │
│  │ int main(){ │        │   int b){   │       │ void log(   │              │
│  │   ...       │        │   return    │       │   ...){     │              │
│  │ }           │        │   a+b;      │       │   ...       │              │
│  │             │        │ }           │       │ }           │              │
│  └──────┬──────┘        └──────┬──────┘       └──────┬──────┘              │
│         │        编译           │      编译           │      编译            │
│         ▼                      ▼                      ▼                     │
│    main.o                  math.o                utils.o                    │
│         │                      │                      │                     │
│         └──────────────────────┼──────────────────────┘                     │
│                                │  链接                                       │
│                                ▼                                            │
│                          my_program(可执行文件)                            │
│                                                                             │
│  核心要点:                                                                  │
│  • 每个 .cpp 独立编译——编译器一次只看一个翻译单元                             │
│  • math.cpp 不知道 main.cpp 里写了什么                                       │
│  • 它们之间的"沟通"靠头文件(声明)和链接器(定义)                           │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

2.2 头文件(.h)和源文件(.cpp)的分工 ​

┌─────────────────────────────────────────────────────────────────────────────┐
│                    声明 vs 定义                                              │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  头文件 (.h) = 接口说明书                                                   │
│  ┌─────────────────────────────────────────────────────────────┐           │
│  │ // math.h                                                    │           │
│  │ #pragma once              // 防止重复包含                     │           │
│  │                                                              │           │
│  │ int add(int a, int b);    // 声明:我承诺存在这个函数         │           │
│  │ int multiply(int a, int b);                                  │           │
│  │                                                              │           │
│  │ const double PI = 3.14159; // 常量可以在头文件中定义          │           │
│  │                                                              │           │
│  │ class Calculator {         // 类也可以声明在头文件中          │           │
│  │ public:                                                     │           │
│  │     int compute(int a, int b);  // 只声明方法                │           │
│  │ };                                                           │           │
│  └─────────────────────────────────────────────────────────────┘           │
│                                                                             │
│  源文件 (.cpp) = 具体实现                                                    │
│  ┌─────────────────────────────────────────────────────────────┐           │
│  │ // math.cpp                                                  │           │
│  │ #include "math.h"                                           │           │
│  │                                                              │           │
│  │ int add(int a, int b) {      // 定义:函数体的具体实现       │           │
│  │     return a + b;                                           │           │
│  │ }                                                            │           │
│  │                                                              │           │
│  │ int Calculator::compute(int a, int b) {                     │           │
│  │     return add(a, b) * 2;                                   │           │
│  │ }                                                            │           │
│  └─────────────────────────────────────────────────────────────┘           │
│                                                                             │
│  为什么分开?                                                                 │
│  • 修改 .cpp 只需要重新编译一个 .o,不需要重编所有文件                         │
│  • 发布库时可以只提供 .h + 编译好的 .lib/.so(不暴露源码)                     │
│  • 头文件是合同,源文件是实现——两者可以独立演化                               │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

2.3 One Definition Rule(ODR 单一定义规则) ​

这是 C++ 里一条极其关键的规则,违反它的后果非常隐蔽:

┌─────────────────────────────────────────────────────────────────────────────┐
│                    ODR 规则                                                  │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  规则1(强 ODR):函数、全局变量——整个程序中只能有一份定义                    │
│                                                                             │
│  ✅ 可以:在 .h 中声明,在 .cpp 中定义                                       │
│  ❌ 不可以:在 .h 中定义非 inline 函数(多个翻译单元包含会导致重复定义)       │
│                                                                             │
│  规则2(弱 ODR):类定义、模板、inline 函数、枚举——可以在多个翻译单元出现     │
│  但所有出现必须"完全相同"(token-by-token identical)                        │
│                                                                             │
│  // 危险示例:如果不同翻译单元看到了不同版本的类定义                          │
│  // 编译器不会报错,但运行时会崩溃(ODR violation, UB)                      │
│                                                                             │
│  这也是为什么:                                                              │
│  • 永远用 #pragma once 或 include guards                                    │
│  • 不要把函数实现写在头文件里(除非是 inline / template)                     │
│  • 编译选项(如 #define)要全局一致                                          │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

第3部分:构建系统的角色 ​

3.1 为什么需要构建系统? ​

如果你只有一个 main.cpp,直接 g++ main.cpp -o hello 就够了。但真实项目可能有几百个 .cpp 文件、几十个第三方库、多种编译选项、多平台支持。

┌─────────────────────────────────────────────────────────────────────────────┐
│                    一个中型 C++ 项目的构建依赖                                │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  src/                               external/                              │
│  ├── main.cpp         ────────────→ libA/include/                          │
│  ├── core/                          libA/src/...                           │
│  │   ├── engine.cpp    ────────────→ libB/include/                          │
│  │   └── parser.cpp                 libB/lib/libB.so                       │
│  ├── ui/                                                                   │
│  │   └── window.cpp    ────────────→ Qt (系统路径)                          │
│  └── utils/                                                                 │
│      └── logger.cpp                                                         │
│                                                                             │
│  问题:                                                                      │
│  • main.cpp 改了 → 只需要重编 main.cpp → 重新链接即可                        │
│  • math.h 改了 → 所有 #include "math.h" 的 .cpp 都要重编                     │
│  • libB 更新到 v2.0 → 可能需要改编译选项                                     │
│  • Debug 和 Release 编译选项不同                                             │
│  • Windows 和 Linux 的编译方式完全不同                                       │
│                                                                             │
│  构建系统的作用:                                                             │
│  1. 追踪文件依赖——只重编需要重编的文件(增量编译)                            │
│  2. 管理编译选项——Debug/Release、平台差异                                    │
│  3. 管理第三方库——找到它们、链接它们                                          │
│  4. 生成最终产物——可执行文件、动态库、静态库                                  │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

3.2 常见构建系统一览 ​

构建系统平台特点配置文件
MakeLinux/Unix 原生古老但通用,手写规则很痛苦Makefile
Ninja跨平台极快,通常由 CMake 生成build.ninja
MSBuildWindows 原生Visual Studio 专用.vcxproj
CMake跨平台现代 C++ 的事实标准CMakeLists.txt
Meson跨平台新时代构建系统,速度快meson.build
Bazel跨平台Google 出品,适合巨型项目BUILD
xmake跨平台Lua 语法,简洁xmake.lua

在我们的文档系列中,CMake 是一等公民。 因为它是目前 C++ 生态中最广泛使用的跨平台构建系统。

3.3 CMake 的宏观视角 ​

CMake 不是编译器,它是"构建系统的生成器":

┌─────────────────────────────────────────────────────────────────────────────┐
│                    CMake 的工作流程                                           │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  CMakeLists.txt(你写的)                                                    │
│       │                                                                     │
│       ▼ cmake -B build -G "Unix Makefiles"                                 │
│                                                                             │
│  CMake 读取你的 CMakeLists.txt                                               │
│       │                                                                     │
│       │  • 检测编译器(gcc?clang?msvc?)                                 │
│       │  • 检测系统库(pthread 在哪?OpenGL 在哪?)                         │
│       │  • 查找第三方库(find_package)                                      │
│       │  • 解析你的源文件列表                                                │
│       │                                                                     │
│       ▼ 生成                                                                 │
│                                                                             │
│  Makefile(或 Ninja build 文件、或 Visual Studio .sln)                      │
│       │                                                                     │
│       ▼ make -j$(nproc)                                                     │
│                                                                             │
│  实际调用 g++/cl.exe 等编译器进行编译和链接                                  │
│                                                                             │
│  简单的 CMakeLists.txt 示例:                                                 │
│  ┌─────────────────────────────────────────────────────────────┐           │
│  │ cmake_minimum_required(VERSION 3.20)                        │           │
│  │ project(MyProject VERSION 1.0 LANGUAGES CXX)                │           │
│  │                                                              │           │
│  │ # 生成可执行文件                                             │           │
│  │ add_executable(my_app main.cpp)                             │           │
│  │                                                              │           │
│  │ # 添加头文件搜索路径                                         │           │
│  │ target_include_directories(my_app PRIVATE include/)         │           │
│  └─────────────────────────────────────────────────────────────┘           │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

第4部分:C++ 项目的标准目录结构 ​

4.1 解剖一个典型 C++ 项目 ​

┌─────────────────────────────────────────────────────────────────────────────┐
│                    典型 C++ 项目目录结构                                      │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  my_project/                          ← 项目根目录                           │
│  ├── CMakeLists.txt                   ← 顶层 CMake 配置                     │
│  ├── README.md                        ← 项目说明                            │
│  ├── LICENSE                          ← 许可证                              │
│  │                                                                          │
│  ├── src/                             ← 自己的源代码                        │
│  │   ├── main.cpp                     ← 入口文件                            │
│  │   ├── core/                        ← 核心逻辑                            │
│  │   │   ├── engine.cpp                                                    │
│  │   │   └── engine.h          ← .h 和 .cpp 放在一起也是常见做法             │
│  │   └── utils/                        ← 工具函数                           │
│  │       ├── logger.cpp                                                   │
│  │       └── logger.h                                                      │
│  │                                                                          │
│  ├── include/                         ← 公开头文件(给外部使用者看的)       │
│  │   └── my_project/                                                       │
│  │       ├── api.h                   ← 公共 API                            │
│  │       └── types.h                  ← 公共类型定义                        │
│  │                                                                          │
│  ├── lib/                             ← 预编译好的第三方库文件               │
│  │   ├── libfoo.so                    ← Linux 动态库                        │
│  │   ├── libbar.a                     ← Linux 静态库                        │
│  │   ├── foo.lib                      ← Windows 导入库(或静态库)           │
│  │   └── foo.dll                      ← Windows 动态库                      │
│  │                                                                          │
│  ├── bin/                             ← 编译输出目录                        │
│  │   ├── my_app                       ← Linux 可执行文件                    │
│  │   ├── my_app.exe                   ← Windows 可执行文件                  │
│  │   └── *.dll / *.so                 ← 运行时需要的动态库                  │
│  │                                                                          │
│  ├── build/                           ← CMake 构建目录(不提交到 git)       │
│  │   ├── Makefile                                                          │
│  │   └── CMakeCache.txt                                                   │
│  │                                                                          │
│  ├── external/ 或 third_party/        ← 第三方库的源码                      │
│  │   ├── spdlog/                      ← 第三方 logging 库                   │
│  │   └── nlohmann_json/              ← 第三方 JSON 库                      │
│  │                                                                          │
│  ├── test/                            ← 测试代码                            │
│  │   ├── CMakeLists.txt                                                    │
│  │   └── test_main.cpp                                                    │
│  │                                                                          │
│  ├── doc/                             ← 文档                                │
│  │                                                                          │
│  ├── cmake/                           ← 自定义 CMake 模块                   │
│  │   └── FindCustomLib.cmake                                               │
│  │                                                                          │
│  └── scripts/                         ← 构建/部署脚本                       │
│      └── build.sh                                                          │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

4.2 各目录的职责 ​

┌─────────────────────────────────────────────────────────────────────────────┐
│  目录       │  放什么                          │  git 提交?                   │
│─────────────┼──────────────────────────────────┼──────────────────────────────│
│  src/       │ 项目的 .cpp 源文件                │  ✅ 提交                      │
│  include/   │ 对外公开的 .h 头文件              │  ✅ 提交                      │
│  lib/       │ 预编译的第三方库二进制            │  看情况(大文件用 git-lfs)    │
│  bin/       │ 编译产物(可执行文件等)           │  ❌ 不提交                    │
│  build/     │ CMake 生成的中间文件               │  ❌ 绝对不提交                │
│  external/  │ 第三方库源码                      │  ✅ 提交(或 git submodule)   │
│  test/      │ 测试代码                          │  ✅ 提交                      │
│  cmake/     │ 自定义 CMake 查找模块             │  ✅ 提交                      │
│  doc/       │ 文档                              │  ✅ 提交                      │
└─────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13

第5部分:四种文件身份——看清一个 C++ 项目中的所有文件类型 ​

一个 C++ 项目完成后,你会面对的文件可以分成四大类:

┌─────────────────────────────────────────────────────────────────────────────┐
│            C++ 项目中的四种文件身份                                          │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  1. 头文件 (.h / .hpp / .hxx)                                               │
│  ┌───────────────────────────────────────────────────────────────┐         │
│  │  • 作用:声明接口——函数签名、类定义、类型别名                  │         │
│  │  • 使用方式:被 .cpp 文件 #include                             │         │
│  │  • 特点:不直接参与编译,在预处理阶段被"融入".cpp               │         │
│  │  • 发布库时:必须随库提供(没有头文件就没法用你的库)           │         │
│  └───────────────────────────────────────────────────────────────┘         │
│                                                                             │
│  2. 源文件 (.cpp / .cc / .cxx)                                              │
│  ┌───────────────────────────────────────────────────────────────┐         │
│  │  • 作用:函数的实际实现                                          │         │
│  │  • 使用方式:被编译器编译成 .o / .obj                           │         │
│  │  • 特点:每个 .cpp 是一个独立的翻译单元                        │         │
│  │  • 发布库时:不需要提供(编译成二进制库后源码就不需要了)       │         │
│  └───────────────────────────────────────────────────────────────┘         │
│                                                                             │
│  3. 库文件 (.a / .lib / .so / .dll / .dylib)                               │
│  ┌───────────────────────────────────────────────────────────────┐         │
│  │  • 作用:可复用的预编译代码集合                                  │         │
│  │  • 使用方式:被链接器链接到可执行文件,或被运行时加载            │         │
│  │  • 分为:静态库(.a / .lib_1)和动态库(.so / .dll / .dylib)  │         │
│  │  • 发布时:静态库只需给 .a/.lib;动态库需要给 .so/.dll + 头文件 │         │
│  │  • lib/ 目录里放的就是这类文件                                  │         │
│  └───────────────────────────────────────────────────────────────┘         │
│                                                                             │
│  4. 可执行文件 (Linux 无后缀 / .exe)                                        │
│  ┌───────────────────────────────────────────────────────────────┐         │
│  │  • 作用:可以直接运行的程序                                      │         │
│  │  • 使用方式:双击(Windows)或 ./运行(Linux)                   │         │
│  │  • 特点:包含 main() 入口点                                     │         │
│  │  • bin/ 目录里放的就是这类文件                                  │         │
│  └───────────────────────────────────────────────────────────────┘         │
│                                                                             │
│  ⚠️ 特别提醒:Windows 上 .lib 后缀有两种完全不同的含义!                    │
│     这个我们将在 01 篇中详细拆解——这是 Windows C++ 开发最大的认知陷阱。     │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

第6部分:一个项目"跑起来"需要什么? ​

6.1 编译时 vs 运行时 ​

┌─────────────────────────────────────────────────────────────────────────────┐
│                    编译时需要 vs 运行需要                                     │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│【编译时(Build Time)需要】                                                  │
│  ✅ 源码 — 自己的 .cpp 和 .h                                                │
│  ✅ 第三方头文件 — 第三方库的 .h(告诉编译器函数签名是什么)                  │
│  ✅ 第三方库文件 — .lib(Windows导入库)或 .a(Linux静态库)                  │
│                    — 让链接器能验证"这个符号确实存在"                         │
│                                                                             │
│【运行时(Runtime)需要】                                                     │
│  ✅ 可执行文件本身                                                           │
│  ✅ 动态库 — 如果用了动态链接,.dll / .so 必须在系统能找到的地方              │
│  ✅ 配置文件、资源文件、数据文件等                                            │
│                                                                             │
│  ⚠️ 常见错误:编译成功但运行时报 "cannot open shared object file"            │
│     → 编译时只需要头文件 + .lib 确认符号存在                                  │
│     → 运行时才真正需要 .dll/.so 中的代码                                      │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

6.2 为什么有些库给 .lib 不给 .cpp? ​

这是很多新手困惑的地方:甲方给你一个 SDK,只给了 .h + .lib + .dll,没有给 .cpp。

因为库的作者已经把 .cpp 编译成了 .lib/.dll。你有头文件就知道怎么调用,有库文件就有实现代码,不需要源码。这也是保护知识产权的方式。

但如果甲方只给了源码(一堆 .cpp 和 .h),那你就需要用 CMake 组织编译。有时给的是预编译二进制但缺少某个平台的版本,或者编译选项不对兼容不了你的编译器——这些痛点将在第 03 篇和第 05 篇详细展开。


核心总结 ​

┌─────────────────────────────────────────────────────────────────────────────┐
│                    核心概念速查                                              │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  编译四阶段:                                                                │
│    预处理(展开头文件/宏) → 编译(源码→汇编) → 汇编(汇编→机器码.o)        │
│    → 链接(合并 .o + 解析符号 + 填地址)                                     │
│                                                                             │
│  翻译单元:一个 .cpp + 它 include 的所有头文件                                │
│                                                                             │
│  头文件 = 声明(接口),源文件 = 定义(实现)                                  │
│                                                                             │
│  构建系统(CMake)的作用:                                                   │
│    • 管理哪些文件需要编译                                                    │
│    • 管理编译选项(Debug/Release/平台差异)                                  │
│    • 找到并链接第三方库                                                      │
│                                                                             │
│  四种文件身份:头文件(.h) / 源文件(.cpp) / 库文件(.a .lib .so .dll) / 可执行文件│
│                                                                             │
│  编译时需要:头文件 + 导入库(告诉链接器符号存在)                             │
│  运行需要:可执行文件 + 动态库(实际代码执行)                                 │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

章节测试 ​

测试1:编译过程 ​

请列出 C++ 源码变成可执行文件的四个阶段,并说明每个阶段的输入和输出。

测试2:翻译单元 ​

什么是翻译单元(Translation Unit)?一个项目有 5 个 .cpp 文件,编译后会产生多少个 .o 文件?

测试3:头文件与源文件 ​

cpp
// math.h
int add(int a, int b);
1
2

这段代码在 .h 文件中是什么?在 .cpp 文件中应该写什么?为什么要这样分开?

测试4:undefined reference ​

出现 undefined reference to 'foo()' 这个错误时,问题出在编译的哪个阶段?通常是什么原因造成的?

测试5:项目结构 ​

一个项目有 src/、include/、lib/、bin/、build/、external/ 六个目录。请说明:

  • 哪些应该提交到 git?
  • 编译后的 .exe 文件通常放在哪个目录?
  • 第三方库的源码通常放在哪个目录?

测试6:编译时 vs 运行时 ​

为什么有时候编译能通过,但运行时报错 "cannot open shared object file"?


参考答案 ​

测试1答案 ​

答案:

  1. 预处理:输入 .cpp/.h → 输出 .i(展开后的纯文本)
  2. 编译:输入 .i → 输出 .s(汇编代码)
  3. 汇编:输入 .s → 输出 .o/.obj(目标文件,机器码 + 符号表 + 重定位表)
  4. 链接:输入 .o/.obj + .a/.lib → 输出可执行文件或动态库

测试2答案 ​

答案:翻译单元 = 一个 .cpp 文件 + 它递归 #include 的所有头文件(预处理展开后)。5 个 .cpp 编译后产生 5 个 .o 文件,每个翻译单元独立编译。

测试3答案 ​

答案:

  • 在 .h 文件中,int add(int a, int b); 是函数声明——告诉编译器"存在这样一个函数,它接受两个 int 参数,返回一个 int"。
  • 在 .cpp 文件中应该写函数定义——int add(int a, int b) { return a + b; },即函数的具体实现。
  • 分开的原因是:
    1. 修改实现时只需重新编译一个 .cpp 而不是全部
    2. 发布库时只需给头文件,不需要暴露源代码

测试4答案 ​

答案:问题出在链接阶段。链接器在合并所有 .o 文件时,发现某个符号被引用(U),但在所有 .o 和 .a/.lib 中都找不到它的定义(T)。常见原因:

  • 忘了链接包含该函数的 .cpp 文件
  • 忘了在 target_link_libraries 中添加对应的库
  • 函数名拼写错误
  • C++ name mangling 导致的符号不匹配(编译为 C++ 但链接为 C)

测试5答案 ​

答案:

  • 应该提交:src/、include/、test/、cmake/、doc/,external/ 也可以提交(或用 git submodule)
  • 不应该提交:bin/、build/(编译产物和构建中间文件)
  • .exe 放在:bin/
  • 第三方库源码放在:external/ 或 third_party/

测试6答案 ​

答案:编译时链接器只需要导入库(.lib)或静态库(.a)来验证符号存在并记录依赖关系,不拷贝 DLL 的实际代码。运行程序时,操作系统才真正需要加载动态库(.dll / .so)中的代码到内存中。如果动态库不在系统搜索路径(如 PATH、LD_LIBRARY_PATH)中,运行时加载失败,就会报这个错误。


相关笔记 ​

  • windows build outputs - Windows 编译产物详解:exe、DLL、两种 lib 的辨析
  • linux build outputs - Linux 编译产物详解:ELF、.a、.so
  • project structure thirdparty cmake - 项目结构、第三方库集成与 CMake 实战

下一步学习 ​

  • [ ] 阅读 01 - Windows 编译产物详解 —— 理解 exe、dll、两种 lib 的区别
  • [ ] 阅读 02 - Linux 编译产物详解 —— 理解 ELF、.a、.so、soname

学习状态:🟡 开始学习

最后更新于:

Pager
下一篇← C++ 编程 / C++ Programming

持续记录,持续成长

Copyright © Tidenflow