CMake 学习笔记:Greeter 项目从可执行文件到库的演进

本笔记旨在演示一个 C++ 项目如何使用 CMake 从一个简单的、所有代码编译成单一可执行文件的结构,演进为一个更模块化、更专业的“库 + 可执行文件”的结构。

第一阶段:Greeter 作为单一可执行文件

这是最直接的入门级项目组织方式,适用于小型项目或快速原型开发。

1. 文件结构

假设我们的项目目录结构如下:

1
2
3
4
5
6
7
greeter_project_executable/
├── CMakeLists.txt
├── include/
│ └── greeter.h # Greeter 类的头文件
└── src/
├── greeter.cpp # Greeter 类的实现
└── main.cpp # 主程序入口

2. C++ 代码

include/greeter.h:

1
2
3
4
5
6
7
8
9
10
11
#ifndef GREETER_H
#define GREETER_H

#include <string>

class Greeter {
public:
void sayHello(const std::string& name);
};

#endif // GREETER_H

src/greeter.cpp:

1
2
3
4
5
6
#include "greeter.h" // 引用我们自己的头文件
#include <iostream>

void Greeter::sayHello(const std::string& name) {
std::cout << "Hello, " << name << " from Greeter (Executable version)!" << std::endl;
}

src/main.cpp:

1
2
3
4
5
6
7
#include "greeter.h" // 引用我们自己的头文件

int main() {
Greeter myGreeter;
myGreeter.sayHello("CMake User");
return 0;
}

3. CMakeLists.txt 写法 (纯可执行文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# CMake 最低版本要求
cmake_minimum_required(VERSION 3.10)

# 项目名称
project(GreeterAppExecutable)

# 设置 C++ 标准 (可选,但推荐)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 添加一个名为 "greeter_app_exe" 的可执行文件
# 将所有相关的 .cpp 源文件都列出来
add_executable(greeter_app_exe
src/main.cpp
src/greeter.cpp
)

# 指定头文件的搜索路径
# 告诉 greeter_app_exe 这个目标,去 "include" 目录下查找头文件
# ${PROJECT_SOURCE_DIR} 是 CMake 内置变量,指向 CMakeLists.txt 所在的目录
target_include_directories(greeter_app_exe PUBLIC ${PROJECT_SOURCE_DIR}/include)

4. 小结 (纯可执行文件)

  • 优点:简单直接,易于理解,适合小型项目。
  • 缺点
    • 模块化差greeter 的功能和主程序耦合在一起。
    • 复用性低:如果其他项目想使用 greeter 的功能,就需要复制源代码,或者也采用类似的编译方式,不方便。
    • 编译效率:如果项目很大,修改任何一个 .cpp 文件都可能导致大量代码重新链接。

第二阶段:为什么要将 Greeter 拆分为库?

当项目逐渐变大,或者 greeter 的功能变得通用,希望被其他部分或其他项目复用时,将其拆分为一个独立的“库”就显得非常重要了。

  • 模块化 (Modularity)
    • greeter 作为一个库,封装了其特定的功能和实现。
    • 主程序或其他使用者只需要关心库提供的接口(头文件),而不需要关心其内部实现细节。
    • 使得项目结构更清晰,职责更分明。
  • 复用性 (Reusability)
    • 编译好的库可以被多个不同的可执行文件链接和使用,甚至可以分发给其他开发者。
    • 避免了代码重复。
  • 独立的编译单元 (Improved Compilation Speed)
    • 库可以被独立编译。
    • 如果库的代码没有改变,而只是修改了使用该库的主程序代码,那么在重新构建时,库不需要重新编译,只需要重新链接,这在大项目中可以显著提高编译速度。

第三阶段:Greeter 作为库 + 可执行文件

这是更推荐的、更符合软件工程实践的项目组织方式。

1. 文件结构

文件结构与第一阶段完全相同。我们改变的是 CMakeLists.txt 的构建逻辑。

1
2
3
4
5
6
7
greeter_project_library/
├── CMakeLists.txt
├── include/
│ └── greeter.h
└── src/
├── greeter.cpp
└── main.cpp

(C++ 代码也与第一阶段相同,此处不再重复列出,只需将 greeter.cpp 中输出的 “Executable version” 改为 “Library version” 以作区分即可)

src/greeter.cpp (修改版,仅为区分):

1
2
3
4
5
6
#include "greeter.h"
#include <iostream>

void Greeter::sayHello(const std::string& name) {
std::cout << "Hello, " << name << " from Greeter (Library version)!" << std::endl;
}

2. CMakeLists.txt 写法 (库 + 可执行文件)

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
# CMake 最低版本要求
cmake_minimum_required(VERSION 3.10)

# 项目名称
project(GreeterAppWithLibrary)

# 设置 C++ 标准
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# --- 步骤 1: 定义 Greeter 库 ---
# 使用 add_library() 创建一个名为 "greeter_lib" 的库
# 这个库由 src/greeter.cpp 编译而来
# 你可以选择 STATIC (静态库) 或 SHARED (共享库/动态库)
# 如果不指定,默认为 STATIC 或根据 BUILD_SHARED_LIBS 变量决定
add_library(greeter_lib src/greeter.cpp) # 默认为静态库

# 为 "greeter_lib" 库指定其公开的头文件目录
# PUBLIC 关键字非常重要:
# 1. greeter_lib 自身编译时会使用这个目录。
# 2. 任何链接到 greeter_lib 的目标也会自动获得这个头文件搜索路径。
target_include_directories(greeter_lib PUBLIC ${PROJECT_SOURCE_DIR}/include)


# --- 步骤 2: 定义使用该库的可执行文件 ---
# 创建一个名为 "app_uses_lib" 的可执行文件
# 注意,这里只包含主程序的源文件 main.cpp
add_executable(app_uses_lib src/main.cpp)


# --- 步骤 3: 链接可执行文件和库 ---
# 告诉 CMake,"app_uses_lib" 这个目标需要链接到 "greeter_lib" 库
# PRIVATE 表示链接关系仅对 app_uses_lib 自身有效
# 由于 greeter_lib 的 include 目录是 PUBLIC 的,app_uses_lib 会自动继承它,
# 所以 app_uses_lib 在编译 main.cpp 时能找到 "greeter.h"
target_link_libraries(app_uses_lib PRIVATE greeter_lib)

3. 小结 (库 + 可执行文件)

  • 定义库:使用 add_library(库名 源文件...)
  • 库的接口:使用 target_include_directories(库名 PUBLIC/INTERFACE 头文件目录) 来声明库对外暴露的头文件路径。PUBLIC 表示库自身编译和链接者都需要此目录;INTERFACE 链接者需要此目录。
  • 表示仅链接:使用 target_link_libraries(可执行文件名 PRIVATE/PUBLIC/INTERFACE 库名) 将可执行文件与库链接起来。
  • 优点
    • 高度模块化greeter_lib 是一个独立的、可复用的组件。
    • 清晰的依赖关系app_uses_lib 明确依赖 greeter_lib
    • 提升编译效率:如前所述。

总结

通过这个演进过程,我们可以看到 CMake 如何帮助我们管理从简单到复杂的项目结构。将功能模块化为库是 C++ 开发中的一个重要实践,它能带来更好的代码组织、复用性和维护性。CMake 提供了清晰的命令 (add_library, target_include_directories, target_link_libraries) 来支持这种模块化的开发方式。