2. CMake构建系统:CMake基础语法、目标定义、库与可执行文件、跨平台编译选项
好,咱们直接进入正题。CMake 这东西,说白了就是一套帮你管理 C++ 项目的“脚本语言”。你想想看,一个项目要在 Windows、Linux、macOS 上都能编译,总不能每个平台都手写一套 Makefile 吧?那太累了。CMake 就是来解决这个问题的。
我个人习惯,不管项目多小,第一件事就是写一个 CMakeLists.txt。哪怕只是一个 hello world,我也用 CMake 来构建。为什么?因为一旦你习惯了这种“声明式”的构建方式,后面加库、加模块、加跨平台逻辑,都会非常顺手。
2.1 CMake 基础语法:项目声明与变量
先看一个最简单的例子。你新建一个项目,根目录下放一个 CMakeLists.txt,内容大概是这样:
cmake_minimum_required(VERSION 3.16)
project(MyApp VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_executable(my_app main.cpp utils.cpp)
这里我解释几个关键点:
cmake_minimum_required:指定 CMake 的最低版本。我建议你至少用 3.16,因为很多好用的特性(比如BUILD_INTERFACE、INSTALL_INTERFACE)是从这个版本开始稳定的。project:声明项目名称、版本、语言。这里LANGUAGES CXX表示我们只用 C++,如果你混用 C 和 C++,可以写成LANGUAGES C CXX。set:设置变量。CMAKE_CXX_STANDARD是 CMake 内置变量,用来控制 C++ 标准。我一般设成 17,因为 C++17 的std::filesystem和std::optional实在太香了。
CMAKE_CXX_STANDARD_REQUIRED 设为 ON。这样如果编译器不支持 C++17,CMake 会直接报错,而不是偷偷降级。我曾经在一个项目里吃过这个亏——编译器不支持 C++17,但 CMake 没报错,结果编译出来的行为完全不对,排查了半天。
2.2 目标定义:可执行文件与库
CMake 的核心概念是“目标”(target)。目标可以是可执行文件,也可以是库。你想想看,一个大型项目里,可能有几十个可执行文件和几十个库,每个都有自己的编译选项、依赖关系。CMake 通过目标来管理这一切。
2.2.1 可执行文件
add_executable(my_app main.cpp)
target_include_directories(my_app PRIVATE include)
target_link_libraries(my_app PRIVATE my_lib)
这里 PRIVATE 表示这些设置只对 my_app 本身生效,不会传递给依赖它的其他目标。如果你想让头文件路径也暴露给依赖者,就用 PUBLIC 或 INTERFACE。
2.2.2 库:静态库与动态库
add_library(my_lib STATIC src/lib.cpp include/lib.h)
add_library(my_shared_lib SHARED src/shared.cpp include/shared.h)
STATIC 生成静态库(.a 或 .lib),SHARED 生成动态库(.so 或 .dll)。我遇到过很多新手问:“到底该用静态还是动态?” 我的建议是:
- 如果你在写一个工具库,希望用户直接链接到自己的可执行文件里,用静态库。
- 如果你在写一个插件系统,或者需要运行时加载,用动态库。
- 如果你不确定,先用静态库。静态库的链接问题比动态库少得多。
__declspec(dllexport) 和 __declspec(dllimport)。CMake 有一个 GenerateExportHeader 模块可以帮你自动生成这些宏,我后面会讲到。
2.3 跨平台编译选项
这才是 CMake 的强项。不同平台有不同的编译器、不同的链接器、不同的系统库。CMake 提供了一套机制来统一处理。
2.3.1 平台判断
if(WIN32)
target_compile_definitions(my_app PRIVATE PLATFORM_WINDOWS)
elseif(APPLE)
target_compile_definitions(my_app PRIVATE PLATFORM_MACOS)
elseif(UNIX)
target_compile_definitions(my_app PRIVATE PLATFORM_LINUX)
endif()
这里 WIN32、APPLE、UNIX 是 CMake 内置的变量。注意 WIN32 在 64 位 Windows 上也是 TRUE,别被名字骗了。
2.3.2 编译器判断
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
target_compile_options(my_app PRIVATE -Wall -Wextra)
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
target_compile_options(my_app PRIVATE -Wall -Wextra -Wpedantic)
elseif(MSVC)
target_compile_options(my_app PRIVATE /W4)
endif()
嗯,这里要注意:MSVC 的警告选项用的是 /W4,而 GCC/Clang 用的是 -Wall。我曾经在一个项目里直接把 GCC 的编译选项扔给 MSVC,结果编译报了一堆莫名其妙的错误。所以,跨平台编译选项一定要分开写。
2.3.3 系统库链接
if(WIN32)
target_link_libraries(my_app PRIVATE ws2_32)
elseif(UNIX)
target_link_libraries(my_app PRIVATE pthread)
endif()
Windows 上的 socket 库是 ws2_32,Linux 上的线程库是 pthread。这些差异 CMake 不会自动帮你处理,你得自己判断。
2.4 知识体系图
下面这张图展示了 CMake 构建系统的核心逻辑。你可以看到,从项目声明开始,到目标定义,再到跨平台选项,最后生成构建文件,整个流程是清晰的。
2.5 实战经验:一个跨平台网络库的 CMake 配置
我拿一个实际项目举例。之前我写过一个跨平台的网络库,需要在 Windows 上用 winsock2,在 Linux 上用 posix socket。CMake 配置大概是这样的:
cmake_minimum_required(VERSION 3.16)
project(NetLib VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_library(netlib STATIC
src/socket.cpp
include/netlib/socket.h
)
target_include_directories(netlib
PUBLIC include
PRIVATE src
)
# 跨平台:选择 socket 实现
if(WIN32)
target_compile_definitions(netlib PRIVATE USE_WINSOCK)
target_link_libraries(netlib PRIVATE ws2_32)
elseif(UNIX)
target_compile_definitions(netlib PRIVATE USE_POSIX_SOCKET)
target_link_libraries(netlib PRIVATE pthread)
endif()
# 编译器警告
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
target_compile_options(netlib PRIVATE -Wall -Wextra -Werror)
elseif(MSVC)
target_compile_options(netlib PRIVATE /W4 /WX)
endif()
这里有几个点值得注意:
target_compile_definitions:通过宏定义来区分平台,这样在 C++ 代码里就可以用#ifdef USE_WINSOCK来写平台相关的代码。/WX:MSVC 的/WX相当于 GCC 的-Werror,把警告当作错误。我建议在开发阶段开启,能帮你提前发现很多问题。PRIVATEvsPUBLIC:target_include_directories里,include是公开头文件,所以用PUBLIC;src是内部实现,用PRIVATE。
2.6 避坑指南
最后,分享几个我踩过的坑:
- 路径分隔符:Windows 用反斜杠
\,Linux 用正斜杠/。CMake 会自动处理,但如果你在代码里硬编码路径,记得用/,因为 CMake 内部统一用正斜杠。 - 静态库的链接顺序:GCC 和 MSVC 对静态库的链接顺序要求不同。GCC 要求被依赖的库放在后面,MSVC 则无所谓。如果你遇到“未定义的引用”错误,试试调整
target_link_libraries的顺序。 - 不要用
add_definitions:这个命令已经过时了。用target_compile_definitions代替,它能精确控制宏定义的作用范围。
好了,这一章的内容就到这里。CMake 的语法其实不复杂,但它的设计思想——以目标为中心、显式声明依赖——是值得你花时间理解的。下一章我们会深入 CMake 的模块系统,看看怎么用 find_package 来管理第三方库。
公众号:蓝海资料掘金营,微信deep3321