3、构建系统入门:CMake基础语法、目标定义、库与可执行文件的构建、跨平台编译配置
说实话,我刚入行那会儿,最头疼的就是构建系统。
那时候项目里还是手写 Makefile,每次加个源文件都得改半天。后来换了 CMake,感觉整个世界都清净了。嗯,今天咱们就来聊聊 CMake 这个老朋友。
3.1 CMake 到底是个啥?
CMake 不是编译器,也不是构建工具。它是个「构建系统生成器」。你想想看,你写一份 CMakeLists.txt,它就能帮你生成 Visual Studio 的 .sln、Linux 下的 Makefile、或者 Xcode 的项目文件。说白了,一次编写,到处编译。
我个人习惯把 CMake 比作「翻译官」。你跟它说人话(CMake 语法),它帮你翻译成各个平台能听懂的语言。
核心思想:CMake 的工作流程分两步:
- 配置阶段(Configure): 读取 CMakeLists.txt,检测编译器、库、头文件等
- 生成阶段(Generate): 根据配置结果,生成对应平台的构建文件
3.2 基础语法:从 Hello World 开始
咱们先来个最简单的例子。创建一个 main.cpp:
#include <iostream>
int main() {
std::cout << "Hello CMake!" << std::endl;
return 0;
}
然后在同目录下创建 CMakeLists.txt:
cmake_minimum_required(VERSION 3.10)
project(HelloCMake)
add_executable(hello main.cpp)
就这么三行,搞定。
cmake_minimum_required 指定最低版本。为什么要有这行?我记得有一次接手一个老项目,CMake 版本太低,新语法全不认识,编译报错一堆。从那以后,我每条 CMakeLists.txt 第一行必写这个。
project 定义项目名称,还会隐式设置一些变量,比如 PROJECT_NAME、PROJECT_SOURCE_DIR 等。
add_executable 就是告诉 CMake:「嘿,我要生成一个可执行文件,名字叫 hello,源文件是 main.cpp」。
3.3 目标定义:一切皆目标
CMake 里有个很重要的概念——目标(Target)。可执行文件、静态库、动态库,都是目标。
为什么要强调这个?因为现代 CMake 的核心理念就是「面向目标编程」。你给每个目标设置属性、链接依赖,CMake 会自动帮你传递这些属性。
我的经验:尽量用 target_include_directories、target_link_libraries 这类带 target_ 前缀的命令,而不是老式的 include_directories、link_libraries。前者是「目标级」的,更精确、更可控。
举个例子:
add_library(mylib STATIC mylib.cpp)
target_include_directories(mylib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE mylib)
这里 PUBLIC 和 PRIVATE 是关键。PUBLIC 表示:这个头文件路径不仅 mylib 自己用,链接 mylib 的目标(比如 myapp)也能用。PRIVATE 表示:只有我自己用,外部看不见。
为什么会这样设计?你想想看,如果你的库内部用了某个第三方头文件,但外部用户根本不需要知道,那就用 PRIVATE。这样接口更干净,编译也更快。
3.4 库与可执行文件的构建
咱们来点实际的。假设项目结构是这样的:
project/
├── CMakeLists.txt
├── src/
│ ├── main.cpp
│ └── utils.cpp
├── include/
│ └── utils.h
└── lib/
└── external/
├── CMakeLists.txt
└── external.cpp
根目录的 CMakeLists.txt:
cmake_minimum_required(VERSION 3.15)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)
# 设置 C++ 标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 添加子目录
add_subdirectory(lib/external)
# 创建静态库
add_library(utils STATIC src/utils.cpp)
target_include_directories(utils PUBLIC include)
# 创建可执行文件
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE utils external)
这里有几个点要注意:
set(CMAKE_CXX_STANDARD 17):指定 C++17 标准。我建议明确写出来,别依赖编译器默认值,不同平台默认值可能不一样。add_subdirectory:处理子目录中的 CMakeLists.txt,相当于递归构建。STATIC表示静态库,SHARED表示动态库。
我曾经踩过的坑:在 Windows 上构建动态库时,记得在导出类/函数前加上 __declspec(dllexport) 和 __declspec(dllimport)。CMake 提供了 GenerateExportHeader 模块可以自动处理,省心不少。
3.5 跨平台编译配置
跨平台是 CMake 的强项。但「跨平台」不是说写一份 CMakeLists.txt 就万事大吉了,你得考虑不同平台的差异。
常见的跨平台问题:
| 问题 | 解决方案 |
|---|---|
| 平台特定的源文件 | 使用 if(WIN32)、if(APPLE)、if(UNIX) 条件判断 |
| 不同编译器标志 | 使用 target_compile_options 配合生成器表达式 |
| 第三方库查找 | 使用 find_package,配合 CMAKE_PREFIX_PATH |
| 安装路径差异 | 使用 GNUInstallDirs 模块 |
来看个实际例子:
add_executable(myapp main.cpp)
# Windows 需要额外链接 ws2_32
if(WIN32)
target_link_libraries(myapp PRIVATE ws2_32)
endif()
# macOS 需要链接 CoreFoundation
if(APPLE)
target_link_libraries(myapp PRIVATE "-framework CoreFoundation")
endif()
# 编译器警告选项
target_compile_options(myapp PRIVATE
$<$<CXX_COMPILER_ID:MSVC>:/W4>
$<$<CXX_COMPILER_ID:GNU>:-Wall -Wextra>
$<$<CXX_COMPILER_ID:Clang>:-Wall -Wextra>
)
这里用了生成器表达式(Generator Expression),就是 $<...> 这种语法。它会在生成阶段才求值,非常灵活。比如 $<$<CXX_COMPILER_ID:MSVC>:/W4> 的意思是:如果编译器是 MSVC,就添加 /W4 标志。
我个人习惯:把跨平台相关的逻辑单独放到一个 cmake/ 目录下,用 include() 引入。这样主 CMakeLists.txt 看起来清爽很多。
3.6 知识体系总览
下面这张图总结了 CMake 的核心知识结构,我画了好一会儿,希望能帮你理清思路:
3.7 实用技巧与避坑指南
最后分享几个我实际项目中用到的技巧:
- 使用
CMAKE_BUILD_TYPE:调试和发布用不同的编译选项。比如cmake -DCMAKE_BUILD_TYPE=Debug ..。 - 善用
add_subdirectory:大型项目按模块拆分子目录,每个子目录有自己的 CMakeLists.txt,清晰好维护。 - 别把路径写死:用
${CMAKE_SOURCE_DIR}、${CMAKE_CURRENT_SOURCE_DIR}这类变量,而不是硬编码路径。 - 生成器表达式是你的朋友:虽然语法丑了点,但功能强大,值得花时间学会。
我曾经踩过的坑:有一次在 Linux 上编译好好的,换到 Windows 上就报链接错误。查了半天,发现是静态库和动态库混用了。记住:同一个项目里,要么全静态链接,要么全动态链接,混着来容易出问题。
嗯,CMake 入门就聊到这儿。这东西上手不难,但想用好确实需要积累。多写几个项目,慢慢就熟练了。
可可精品资料库