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
) 来支持这种模块化的开发方式。