开篇:工地上的建筑蓝图
如果说CMake是一位施工队长,那么在前面的章节里,我们只学会了怎么把队长请到工地(安装配置),以及怎么让他听懂简单的指令(基础语法)。但真正要让工程运转起来,我们必须明白一个问题:这位队长到底能帮我们建造哪些”建筑”?
在CMake的世界里,这些待建造的建筑统称为目标(Target)。你可以把目标理解为施工蓝图——它告诉CMake:”我要盖一栋楼(可执行程序)”或者”我要造一个零件仓库(库文件)”。CMake会根据蓝图,调度编译器、链接器,把源代码变成最终的产物。
这一节,我们将系统认识CMake中所有的目标类型。这是Modern CMake最核心的概念,也是你从”能跑就行”迈向”工程化构建”的关键一步。
可执行文件目标:add_executable
最常见的目标类型当然是可执行文件。在CMake中,使用 add_executable 命令来定义它。
完整语法解析
add_executable(<name> [WIN32] [MACOSX_BUNDLE]
[EXCLUDE_FROM_ALL]
[source1] [source2 ...])
各参数含义如下:
<name>:目标的名称,在CMake项目内部唯一标识这个目标。默认情况下,生成的可执行文件名也是它。WIN32:仅在Windows平台有效。添加后表示构建的是GUI应用程序,不会弹出控制台窗口(即链接时会设置/SUBSYSTEM:WINDOWS)。MACOSX_BUNDLE:在macOS上,将可执行文件打包成.app目录结构。EXCLUDE_FROM_ALL:这个目标不会被默认构建,只有显式指定构建它时才会编译。适合测试工具、示例程序等。
来看一个实际的例子:
# 定义一个普通的命令行工具
add_executable(my_tool main.cpp utils.cpp)
# 定义一个Windows GUI程序(不会显示黑框框)
add_executable(my_gui_app WIN32 main.cpp window.cpp)
# 定义一个仅在需要时才构建的测试工具
add_executable(stress_test EXCLUDE_FROM_ALL test_main.cpp)
需要注意的是,add_executable 定义的目标名 my_tool 是CMake内部使用的”代号”。你可以通过后续学到的目标属性,让最终生成的文件名和代号不同。
库目标类型:add_library 的三兄弟
比可执行文件更灵活的是库(Library)。CMake中的 add_library 可以创建三种实体库类型:
1. 静态库(STATIC)
add_library(my_static_lib STATIC lib.cpp)
静态库会被打包成 .a(Linux/macOS)或 .lib(Windows)文件。链接静态库时,链接器会把库中需要的代码复制到最终的可执行文件中。
特点:部署简单(不需要带着库文件跑),但多个可执行文件分别链接同一份静态库时,会导致二进制体积膨胀。
2. 动态库(SHARED)
add_library(my_shared_lib SHARED lib.cpp)
动态库生成 .so(Linux)、.dylib(macOS)或 .dll(Windows)。它不会被复制到可执行文件里,而是在程序运行时被加载到内存中供多个程序共享。
特点:节省磁盘和内存空间,更新库不需要重新编译主程序,但部署时需要确保运行时能找到库文件。
3. 模块库(MODULE)
add_library(my_plugin MODULE plugin.cpp)
模块库也生成动态库文件,但它不是用于链接的,而是用于运行时动态加载(比如插件系统,通过 dlopen 或 LoadLibrary 加载)。
在Linux上,MODULE和SHARED的后缀都是 .so,但MODULE不会被链接器用于普通的依赖解析。
小贴士:不指定类型会怎样?
如果你写 add_library(my_lib lib.cpp) 而不加类型参数,CMake会检查一个变量:
set(BUILD_SHARED_LIBS ON) # 开启后,所有未指定类型的库默认变成SHARED
这是一个全局开关。在现代CMake项目中,建议始终显式指定库类型,避免行为不可预测。
对象库(OBJECT):中间产物的复用
除了上面三种”成品”库,CMake还提供一种特殊的库类型:对象库(OBJECT)。
add_library(my_objects OBJECT obj1.cpp obj2.cpp)
对象库不会生成最终的 .a 或 .so 文件,它只编译源代码生成 .o(或 .obj)对象文件。
这有什么用呢?想象这样一个场景:你有一组通用的工具代码,想同时提供给主程序和测试程序使用,但又不想把它们打包成独立的库文件。这时对象库就是完美的选择:
add_library(common_objs OBJECT logger.cpp utils.cpp)
# 主程序直接使用这些对象文件
add_executable(main_app main.cpp)
target_link_libraries(main_app PRIVATE common_objs)
# 测试程序也复用同一批对象文件,无需重新编译
add_executable(test_app test.cpp)
target_link_libraries(test_app PRIVATE common_objs)
对象库的精髓在于编译一次,多处复用,避免了重复编译同一批源文件。需要注意的是,对象库不能直接被链接,必须通过生成器表达式 $<TARGET_OBJECTS:common_objs> 或者像上面那样直接 target_link_libraries 传递(CMake 3.12+ 支持直接链接对象库)。
接口库(INTERFACE):没有实体的”幽灵”库
如果说对象库是”只有零件没有成品”,那么接口库(INTERFACE)就是”连零件都没有,只有使用说明书”。
add_library(my_header_only INTERFACE)
接口库不编译任何源代码,也不生成任何二进制文件。它的作用是传递构建要求——比如头文件搜索路径、编译宏、编译选项等。
典型使用场景
场景一:纯头文件库(Header-only Library)
比如你想在项目中使用一个只有 .h 文件的数学工具库:
add_library(math_utils INTERFACE)
target_include_directories(math_utils INTERFACE include/)
target_compile_features(math_utils INTERFACE cxx_std_17)
# 使用时
add_executable(calculator main.cpp)
target_link_libraries(calculator PRIVATE math_utils)
虽然 math_utils 没有实体,但链接它之后,calculator 会自动获得 include/ 目录和C++17标准的要求。
场景二:统一的编译配置集合
你可以把一组警告选项封装成一个接口库,方便统一施加给多个目标:
add_library(strict_warnings INTERFACE)
target_compile_options(strict_warnings INTERFACE
-Wall -Wextra -Wpedantic
)
add_executable(app1 main1.cpp)
add_executable(app2 main2.cpp)
target_link_libraries(app1 PRIVATE strict_warnings)
target_link_libraries(app2 PRIVATE strict_warnings)
这是Modern CMake的精髓——基于目标传递依赖,而非全局变量污染。
导入目标与别名目标:外部世界的连接器
导入目标(IMPORTED)
前面讲的都是CMake自己”建造”的目标。但如果一个库已经由别人编译好了(比如系统安装的OpenSSL、通过包管理器下载的库),CMake如何引用它?
答案就是导入目标(IMPORTED):
add_library(OpenSSL::SSL IMPORTED SHARED)
导入目标告诉CMake:”这个目标存在,但不是我负责编译的。”你需要通过设置目标属性来告诉CMake库文件和头文件在哪里:
set_target_properties(OpenSSL::SSL PROPERTIES
IMPORTED_LOCATION "/usr/lib/libssl.so"
INTERFACE_INCLUDE_DIRECTORIES "/usr/include"
)
在实际项目中,你很少手动写这些,通常由 find_package 自动完成。但理解IMPORTED的机制,有助于你在查找包失败时手动修复。
别名目标(ALIAS)
别名目标就像给目标起一个”艺名”或”规范化名字”:
add_library(my_real_lib STATIC real.cpp)
# 给它起一个别名
add_library(MyProject::real_lib ALIAS my_real_lib)
# 之后可以用别名来引用
target_link_libraries(main_app PRIVATE MyProject::real_lib)
别名有两个重要作用:
- 命名空间化:避免目标名称冲突。比如第三方库和目标库都叫
json,通过MyProject::json和ThirdParty::json可以清晰区分。 - 接口统一:你可以先创建一个IMPORTED目标,再给它一个ALIAS,让内部库和外部库的使用方式看起来完全一致。
注意:别名目标本身不能再设置属性,它完全是被引用目标的”影子”。
目标属性概览:查看与设置
每个目标都携带着大量”属性(Properties)”,描述了如何编译、链接、输出到哪里等信息。CMake提供了两个基础命令来操作属性:
# 设置属性
set_target_properties(my_target PROPERTIES
OUTPUT_NAME "awesome_app"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
)
# 查看属性
get_target_property(OUTPUT_DIR my_target RUNTIME_OUTPUT_DIRECTORY)
message(STATUS "输出目录: ${OUTPUT_DIR}")
常用属性速查表
| 属性名 | 说明 |
|---|---|
OUTPUT_NAME |
最终生成的文件名(不含后缀) |
RUNTIME_OUTPUT_DIRECTORY |
可执行文件和DLL的输出目录 |
LIBRARY_OUTPUT_DIRECTORY |
动态库(.so/.dylib)的输出目录 |
ARCHIVE_OUTPUT_DIRECTORY |
静态库(.a/.lib)的输出目录 |
INCLUDE_DIRECTORIES |
目标私有的头文件搜索路径 |
COMPILE_OPTIONS |
专用于该目标的编译选项 |
COMPILE_DEFINITIONS |
该目标的宏定义 |
LINK_LIBRARIES |
该目标链接的库 |
LINK_OPTIONS |
该目标的链接器选项 |
CXX_STANDARD |
该目标使用的C++标准 |
POSITION_INDEPENDENT_CODE |
是否生成位置无关代码(对库很重要) |
不过,Modern CMake更推荐你使用语义化的 target_xxx 命令(如 target_compile_options、target_link_libraries)来设置这些属性,而非直接操作属性值。因为前者能更好地处理传递性依赖(Transitivity),这也是我们下一节要深入探讨的话题。
小结:目标类型全景图
让我们用一张”工地蓝图”来总结今天学到的目标类型:
- add_executable:盖一栋成品大楼(可执行程序)。
- add_library STATIC:建一个固定仓库(静态库),把零件复制到每栋楼里。
- add_library SHARED:建一个公共租赁中心(动态库),大家运行时去借用。
- add_library MODULE:建一个插件工坊,主楼运行时按需加载。
- add_library OBJECT:只生产标准件(对象文件),供多栋楼复用。
- add_library INTERFACE:制定一套建筑规范(头文件、编译选项),没有实体建筑。
- IMPORTED:隔壁工厂已经造好的设备,我们直接搬过来用。
- ALIAS:给建筑挂一个正式的招牌名,方便统一管理。
掌握这些目标类型后,你已经具备了搭建复杂CMake项目的基本素材。下一节,我们将学习如何管理源文件和头文件——如何优雅地告诉CMake:”这栋楼要用到哪些砖头和图纸?”


没有回复内容