5. 2.1 目标的概念与类型

导语

在前面的章节中,我们已经掌握了 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 上,将可执行文件打包为 .app Bundle 格式。
  • 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_LIBSON,则默认生成 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 中所有目标类型及其适用场景:

  1. add_executable:构建可执行文件,支持 WIN32、MACOSX_BUNDLE、EXCLUDE_FROM_ALL 等选项;
  2. add_library 实体库:STATIC(静态库)、SHARED(动态库)、MODULE(插件模块),默认类型受 BUILD_SHARED_LIBS 控制;
  3. OBJECT 对象库:只生成对象文件,用于代码复用,避免生成独立库文件;
  4. INTERFACE 接口库:不生成实体,用于传递头文件路径、编译选项等配置,是头文件库和配置集合的绝佳抽象;
  5. IMPORTED 导入目标:引用外部已构建的库,设置其位置及接口属性;
  6. ALIAS 别名目标:为已有库创建命名空间风格的别名,提升代码可读性。

目标是 Modern CMake 的核心载体,后续我们将继续学习如何通过 target_link_librariestarget_include_directories 等命令在目标之间建立依赖关系,实现“基于目标”的现代构建管理。下节见!

请登录后发表评论

    没有回复内容

正在唤醒异次元光景……