第10章:目标与属性——构建系统的核心拼图
说实话,CMake 里最让我着迷的部分,就是「目标」和「属性」这套机制。我刚接触 CMake 那会儿,总觉得它就是个高级的 makefile 生成器。直到有一天,我在一个大型项目中需要同时管理十几个库和可执行文件,才真正体会到——没有目标与属性的概念,你根本玩不转。
这一章,我们就来彻底搞懂 add_library、add_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 |
我在项目中遇到过最坑的事,就是有人把 SHARED 和 MODULE 搞混。前者可以用 target_link_libraries 链接,后者不行。说白了,MODULE 是给 dlopen 用的,不是给链接器用的。
10.3 add_executable:创建可执行文件
这个命令更直接:
add_executable(my_app main.cpp)
它会生成一个可执行文件。你可以给它添加源文件、设置属性、链接库。嗯,这里要注意:如果你不指定任何属性,CMake 会使用默认值。但默认值不一定适合你的项目。
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 知识体系图
下面这张图,帮你理清本章的核心逻辑:
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}")
这个例子涵盖了本章所有核心知识点。你想想看,如果没有目标和属性,你要怎么控制输出文件名?怎么确保动态库的位置无关代码?怎么跨平台传递编译选项?
get_target_property 验证一下。这样不容易出错。
10.9 避坑指南
最后,分享几个我踩过的坑:
- 属性名大小写敏感:
CXX_STANDARD和cxx_standard是两个不同的属性。CMake 的属性名通常全大写。 - INTERFACE 属性传递:如果你想让一个库的消费者自动获得某些属性(比如头文件路径),记得用
target_include_directories的INTERFACE或PUBLIC关键字。 - 不要滥用全局属性:
set(CMAKE_CXX_FLAGS ...)会影响所有目标。我建议尽量用目标属性,这样更可控。
嗯,这一章的内容就到这里。目标和属性是 CMake 的基石,理解了它们,你就能写出更灵活、更可维护的构建系统。记住:每个目标都是一个独立的小世界,而属性就是控制这个世界运行规则的开关。
公众号:蓝海资料掘金营,微信deep3321