导语
在前面的章节中,我们已经掌握了 CMake 的基础语法、构建流程以及环境配置。从本章开始,我们将正式进入 Modern CMake 的核心地带——目标(Target)。如果说变量是 CMake 脚本语言的“词汇”,那么目标就是构建系统的“主语”。在 Modern CMake 的范式中,我们不再通过反复操作全局变量来控制编译,而是围绕目标组织源代码、管理依赖关系、传递编译选项。
理解目标的概念与类型,是你从“会写 CMakeLists.txt”迈向“精通 CMake 构建设计”的关键一步。本节将系统讲解 CMake 中所有目标类型,包括可执行文件、各类库(静态/动态/模块)、对象库、接口库,以及导入目标和别名目标。每个知识点都会配以完整的代码示例,请务必跟随动手实践。
一、什么是目标(Target)
在 CMake 中,目标(Target)是构建系统生成的基本单元,它代表一个最终会被构建出来的实体——可能是一个可执行文件、一个库文件,或者是一个纯逻辑层面的配置集合。
目标的核心特征在于:它是属性(Properties)的载体。源码文件、头文件搜索路径、编译宏、链接库等信息,都会以属性的形式附加在目标上。Modern CMake 的核心理念就是“一切围绕目标转”,尽量避免全局变量,让依赖关系通过目标属性自动传播。
二、可执行文件目标:add_executable
可执行文件目标是最直观的目标类型,它最终会产生一个可以运行的程序。
2.1 基本语法
add_executable(<name> [source1] [source2 ...])
这是最常用的形式。<name> 既是构建目标的逻辑名称,也会作为生成的可执行文件的默认文件名。
2.2 完整语法与选项
add_executable(<name> [WIN32] [MACOSX_BUNDLE]
[EXCLUDE_FROM_ALL]
[source1] [source2 ...])
WIN32:在 Windows 平台上,表示构建 GUI 应用程序(链接时会使用/SUBSYSTEM:WINDOWS而非CONSOLE),且会自动生成一个main的入口点适配。MACOSX_BUNDLE:在 macOS 上,将可执行文件打包为.appBundle 格式。EXCLUDE_FROM_ALL:将该目标排除在默认构建目标之外。只有当显式构建该目标,或被其他目标依赖时,它才会被编译。这非常适合构建测试程序或示例程序。
2.3 代码示例
cmake_minimum_required(VERSION 3.20)
project(TargetDemo LANGUAGES CXX)
# 1. 基础可执行文件
add_executable(hello_app main.cpp utils.cpp)
# 2. 排除在默认构建之外的工具程序
add_executable(data_generator helper.cpp
EXCLUDE_FROM_ALL
)
# 3. Windows GUI 程序
if(WIN32)
add_executable(gui_app WIN32 main_gui.cpp resources.rc)
endif()
# 4. macOS Bundle
if(APPLE)
add_executable(MyBundle MACOSX_BUNDLE main_mac.mm)
endif()
三、库目标类型:add_library
库(Library)是代码复用的主要形式。CMake 通过 add_library 支持三种实体库类型,以及后续要讲到的对象库和接口库。
3.1 静态库(STATIC)
静态库在 Linux/macOS 下通常生成 .a 文件,在 Windows 下生成 .lib 文件。链接静态库时,所需的代码会被复制到最终的可执行文件中。
add_library(math STATIC math.cpp algebra.cpp)
3.2 动态库 / 共享库(SHARED)
动态库在 Linux 下为 .so,macOS 下为 .dylib,Windows 下为 .dll(配合导入库 .lib)。动态库在运行时被加载,多个程序可以共享同一份库文件,节省磁盘和内存空间。
add_library(network SHARED socket.cpp http.cpp)
3.3 模块库(MODULE)
模块库本质上也是一种动态库,但它不参与链接阶段,通常用于运行时动态加载(如插件机制,通过 dlopen / LoadLibrary 加载)。在某些平台上,模块库和共享库的编译选项可能不同。
add_library(my_plugin MODULE plugin.cpp)
3.4 不指定类型时的默认行为
如果在 add_library 中不指定类型,CMake 会检查变量 BUILD_SHARED_LIBS:
- 如果
BUILD_SHARED_LIBS为ON,则默认生成SHARED库; - 否则默认生成
STATIC库。
# 命令行配置时设置: cmake -B build -DBUILD_SHARED_LIBS=ON
add_library(utils math.cpp string.cpp) # 类型由 BUILD_SHARED_LIBS 决定
3.5 三种实体库对比示例
cmake_minimum_required(VERSION 3.20)
project(LibraryTypes LANGUAGES CXX)
# 静态库
add_library(calc STATIC calc.cpp)
# 动态库
add_library(logger SHARED logger.cpp)
# 插件模块
add_library(effects MODULE effects.cpp)
# 主程序分别链接静态库和动态库
add_executable(main_app main.cpp)
target_link_libraries(main_app PRIVATE calc logger)
注意:虽然 main_app 链接了 logger(动态库),但 effects(MODULE)不会自动被链接,它需要你在代码中通过平台相关的 API 手动加载。
四、对象库:OBJECT
对象库是一种特殊的目标,它不会生成最终的库文件(既不是 .a 也不是 .so),而是仅将源代码编译成对象文件(.o 或 .obj)并打包在内部供复用。
4.1 适用场景
对象库非常适合以下场景:
- 一组源文件需要在多个目标(可执行文件或其他库)中复用,但你不想生成一个独立的静态库文件;
- 你想避免静态库链接时可能带来的符号重复或链接顺序问题;
- 构建大型项目时,将公共代码编译为对象文件直接嵌入最终目标。
4.2 定义与使用
add_library(common_objs OBJECT file1.cpp file2.cpp)
# 方式1:在可执行文件中复用对象文件
add_executable(app1 main1.cpp)
target_sources(app1 PRIVATE $)
# 方式2:在另一个库中复用对象文件
add_library(combined STATIC combined.cpp)
target_sources(combined PRIVATE $)
这里使用了生成器表达式(Generator Expression) $<TARGET_OBJECTS:common_objs>,它会在构建时将对象库包含的对象文件列表展开。
4.3 对象库的局限性
- 对象库不能像普通库那样直接用于
target_link_libraries(虽然 CMake 3.12+ 支持链接对象库,但底层逻辑本质还是对象文件传递); - 对象库的属性传播能力不如普通库,比如包含目录不会自动传递,需要额外处理。
# CMake 3.12+ 支持直接链接对象库(推荐在新版本中使用)
add_executable(modern_app main.cpp)
target_link_libraries(modern_app PRIVATE common_objs)
五、接口库:INTERFACE
接口库是 Modern CMake 中极具创造力的设计。它不产生任何编译输出(没有实体文件生成),纯粹作为一个逻辑目标存在,用于传递编译配置(头文件路径、宏定义、编译选项等)。
5.1 纯头文件库(Header-Only Library)
纯头文件库没有 .cpp 实现文件,所有代码都在 .h 或 .hpp 中。使用接口库可以完美地建模这类库:
add_library(my_headers INTERFACE)
# 为接口库附加属性
target_include_directories(my_headers INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/include
)
target_compile_definitions(my_headers INTERFACE
USE_FAST_ALGORITHM=1
)
# 其他目标链接该接口库后,会自动获得上述配置
add_executable(consumer main.cpp)
target_link_libraries(consumer PRIVATE my_headers)
关键点在于使用 INTERFACE 可见性。这表示这些属性只用于依赖该目标的其他目标,而接口库自身不需要编译(因为它没有源文件)。
5.2 作为配置集合
接口库还可以用来封装一组通用的编译警告或选项,方便在多个目标间复用:
add_library(project_warnings INTERFACE)
if(MSVC)
target_compile_options(project_warnings INTERFACE /W4 /WX)
else()
target_compile_options(project_warnings INTERFACE -Wall -Wextra -Wpedantic)
endif()
# 所有项目目标统一链接此接口库
add_executable(app main.cpp)
target_link_libraries(app PRIVATE project_warnings)
六、导入目标(IMPORTED)与别名目标(ALIAS)
6.1 导入目标:IMPORTED
导入目标用于引用已经构建好的外部库(非当前 CMake 项目生成)。它告诉 CMake:“这个库已经存在于系统某处,你只需要知道如何链接它即可。” 这在 find_package 查找系统库时非常常见。
# 手动创建一个导入的静态库目标
add_library(external_lib STATIC IMPORTED)
# 指定库文件的实际位置
set_target_properties(external_lib PROPERTIES
IMPORTED_LOCATION "/usr/local/lib/libexternal.a"
INTERFACE_INCLUDE_DIRECTORIES "/usr/local/include"
)
# 使用它
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE external_lib)
对于动态库,如果平台需要(如 Windows),还需要设置 IMPORTED_IMPLIB(指向导入库 .lib)和 IMPORTED_LOCATION(指向 .dll)。
6.2 全局导入目标
默认情况下,导入目标的作用域仅限于当前目录及其子目录。如果希望在整个构建树中都能引用,需要添加 GLOBAL 关键字:
add_library(png SHARED IMPORTED GLOBAL)
set_target_properties(png PROPERTIES
IMPORTED_LOCATION "${PNG_LIBRARY}"
INTERFACE_INCLUDE_DIRECTORIES "${PNG_INCLUDE_DIR}"
)
6.3 别名目标:ALIAS
别名目标是为已有的目标创建一个额外的名称。它本身不生成新的构建规则,只是原目标的“快捷方式”。
add_library(real_math STATIC math.cpp)
# 创建别名
add_library(Math::Core ALIAS real_math)
# 使用别名链接(命名空间风格,更具可读性)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE Math::Core)
别名目标有两个重要限制:
- 只能为库目标创建别名(不可用于可执行文件);
- 不能为
IMPORTED库创建别名(但可以为ALIAS本身再创建引用,需注意版本限制)。
七、目标属性概览
每种目标类型都附带有大量属性,控制其编译、链接和输出行为。掌握如何查看和设置属性,是调试 CMake 构建的关键技能。
7.1 查看目标属性
使用 get_target_property 命令读取属性值。如果属性未设置,结果会落入 <variable>-NOTFOUND。
add_executable(demo main.cpp)
# 查看目标的源文件列表
get_target_property(SRCS demo SOURCES)
message(STATUS "demo sources: ${SRCS}")
# 查看输出名称
get_target_property(OUT_NAME demo OUTPUT_NAME)
if(OUT_NAME)
message(STATUS "demo output name: ${OUT_NAME}")
else()
message(STATUS "demo uses default output name")
endif()
7.2 设置目标属性
使用 set_target_properties 可以同时设置多个属性:
add_library(engine SHARED engine.cpp renderer.cpp)
set_target_properties(engine PROPERTIES
# 输出文件名
OUTPUT_NAME "MyGameEngine"
# Windows 运行时输出目录(.exe, .dll)
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
# Linux/macOS 动态库输出目录(.so, .dylib)
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib"
# 静态库/导入库输出目录(.a, .lib)
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib"
# C++ 标准
CXX_STANDARD 17
CXX_STANDARD_REQUIRED ON
# 位置无关代码(对动态库很重要)
POSITION_INDEPENDENT_CODE ON
)
7.3 常用目标属性速查表
下表列举了日常开发中最常用的目标属性:
| 属性名 | 说明 |
|---|---|
SOURCES |
目标关联的源文件列表 |
INCLUDE_DIRECTORIES |
目标私有的头文件搜索路径 |
LINK_LIBRARIES |
直接链接的库列表 |
COMPILE_DEFINITIONS |
编译宏定义 |
COMPILE_OPTIONS |
编译器选项 |
OUTPUT_NAME |
最终输出文件名(不含扩展名) |
RUNTIME_OUTPUT_DIRECTORY |
可执行文件 / Windows DLL 输出目录 |
LIBRARY_OUTPUT_DIRECTORY |
动态库输出目录(Linux/macOS) |
ARCHIVE_OUTPUT_DIRECTORY |
静态库 / Windows 导入库输出目录 |
CXX_STANDARD |
C++ 标准版本(如 11, 14, 17, 20) |
POSITION_INDEPENDENT_CODE |
是否生成位置无关代码(-fPIC) |
VISIBILITY_PRESET |
符号默认可见性(hidden/default) |
7.4 属性设置的最佳实践
在 Modern CMake 中,虽然 set_target_properties 很强大,但更推荐使用专用的 target_* 命令来操作属性,因为它们能更好地处理可见性(PRIVATE/PUBLIC/INTERFACE)和传播机制:
- 使用
target_compile_definitions()代替直接设置COMPILE_DEFINITIONS; - 使用
target_include_directories()代替直接设置INCLUDE_DIRECTORIES; - 使用
target_compile_options()代替直接设置COMPILE_OPTIONS; - 使用
target_link_libraries()管理依赖。
这些命令的详细用法,我们将在后续章节深入讲解。
八、小结
本节我们系统梳理了 CMake 中所有目标类型及其适用场景:
- add_executable:构建可执行文件,支持 WIN32、MACOSX_BUNDLE、EXCLUDE_FROM_ALL 等选项;
- add_library 实体库:STATIC(静态库)、SHARED(动态库)、MODULE(插件模块),默认类型受 BUILD_SHARED_LIBS 控制;
- OBJECT 对象库:只生成对象文件,用于代码复用,避免生成独立库文件;
- INTERFACE 接口库:不生成实体,用于传递头文件路径、编译选项等配置,是头文件库和配置集合的绝佳抽象;
- IMPORTED 导入目标:引用外部已构建的库,设置其位置及接口属性;
- ALIAS 别名目标:为已有库创建命名空间风格的别名,提升代码可读性。
目标是 Modern CMake 的核心载体,后续我们将继续学习如何通过 target_link_libraries、target_include_directories 等命令在目标之间建立依赖关系,实现“基于目标”的现代构建管理。下节见!


没有回复内容