4. 目标与属性:add_executable 与 add_library、目标属性、设置包含目录与链接库

这一章我们聊聊 CMake 里最核心的两个概念——目标(Target)和属性(Property)。说白了,整个构建系统就是围绕目标展开的。你定义一个可执行文件,或者一个库,CMake 就知道该编译哪些源文件、怎么链接、头文件去哪找。

我刚开始用 CMake 时,总觉得写几个 add_executable 就完事了。后来项目大了才发现,目标属性才是真正的精髓。你想想看,一个库要暴露头文件路径,另一个可执行文件要链接它,这些信息怎么传递?靠的就是目标属性和 target_ 系列命令。

4.1 定义目标:add_executable 与 add_library

这两个命令是 CMake 的起点。一个项目里,要么生成可执行文件,要么生成库文件,或者两者都有。

4.1.1 add_executable

语法很简单:

add_executable(<name> [WIN32] [MACOSX_BUNDLE]
               [EXCLUDE_FROM_ALL]
               [source1] [source2 ...])

<name> 就是目标的名字,也是最终生成的可执行文件名(在 Windows 上会自动加 .exe)。后面的源文件列表,你可以一个一个列,也可以用变量。

我的习惯:源文件尽量用变量或者 GLOB 收集,别硬编码在 add_executable 里。否则加一个文件就要改 CMakeLists.txt,太容易漏了。

举个例子:

set(MY_SOURCES
    src/main.cpp
    src/utils.cpp
    src/network.cpp
)

add_executable(my_app ${MY_SOURCES})

如果你在 Windows 上开发 GUI 程序,可以加上 WIN32 参数,这样就不会弹出控制台窗口。我在做跨平台工具时用过这个,省得用户看到黑框一闪而过。

4.1.2 add_library

库有三种类型:STATIC、SHARED 和 MODULE。语法和可执行文件类似:

add_library(<name> [STATIC | SHARED | MODULE]
            [EXCLUDE_FROM_ALL]
            [source1] [source2 ...])
类型 说明 生成文件
STATIC 静态库,编译时直接链接进目标 .a (Unix) / .lib (Windows)
SHARED 动态库,运行时加载 .so (Unix) / .dll (Windows)
MODULE 插件式动态库,不直接链接,用 dlopen 加载 .so (Unix) / .dll (Windows)

不指定类型时,CMake 默认生成 STATIC 库。我个人建议显式写清楚,别让读代码的人猜。

add_library(my_lib STATIC
    src/lib.cpp
    src/helper.cpp
)
注意:SHARED 库在 Windows 上需要导出符号,否则外部代码链接时会报 unresolved external symbol。我踩过这个坑,后来老老实实加了 __declspec(dllexport)。

4.2 目标属性(Target Properties)

每个目标都有一堆属性,控制着编译、链接、安装等行为。你可以用 set_target_properties 来设置,也可以用 get_target_property 来读取。

常用的属性有哪些?我列几个你一定会碰到的:

属性名 作用 示例值
CXX_STANDARD 指定 C++ 标准版本 17, 20
CXX_STANDARD_REQUIRED 是否强制要求该标准 ON, OFF
COMPILE_DEFINITIONS 编译时预定义宏 DEBUG=1
COMPILE_OPTIONS 编译器选项 -Wall, -O2
LINK_LIBRARIES 链接的库 m, pthread
OUTPUT_NAME 生成的文件名(不含后缀) my_app
FOLDER IDE 中分组显示(VS/Xcode) Libs

举个例子,设置 C++17 标准:

set_target_properties(my_app PROPERTIES
    CXX_STANDARD 17
    CXX_STANDARD_REQUIRED ON
    CXX_EXTENSIONS OFF
)

这里 CXX_EXTENSIONS OFF 表示禁用编译器扩展(比如 GCC 的 -std=gnu++17),保证代码严格符合标准。我在做跨平台项目时,这个属性帮我避免了不少编译器差异带来的问题。

4.3 设置包含目录与链接库

这部分是日常开发中最频繁的操作。CMake 提供了 target_ 系列命令,专门用来管理目标的依赖关系。

4.3.1 target_include_directories

告诉编译器去哪里找头文件。语法:

target_include_directories(<target> [SYSTEM] [AFTER|BEFORE]
    <INTERFACE|PUBLIC|PRIVATE> [items1...]
    [<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])

这里的关键是 可见性(Visibility):

  • PRIVATE:只对当前目标生效,不传递给依赖者。
  • PUBLIC:对当前目标生效,也传递给依赖者。
  • INTERFACE:不对当前目标生效,只传递给依赖者(常用于纯头文件库)。

举个例子:

add_library(my_lib STATIC src/lib.cpp)
target_include_directories(my_lib
    PUBLIC include
    PRIVATE src
)

add_executable(my_app src/main.cpp)
target_link_libraries(my_app PRIVATE my_lib)

这里 include 目录是 PUBLIC,所以 my_app 也能找到 my_lib 的公开头文件。而 src 目录是 PRIVATE,只有 my_lib 自己能用。

核心原则:能用 PUBLIC 就别用 INTERFACE,能用 PRIVATE 就别用 PUBLIC。暴露最少的信息,减少耦合。

4.3.2 target_link_libraries

链接库的命令,同样有可见性:

target_link_libraries(<target>
    <PRIVATE|PUBLIC|INTERFACE> <item>...
    [<PRIVATE|PUBLIC|INTERFACE> <item>... ...])

比如:

add_library(engine STATIC engine.cpp)
target_link_libraries(engine
    PUBLIC math_utils
    PRIVATE logger
)

add_executable(game main.cpp)
target_link_libraries(game PRIVATE engine)

这里 engine 公开依赖 math_utils,所以 game 也会链接 math_utils。而 logger 是私有依赖,game 完全不知道它的存在。

我曾经在一个大型项目里,因为 PUBLIC 滥用导致链接了十几个不必要的库,编译慢得要命。后来花了一周重构,把大部分改成 PRIVATE,编译时间直接砍半。

4.4 知识体系图

下面这张图帮你理清本章的核心逻辑:

CMake 目标与属性知识体系 CMake 目标与属性核心逻辑 目标定义 add_executable add_library STATIC SHARED MODULE 目标属性(Target Properties) CXX_STANDARD / COMPILE_DEFINITIONS OUTPUT_NAME / LINK_LIBRARIES target_include_directories / target_link_libraries 通过 PUBLIC/PRIVATE/INTERFACE 控制依赖传递

4.5 实战建议

说了这么多,我总结几条实战中容易踩的坑:

  • 别用全局的 include_directories 和 link_libraries。它们会影响所有目标,破坏模块化。用 target_ 系列命令,精确控制每个目标的依赖。
  • PUBLIC 要克制。每次用 PUBLIC 之前问自己:这个依赖真的需要暴露给下游吗?如果不是,用 PRIVATE。
  • INTERFACE 库很香。对于纯头文件库(比如 Eigen、Catch2),用 add_library(my_header_only INTERFACE),然后 target_include_directories(my_header_only INTERFACE include)。这样下游链接时自动获得头文件路径。
  • 属性继承要小心。比如你给一个库设置了 CXX_STANDARD 17,但链接它的可执行文件可能还是 C++14。这时候要么统一设置,要么用 target_compile_features 来要求特定特性。
一个小技巧:用 cmake --build . --target <name> -- -n 可以查看实际编译命令,帮你调试包含目录和链接库是否正确。我在排查链接错误时经常用这招。

嗯,这一章的内容就到这里。目标与属性是 CMake 的基石,理解透了,后面的模块化构建和依赖管理就顺理成章了。