第10章:目标与属性——构建系统的核心拼图

说实话,CMake 里最让我着迷的部分,就是「目标」和「属性」这套机制。我刚接触 CMake 那会儿,总觉得它就是个高级的 makefile 生成器。直到有一天,我在一个大型项目中需要同时管理十几个库和可执行文件,才真正体会到——没有目标与属性的概念,你根本玩不转。

这一章,我们就来彻底搞懂 add_libraryadd_executable,以及它们身上挂着的那些属性。嗯,准备好了吗?

10.1 什么是「目标」?

在 CMake 的世界里,目标就是你最终要构建的东西。它要么是一个库(静态库、动态库、模块库),要么是一个可执行文件。每个目标都有自己的名字、源文件、依赖关系,以及一堆控制行为的属性。

我个人习惯把目标想象成一个「对象」——它有属性、有方法(虽然 CMake 里叫命令),还能继承父目录的某些特性。你想想看,这不就是面向对象编程的思路吗?

10.2 add_library:创建库目标

这是最常用的命令之一。它的基本用法很简单:

add_library(my_lib STATIC src/lib.cpp)

这里 STATIC 是库类型。CMake 支持四种类型:

类型 说明 典型后缀
STATIC 静态库,链接时直接嵌入可执行文件 .a / .lib
SHARED 动态库,运行时加载 .so / .dll
MODULE 插件式动态库,不参与链接 .so / .dll
OBJECT 对象库,只编译不链接 .o / .obj

我在项目中遇到过最坑的事,就是有人把 SHAREDMODULE 搞混。前者可以用 target_link_libraries 链接,后者不行。说白了,MODULE 是给 dlopen 用的,不是给链接器用的。

10.3 add_executable:创建可执行文件

这个命令更直接:

add_executable(my_app main.cpp)

它会生成一个可执行文件。你可以给它添加源文件、设置属性、链接库。嗯,这里要注意:如果你不指定任何属性,CMake 会使用默认值。但默认值不一定适合你的项目。

我曾经在一个跨平台项目里,默认生成的控制台程序在 Windows 上总是弹出黑框。后来才发现,需要设置 WIN32_EXECUTABLE 属性为 TRUE。这个坑,我踩了整整一个下午。

10.4 目标属性:控制行为的开关

每个目标都有一堆属性。你可以通过 set_target_properties 来设置它们:

set_target_properties(my_app PROPERTIES
    CXX_STANDARD 17
    CXX_STANDARD_REQUIRED ON
    WIN32_EXECUTABLE TRUE
)

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

  • CXX_STANDARD:C++ 标准版本,比如 11、14、17、20
  • CXX_STANDARD_REQUIRED:是否强制使用指定标准
  • POSITION_INDEPENDENT_CODE:生成位置无关代码,动态库必须
  • OUTPUT_NAME:指定输出文件名,不一定是目标名
  • LINKER_LANGUAGE:强制使用某个语言的链接器

你可能会问:为什么要用属性,而不是直接在编译器参数里写?因为属性是跨平台的。CMake 会根据平台自动转换成对应的编译器标志。这就是 CMake 的价值所在。

10.5 获取属性:看看目标到底长什么样

设置完属性,你总得检查一下吧?get_target_property 就是干这个的:

get_target_property(my_std my_app CXX_STANDARD)
message("C++ standard: ${my_std}")

如果属性没设置,它会返回 NOTFOUND。我个人习惯在调试时加一段这样的代码,确保属性被正确传递。

还有一个更强大的命令 get_property,它可以获取各种作用域下的属性:

get_property(is_win32 TARGET my_app PROPERTY WIN32_EXECUTABLE)

嗯,这里要注意:get_property 的第一个参数是变量名,不是目标名。我刚开始用的时候经常搞反。

10.6 属性继承:父目录的「遗传」

CMake 的属性继承机制,说白了就是:子目录会继承父目录的某些属性。但不是所有属性都继承。哪些会继承?

  • 目录属性:比如 CMAKE_CXX_STANDARD,子目录会继承
  • 目标属性:目标之间不会自动继承,但可以通过 INTERFACE 属性传递

举个例子:

# 顶层 CMakeLists.txt
set(CMAKE_CXX_STANDARD 17)

add_subdirectory(src)

# src/CMakeLists.txt 中的目标会自动继承 CXX_STANDARD=17

但如果你在顶层设置了一个目标的属性,子目录里的目标是看不到的。这就是为什么我建议:公共属性尽量用目录属性或全局属性,不要挂在具体目标上

核心原则:属性继承遵循「就近原则」。子目录可以覆盖父目录的属性,但不会影响父目录。目标属性只影响自身,除非你显式使用 INTERFACE 关键字。

10.7 知识体系图

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

CMake 目标 add_library add_executable STATIC SHARED MODULE 目标属性 set_target_properties get_target_property 属性继承 目标 = 源文件 + 属性 + 依赖关系

10.8 实战:一个完整的例子

光说不练假把式。我们来看一个实际项目中的 CMakeLists.txt:

cmake_minimum_required(VERSION 3.16)
project(MyProject LANGUAGES CXX)

# 设置全局 C++ 标准
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 创建一个静态库
add_library(core STATIC
    src/core/engine.cpp
    src/core/parser.cpp
)

# 设置库的输出名
set_target_properties(core PROPERTIES
    OUTPUT_NAME "my_core"
    POSITION_INDEPENDENT_CODE ON
)

# 创建一个可执行文件
add_executable(my_app
    src/main.cpp
)

# 链接库
target_link_libraries(my_app PRIVATE core)

# 检查属性
get_target_property(out_name core OUTPUT_NAME)
message(STATUS "Core library output name: ${out_name}")

这个例子涵盖了本章所有核心知识点。你想想看,如果没有目标和属性,你要怎么控制输出文件名?怎么确保动态库的位置无关代码?怎么跨平台传递编译选项?

我的建议:刚开始写 CMake 时,不要追求一步到位。先写出能编译的版本,然后逐步添加属性控制。每加一个属性,就用 get_target_property 验证一下。这样不容易出错。

10.9 避坑指南

最后,分享几个我踩过的坑:

  • 属性名大小写敏感CXX_STANDARDcxx_standard 是两个不同的属性。CMake 的属性名通常全大写。
  • INTERFACE 属性传递:如果你想让一个库的消费者自动获得某些属性(比如头文件路径),记得用 target_include_directoriesINTERFACEPUBLIC 关键字。
  • 不要滥用全局属性set(CMAKE_CXX_FLAGS ...) 会影响所有目标。我建议尽量用目标属性,这样更可控。

嗯,这一章的内容就到这里。目标和属性是 CMake 的基石,理解了它们,你就能写出更灵活、更可维护的构建系统。记住:每个目标都是一个独立的小世界,而属性就是控制这个世界运行规则的开关。


公众号:蓝海资料掘金营,微信deep3321