7、目标与属性:构建系统的基本单元

说实话,CMake 里最核心的概念就是「目标」(Target)。

我刚开始用 CMake 时,以为它就是个高级的 Makefile 生成器。后来才发现,真正让 CMake 强大的,是它这套基于目标的设计哲学。你想想看,一个项目少则几个文件,多则成百上千个模块,如果没有清晰的目标管理,那编译链接就是一团乱麻。

7.1 什么是目标?

目标,说白了就是你要构建的东西。它可以是:

  • 一个库(静态库或动态库)
  • 一个可执行程序
  • 一个自定义的构建动作

每个目标都有自己的属性。比如头文件搜索路径、链接的库、编译选项等等。这些属性可以层层传递,形成依赖关系。

核心思想:CMake 的目标系统是「声明式」的。你只需要告诉它「我要什么」,它自己会推导出「怎么做」。

CMake 目标系统 add_library add_executable 目标属性 STATIC / SHARED / MODULE OBJECT 库(CMake 3.12+) INTERFACE 头文件库 可执行程序(.exe / 二进制) 测试程序 / 工具链 target_include_directories target_link_libraries target_compile_definitions 属性通过 PUBLIC / PRIVATE / INTERFACE 控制传递范围

7.2 add_library:创建库目标

这是最常用的命令之一。我个人的习惯是:只要代码可能被复用,就做成库。

# 最简单的静态库
add_library(my_lib STATIC
    src/utils.cpp
    src/core.cpp
)

# 动态库
add_library(my_shared SHARED
    src/plugin.cpp
)

# 头文件库(没有源文件,只有头文件)
add_library(my_headers INTERFACE)
target_include_directories(my_headers INTERFACE include/)

我的经验:INTERFACE 库是个好东西。它不产生任何二进制文件,只传递头文件和编译选项。我经常用它来管理第三方头文件依赖。

库的类型有三种:

类型 说明 适用场景
STATIC 静态库(.a / .lib) 小模块、内部使用
SHARED 动态库(.so / .dll) 插件、公共接口
INTERFACE 纯头文件库 模板库、头文件依赖

注意:Windows 下动态库需要导出符号。我曾经踩过这个坑——在 Linux 上编译好好的,到 Windows 上链接报错。后来加了 __declspec(dllexport) 才解决。

7.3 add_executable:创建可执行文件

这个命令更直接——生成一个能跑的程序。

# 最简单的可执行文件
add_executable(my_app
    src/main.cpp
    src/app.cpp
)

# 也可以只写 main.cpp,其他源文件通过库引入
add_executable(my_app src/main.cpp)
target_link_libraries(my_app PRIVATE my_lib)

我个人建议:把业务逻辑放在库里,main 函数只做启动和组装。这样测试起来方便很多。

7.4 target_include_directories:管理头文件搜索路径

这个命令用来告诉编译器:「头文件在哪里找」。

# 语法
target_include_directories(target 
    <PUBLIC|PRIVATE|INTERFACE> 
    dir1 dir2 ...
)

# 实际例子
target_include_directories(my_lib
    PUBLIC  include/    # 外部也能用
    PRIVATE src/        # 只有自己用
)

这里的关键是访问权限:

  • PUBLIC:当前目标用,链接它的目标也能用
  • PRIVATE:只有当前目标用
  • INTERFACE:当前目标不用,但链接它的目标要用

避坑指南:我曾经把应该 PRIVATE 的路径设成了 PUBLIC,结果下游目标莫名其妙地找到了不该找的头文件,编译报错查了半天。记住:能 PRIVATE 就别 PUBLIC。

7.5 target_link_libraries:链接依赖

这是把目标串联起来的关键命令。

# 语法
target_link_libraries(target 
    <PUBLIC|PRIVATE|INTERFACE> 
    lib1 lib2 ...
)

# 例子
target_link_libraries(my_app
    PRIVATE
        my_lib
        fmt::fmt
)

链接的传递规则和头文件一样:

  • PRIVATE:只给自己用
  • PUBLIC:给自己用,也传给下游
  • INTERFACE:自己不链接,但下游必须链接

我的习惯:大部分情况下用 PRIVATE。只有当你明确知道「这个库的接口暴露了另一个库的类型」时,才用 PUBLIC。比如你的库返回了一个 std::string,那下游自然需要链接 C++ 标准库——但标准库通常不用你操心。

7.6 实战:一个完整的小项目

把上面这些串起来,看看实际项目怎么写:

# CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)

# 核心库
add_library(core STATIC
    src/core/engine.cpp
    src/core/manager.cpp
)
target_include_directories(core
    PUBLIC  include/
    PRIVATE src/
)

# 工具库(头文件库)
add_library(utils INTERFACE)
target_include_directories(utils INTERFACE include/utils/)

# 主程序
add_executable(my_app src/main.cpp)
target_link_libraries(my_app
    PRIVATE
        core
        utils
)

这个结构很清晰:

  • core 是静态库,对外暴露 include/ 下的头文件
  • utils 是头文件库,只传递头文件路径
  • my_app 链接了这两个库,就能用它们的接口了

重要提醒:链接顺序在有些编译器上很敏感。比如 GCC,如果你把 my_app 写在 core 前面,可能会链接失败。CMake 的 target_link_libraries 会自动处理顺序,但如果你手写 Makefile 就要小心了。

7.7 总结

目标与属性是 CMake 的基石。你只要记住:

  • add_libraryadd_executable 创建目标
  • target_include_directories 管理头文件
  • target_link_libraries 管理依赖
  • 权限控制用 PUBLIC / PRIVATE / INTERFACE

嗯,掌握了这些,你已经能写出像样的 CMake 项目了。剩下的就是多练、多踩坑、多总结。