4、CMake目标与属性:add_library、target_include_directories、target_link_libraries、属性设置
好,咱们今天聊聊CMake里最核心的几个概念——目标(Target)和属性(Property)。
说实话,我刚开始用CMake那会儿,也是从变量和目录命令一路写过来的。后来项目越来越大,发现那种全局式的写法简直是个噩梦。直到我真正理解了“目标”这个概念,才觉得CMake的世界一下子清晰了。
说白了,CMake里的“目标”就是一个构建单元。它可以是库,也可以是可执行文件。而“属性”就是挂在这个目标上的各种配置信息。你想想看,一个库需要知道它的头文件在哪、它依赖谁、它编译时要加什么宏——这些,都是属性。
4.1 add_library:创建你的第一个目标
add_library 是创建库目标的入口。它的基本用法很简单:
add_library(my_lib STATIC src/lib.cpp src/utils.cpp)
这里我创建了一个叫 my_lib 的静态库。STATIC 表示静态库,SHARED 是动态库,MODULE 是插件模块。
我在项目中遇到过一种情况:团队里有人把源文件路径写错了,结果编译时死活找不到符号。后来我养成了一个习惯——源文件列表尽量用变量管理,比如:
set(MY_LIB_SOURCES
src/lib.cpp
src/utils.cpp
src/helper.cpp
)
add_library(my_lib STATIC ${MY_LIB_SOURCES})
这样改起来方便,也容易排查。
4.2 target_include_directories:告诉编译器头文件在哪
库写好了,别人怎么用?首先得让人家能找到你的头文件。这就是 target_include_directories 的活。
target_include_directories(my_lib
PUBLIC
include
PRIVATE
src
)
这里有两个关键字:PUBLIC 和 PRIVATE。它们的区别很关键:
- PUBLIC:当前目标能看到,链接当前目标的其他目标也能看到。
- PRIVATE:只有当前目标能看到,别人看不到。
- INTERFACE:当前目标看不到,但链接它的目标能看到(常用于纯头文件库)。
嗯,这里要注意:PUBLIC 和 INTERFACE 用错了,很容易导致头文件泄露或者找不到头文件。我曾经接手过一个项目,所有头文件路径都设成了 PUBLIC,结果下游项目莫名其妙地包含了不该包含的内部头文件,编译报错一堆。后来我花了一下午才理清楚。
4.3 target_link_libraries:把目标粘在一起
一个库很少是孤立的。它通常会依赖其他库。这时候就需要 target_link_libraries 了。
target_link_libraries(my_app
PUBLIC
my_lib
fmt::fmt
PRIVATE
zlib
)
这个命令的作用是:告诉链接器,my_app 需要链接 my_lib、fmt 和 zlib。
你可能会问:为什么还要分 PUBLIC 和 PRIVATE?
原因很简单——传递性。如果 my_app 是一个库,它 PUBLIC 链接了 my_lib,那么任何链接 my_app 的目标也会自动链接 my_lib。这叫做“依赖传递”。
我个人习惯是:只在最顶层可执行文件里用 PRIVATE,库之间尽量用 PUBLIC 或 INTERFACE,这样依赖关系更清晰。
4.4 属性设置:set_target_properties 与 get_target_property
除了上面三个命令,CMake 还提供了更通用的属性设置方式——set_target_properties。
set_target_properties(my_lib
PROPERTIES
CXX_STANDARD 17
CXX_STANDARD_REQUIRED ON
POSITION_INDEPENDENT_CODE ON
)
这里我设置了三个属性:
CXX_STANDARD:指定 C++ 标准版本。CXX_STANDARD_REQUIRED:强制要求编译器支持该标准。POSITION_INDEPENDENT_CODE:生成位置无关代码,动态库必须开启。
如果你想读取某个属性,可以用 get_target_property:
get_target_property(my_std my_lib CXX_STANDARD)
message(STATUS "my_lib 的 C++ 标准是: ${my_std}")
我在项目中遇到过一个问题:有人用 set(CMAKE_CXX_STANDARD 17) 全局设置,结果不同目标需要不同标准时,就乱套了。后来我改成每个目标单独设置属性,问题就解决了。
OUTPUT_NAME(修改输出文件名)、VERSION 和 SOVERSION(动态库版本号)、COMPILE_DEFINITIONS(编译宏)。你可以用 cmake --help-property-list 查看所有属性。
4.5 知识体系图:目标与属性的关系
下面这张图展示了本章的核心逻辑——目标如何通过属性与外部交互:
从这张图可以看得很清楚:一个目标接收源文件和依赖,输出头文件和库文件,同时受属性控制。这就是 CMake 构建系统的核心模型。
4.6 实战建议:如何组织你的 CMakeLists.txt
最后,我分享一个我在实际项目中用的组织方式:
# 1. 定义源文件
set(MYLIB_SOURCES src/core.cpp src/io.cpp)
# 2. 创建目标
add_library(mylib STATIC ${MYLIB_SOURCES})
# 3. 设置头文件路径
target_include_directories(mylib
PUBLIC include
PRIVATE src
)
# 4. 设置编译属性
set_target_properties(mylib PROPERTIES
CXX_STANDARD 20
CXX_STANDARD_REQUIRED ON
)
# 5. 链接依赖
target_link_libraries(mylib
PUBLIC fmt::fmt
PRIVATE zlib
)
这个顺序是我个人习惯——先有目标,再配属性,最后链接依赖。逻辑清晰,不容易漏掉东西。
嗯,今天就聊到这儿。记住一句话:用目标思维写 CMake,而不是用目录思维写 CMake。你会发现,项目越大,这种写法越省心。