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.a 或 xxx.lib |
SHARED |
动态库 | libxxx.so 或 xxx.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)
这里有三个关键字:PRIVATE、PUBLIC、INTERFACE。我刚开始也搞混过,后来总结了一个规律:
- 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)
同样有 PRIVATE、PUBLIC、INTERFACE 三个关键字。语义和头文件路径完全一致:
- PRIVATE:当前目标链接这个库,但别人链接我时不需要。
- PUBLIC:当前目标链接这个库,别人链接我时也会自动链接它。
- INTERFACE:当前目标不链接,但别人链接我时必须链接它。
举个例子:你写了一个图形库 graphics,它内部用了 freetype 来渲染字体。如果 graphics 对外暴露的接口里用到了 freetype 的类型,那就得用 PUBLIC。否则用 PRIVATE 就够了。
-fPIC 编译选项,结果链接时报了一堆重定位错误。后来发现,生成动态库时,所有源文件都得用 set(CMAKE_POSITION_INDEPENDENT_CODE ON) 或者目标属性里设置 POSITION_INDEPENDENT_CODE 为 ON。
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,那你的 auto、lambda 全得报错。
set_target_properties,你还可以用 get_target_property 来读取属性值。调试时很有用,比如:get_target_property(out_name my_app OUTPUT_NAME),然后 message("Output name: ${out_name}")。
10.5 知识体系总览
说了这么多,我画了一张图,帮你把本章的知识点串起来:
这张图展示了本章的核心逻辑:先创建目标,然后配置头文件路径和链接库,最后通过属性进行精细控制。每一步都离不开 PRIVATE、PUBLIC、INTERFACE 这三个关键字的正确使用。
10.6 实战建议
最后,给你几个我踩过坑之后总结的建议:
- 头文件路径尽量用 PRIVATE:除非你确定外部需要引用,否则别污染别人的编译环境。
- 链接库时注意传递性:如果 A 链接 B,B 链接 C,A 是否需要知道 C?想清楚再用 PUBLIC。
- 属性设置要显式:不要依赖 CMake 的默认值。比如 C++ 标准,你不设,它可能用 C++98,到时候一堆编译错误等着你。
- 多用
message()调试:不确定某个属性有没有生效?打印出来看看。
嗯,这一章的内容就到这里。记住,目标与属性是 CMake 的基石,搞懂了它们,后面的模块、生成器表达式什么的,学起来就轻松多了。