4. 目标与属性:构建项目的基石
好,我们进入第四章。这一章,说白了就是CMake的核心操作——创建目标、配置目标、链接目标。你想想看,一个项目再复杂,最后不就是要生成一个可执行文件或者一个库吗?所以这一章的内容,你几乎每天都会用到。
4.1 add_library():创建你的库
先说说库。我个人习惯把项目拆成多个库,这样代码复用性高,编译也快。add_library()就是干这个的。
add_library(my_library STATIC
src/utils.cpp
src/core.cpp
)
这里STATIC表示静态库。还有SHARED(动态库)和MODULE(模块库)。
三种库类型对比
| 类型 | 生成文件 | 链接方式 | 典型场景 |
|---|---|---|---|
| STATIC | .a / .lib | 编译时链接 | 内部工具库 |
| SHARED | .so / .dll | 运行时加载 | 插件、公共组件 |
| MODULE | .so / .dll | 动态加载 | 插件系统 |
我的经验: 静态库在项目内部用起来最省心。动态库虽然灵活,但版本管理是个坑。我曾经在一个项目中,因为动态库版本不一致,调试了整整两天。
4.2 add_executable():生成可执行文件
这个更简单,就是告诉CMake:我要生成一个能跑的程序。
add_executable(my_app
src/main.cpp
src/app.cpp
)
嗯,这里要注意:可执行文件的名字不要和库的名字冲突。我见过有人把库和可执行文件都叫core,结果链接的时候CMake一脸懵。
4.3 target_include_directories():告诉编译器头文件在哪
这个函数,说白了就是给编译器加-I参数。但它的好处是——只对当前目标生效。
target_include_directories(my_library
PUBLIC
include/
PRIVATE
src/
)
这里有两个关键字:PUBLIC和PRIVATE。什么意思呢?
- PUBLIC:当前目标用,链接它的目标也能用
- PRIVATE:只有当前目标能用
- INTERFACE:当前目标不用,但链接它的目标能用
避坑指南: 我曾经把内部实现的头文件设成了PUBLIC,结果外部用户直接包含了这些头文件。后来改接口的时候,一堆人来找我。所以,能PRIVATE就别PUBLIC。
4.4 target_link_libraries():把目标串起来
这是CMake里最常用的函数之一。它决定了你的目标依赖谁。
target_link_libraries(my_app
PRIVATE
my_library
fmt::fmt
)
这里同样有PUBLIC、PRIVATE、INTERFACE。逻辑和头文件目录一样:
- PRIVATE:自己用,不传给下游
- PUBLIC:自己用,也传给下游
- INTERFACE:自己不直接依赖,但下游需要
一个典型场景:
# 基础库
add_library(core STATIC core.cpp)
target_include_directories(core PUBLIC include/)
# 业务库
add_library(business STATIC business.cpp)
target_link_libraries(business PUBLIC core)
# 可执行文件
add_executable(app main.cpp)
target_link_libraries(app PRIVATE business)
# 这里app不仅能链接business,还能自动获得core的头文件路径
4.5 目标属性设置:精细控制
有时候,光靠函数参数不够。你需要直接操作目标的属性。
set_target_properties(my_library PROPERTIES
CXX_STANDARD 17
CXX_STANDARD_REQUIRED ON
POSITION_INDEPENDENT_CODE ON
)
常用的属性有哪些?我列几个:
| 属性名 | 作用 | 示例值 |
|---|---|---|
| CXX_STANDARD | 指定C++标准 | 17, 20 |
| CXX_STANDARD_REQUIRED | 是否强制使用该标准 | ON, OFF |
| POSITION_INDEPENDENT_CODE | 生成位置无关代码 | ON, OFF |
| OUTPUT_NAME | 修改输出文件名 | my_lib |
| COMPILE_DEFINITIONS | 添加宏定义 | DEBUG=1 |
我的习惯: 我会把C++标准统一设置成一个变量,然后在各个目标里引用。这样升级标准时,改一个地方就行。
set(MY_CXX_STANDARD 17)
set_target_properties(my_library PROPERTIES
CXX_STANDARD ${MY_CXX_STANDARD}
)
4.6 一个完整的例子
把上面的知识点串起来,看看一个实际项目长什么样:
# 基础数学库
add_library(math STATIC
src/math/add.cpp
src/math/sub.cpp
)
target_include_directories(math PUBLIC include/math)
set_target_properties(math PROPERTIES
CXX_STANDARD 17
POSITION_INDEPENDENT_CODE ON
)
# 工具库
add_library(utils STATIC
src/utils/string.cpp
src/utils/file.cpp
)
target_include_directories(utils PUBLIC include/utils)
target_link_libraries(utils PUBLIC math)
set_target_properties(utils PROPERTIES
CXX_STANDARD 17
)
# 主程序
add_executable(my_app
src/main.cpp
)
target_link_libraries(my_app PRIVATE utils)
target_include_directories(my_app PRIVATE include/)
set_target_properties(my_app PROPERTIES
CXX_STANDARD 17
CXX_STANDARD_REQUIRED ON
)
你看,这样一层层依赖下来,结构清晰,维护起来也方便。我曾经在一个遗留项目里,看到所有代码都塞在一个可执行文件里,编译一次要10分钟。后来拆成几个库,编译时间直接降到2分钟。
4.7 小结
这一章的内容,说白了就是CMake的骨架。add_library和add_executable创建目标,target_include_directories和target_link_libraries建立依赖关系,set_target_properties做精细控制。这三板斧用好了,大部分项目的构建需求都能搞定。
下一章,我们会聊聊变量和缓存,这是CMake的血液。到时候你会发现,有了变量,CMake才能真正灵活起来。