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_INTERFACEINSTALL_INTERFACE)是从这个版本开始稳定的。
  • project:声明项目名称、版本、语言。这里 LANGUAGES CXX 表示我们只用 C++,如果你混用 C 和 C++,可以写成 LANGUAGES C CXX
  • set:设置变量。CMAKE_CXX_STANDARD 是 CMake 内置变量,用来控制 C++ 标准。我一般设成 17,因为 C++17 的 std::filesystemstd::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 本身生效,不会传递给依赖它的其他目标。如果你想让头文件路径也暴露给依赖者,就用 PUBLICINTERFACE

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)。我遇到过很多新手问:“到底该用静态还是动态?” 我的建议是:

  • 如果你在写一个工具库,希望用户直接链接到自己的可执行文件里,用静态库。
  • 如果你在写一个插件系统,或者需要运行时加载,用动态库。
  • 如果你不确定,先用静态库。静态库的链接问题比动态库少得多。
注意: 在 Windows 上,动态库(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()

这里 WIN32APPLEUNIX 是 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 构建系统的核心逻辑。你可以看到,从项目声明开始,到目标定义,再到跨平台选项,最后生成构建文件,整个流程是清晰的。

CMake 构建系统核心逻辑 cmake_minimum_required + project set(CMAKE_CXX_STANDARD ...) add_executable / add_library (STATIC / SHARED) if(WIN32/APPLE/UNIX) + target_compile_options/link_libraries cmake --build .

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,把警告当作错误。我建议在开发阶段开启,能帮你提前发现很多问题。
  • PRIVATE vs PUBLICtarget_include_directories 里,include 是公开头文件,所以用 PUBLICsrc 是内部实现,用 PRIVATE
核心要点: CMake 的跨平台能力,不是靠“自动检测”实现的,而是靠你手动编写条件判断。你写得越细致,项目就越健壮。不要指望 CMake 帮你搞定一切。

2.6 避坑指南

最后,分享几个我踩过的坑:

  • 路径分隔符:Windows 用反斜杠 \,Linux 用正斜杠 /。CMake 会自动处理,但如果你在代码里硬编码路径,记得用 /,因为 CMake 内部统一用正斜杠。
  • 静态库的链接顺序:GCC 和 MSVC 对静态库的链接顺序要求不同。GCC 要求被依赖的库放在后面,MSVC 则无所谓。如果你遇到“未定义的引用”错误,试试调整 target_link_libraries 的顺序。
  • 不要用 add_definitions:这个命令已经过时了。用 target_compile_definitions 代替,它能精确控制宏定义的作用范围。

好了,这一章的内容就到这里。CMake 的语法其实不复杂,但它的设计思想——以目标为中心、显式声明依赖——是值得你花时间理解的。下一章我们会深入 CMake 的模块系统,看看怎么用 find_package 来管理第三方库。


公众号:蓝海资料掘金营,微信deep3321