10. 目标与属性:add_executable、add_library、target_include_directories、target_link_libraries、目标属性

好,咱们今天聊点实在的。CMake 里最核心的概念,说白了就是「目标」和「属性」。

你想想看,一个项目无非就是:我要编译一个可执行文件,或者一个库。然后告诉编译器去哪里找头文件,链接器去哪里找库文件。这些在 CMake 里,都围绕着「目标」展开。

10.1 创建目标:add_executable 与 add_library

这是最基础的两个命令。我刚开始学 CMake 时,觉得这俩就是声明一下文件名而已。后来才发现,它们创建的「目标」是整个构建系统的基石。

add_executable

用来生成可执行文件。语法很简单:

add_executable(my_app main.cpp utils.cpp)

这样,CMake 就会创建一个名为 my_app 的目标。后面所有跟这个可执行文件相关的配置,比如头文件路径、链接库,都挂在这个目标上。

小提示: 我个人习惯把源文件列表单独存到一个变量里,比如 set(SOURCES main.cpp utils.cpp),然后再 add_executable(my_app ${SOURCES})。这样如果源文件多了,看着清爽。

add_library

用来生成库文件。有三种类型:

类型 说明 生成文件
STATIC 静态库 libxxx.axxx.lib
SHARED 动态库 libxxx.soxxx.dll
MODULE 插件库 类似动态库,但不参与链接
add_library(my_lib STATIC math.cpp io.cpp)

嗯,这里要注意:如果你不指定类型,默认是 STATIC。但我在跨平台项目里吃过亏——Windows 下静态库和动态库的导出符号处理方式完全不同。所以,建议显式指定类型

10.2 配置头文件路径:target_include_directories

这个命令,说白了就是告诉编译器:「喂,头文件在这个目录里,给我加上 -I 参数」。

target_include_directories(my_app PRIVATE src/include)

这里有三个关键字:PRIVATEPUBLICINTERFACE。我刚开始也搞混过,后来总结了一个规律:

  • PRIVATE:只影响当前目标自己。头文件路径只给自己用。
  • PUBLIC:当前目标用,链接这个目标的其他目标也能用。
  • INTERFACE:当前目标自己不用,但链接它的目标必须用。
举个例子: 你写了一个库 my_lib,它内部用了 json.hpp。如果这个头文件只在库内部实现里用到,对外暴露的接口不需要,那就用 PRIVATE。如果对外暴露的接口里也引用了 json.hpp,那就得用 PUBLIC

我曾经在一个项目里,把所有头文件路径都设成了 PUBLIC,结果下游项目莫名其妙地编译报错,查了半天才发现是某个内部头文件污染了全局命名空间。从那以后,我养成了习惯:能用 PRIVATE 就别用 PUBLIC

10.3 链接库:target_link_libraries

这个命令用来告诉链接器:「把这两个目标链接到一起」。

target_link_libraries(my_app PRIVATE my_lib)

同样有 PRIVATEPUBLICINTERFACE 三个关键字。语义和头文件路径完全一致:

  • PRIVATE:当前目标链接这个库,但别人链接我时不需要。
  • PUBLIC:当前目标链接这个库,别人链接我时也会自动链接它。
  • INTERFACE:当前目标不链接,但别人链接我时必须链接它。

举个例子:你写了一个图形库 graphics,它内部用了 freetype 来渲染字体。如果 graphics 对外暴露的接口里用到了 freetype 的类型,那就得用 PUBLIC。否则用 PRIVATE 就够了。

避坑指南: 我曾经在 Linux 下链接一个动态库,忘了加 -fPIC 编译选项,结果链接时报了一堆重定位错误。后来发现,生成动态库时,所有源文件都得用 set(CMAKE_POSITION_INDEPENDENT_CODE ON) 或者目标属性里设置 POSITION_INDEPENDENT_CODEON

10.4 目标属性:精细控制构建行为

每个目标都有一堆属性,你可以通过 set_target_properties 来设置。这些属性控制着编译、链接、安装等各个环节的行为。

常用的属性有:

属性名 作用 示例
CXX_STANDARD 指定 C++ 标准 set_target_properties(my_app PROPERTIES CXX_STANDARD 17)
OUTPUT_NAME 修改生成的文件名 set_target_properties(my_app PROPERTIES OUTPUT_NAME "my_custom_app")
POSITION_INDEPENDENT_CODE 生成位置无关代码(动态库需要) set_target_properties(my_lib PROPERTIES POSITION_INDEPENDENT_CODE ON)
COMPILE_DEFINITIONS 添加编译宏定义 set_target_properties(my_app PROPERTIES COMPILE_DEFINITIONS "DEBUG=1")

我个人觉得,CXX_STANDARD 是最常用的。你想想看,如果不指定,CMake 会用默认的 C++ 标准,可能是 C++98 或者 C++11,那你的 autolambda 全得报错。

小技巧: 除了 set_target_properties,你还可以用 get_target_property 来读取属性值。调试时很有用,比如:get_target_property(out_name my_app OUTPUT_NAME),然后 message("Output name: ${out_name}")

10.5 知识体系总览

说了这么多,我画了一张图,帮你把本章的知识点串起来:

CMake 目标与属性知识体系 目标 (Target) 创建目标 add_executable / add_library 配置依赖 target_include_directories 链接库 target_link_libraries 目标属性 set_target_properties

这张图展示了本章的核心逻辑:先创建目标,然后配置头文件路径和链接库,最后通过属性进行精细控制。每一步都离不开 PRIVATEPUBLICINTERFACE 这三个关键字的正确使用。

10.6 实战建议

最后,给你几个我踩过坑之后总结的建议:

  • 头文件路径尽量用 PRIVATE:除非你确定外部需要引用,否则别污染别人的编译环境。
  • 链接库时注意传递性:如果 A 链接 B,B 链接 C,A 是否需要知道 C?想清楚再用 PUBLIC。
  • 属性设置要显式:不要依赖 CMake 的默认值。比如 C++ 标准,你不设,它可能用 C++98,到时候一堆编译错误等着你。
  • 多用 message() 调试:不确定某个属性有没有生效?打印出来看看。

嗯,这一章的内容就到这里。记住,目标与属性是 CMake 的基石,搞懂了它们,后面的模块、生成器表达式什么的,学起来就轻松多了。


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