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才能真正灵活起来。