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)。后面的源文件列表,你可以一个一个列,也可以用变量。
举个例子:
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
)
__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 自己能用。
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 知识体系图
下面这张图帮你理清本章的核心逻辑:
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 的基石,理解透了,后面的模块化构建和依赖管理就顺理成章了。