4、目标与可执行文件:add_executable()、add_library()、目标的概念与属性

好,咱们进入正题。这一章聊的是 CMake 里最核心的概念——目标(Target)。说白了,你写 CMakeLists.txt 就是在描述怎么把源代码变成一个个目标。可执行文件、库文件,都是目标。

我个人习惯把目标想象成一个「盒子」。这个盒子装着源文件、编译选项、链接库、头文件路径……所有你需要的东西。CMake 帮你管理这些盒子,最后生成你想要的产物。

4.1 可执行文件目标:add_executable()

这个命令最常用,也最简单。它的作用就是告诉 CMake:「嘿,我要生成一个可执行文件。」

add_executable(my_app main.cpp utils.cpp)

这里 my_app 就是目标的名字。你可以把它理解成这个盒子的标签。后面跟着的源文件,就是放进盒子里的原材料。

嗯,这里要注意:目标名字在整个 CMake 工程里必须是唯一的。你不能同时有两个 my_app,否则 CMake 会报错。

小技巧:目标名字和可执行文件名默认是一样的。但你可以通过 set_target_properties() 修改输出文件名。比如:
set_target_properties(my_app PROPERTIES OUTPUT_NAME "my_super_app")
这样生成的可执行文件就叫 my_super_app 了。

我在项目中遇到过一个问题:有人把源文件路径写错了,结果编译了半天全是链接错误。后来发现是 add_executable() 里漏了一个文件。所以,我建议你养成好习惯——每次添加新源文件,第一时间更新 CMakeLists.txt。

4.2 库文件目标:add_library()

库文件分三种:静态库、动态库、模块库。用 add_library() 来创建。

add_library(my_lib STATIC utils.cpp math.cpp)   # 静态库
add_library(my_lib SHARED utils.cpp math.cpp)   # 动态库
add_library(my_lib MODULE utils.cpp math.cpp)   # 模块库(很少用)

如果不指定类型,默认是 STATIC 还是 SHARED?其实都不是。默认是 STATIC,但你可以通过 BUILD_SHARED_LIBS 变量来改变这个默认行为。

避坑指南:我曾经在 Windows 上编译动态库,忘了导出符号,结果链接器报了一堆 unresolved external symbol。解决方案是:在头文件里用 __declspec(dllexport) 和 __declspec(dllimport) 控制符号可见性。或者,更现代的做法是用 CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS 这个属性。

你想想看,为什么要有静态库和动态库的区别?静态库会把代码直接复制到可执行文件里,体积大但部署简单。动态库是运行时加载,体积小但需要处理依赖。我个人习惯:小工具用静态库,大型项目用动态库。

4.3 目标的概念:不只是个名字

目标在 CMake 里是一个「一等公民」。它不仅仅是名字和源文件的集合。每个目标都有一堆属性(Properties),控制着编译、链接、安装等行为。

举个例子,你可以给目标设置头文件搜索路径:

target_include_directories(my_app PRIVATE include/)

也可以设置链接库:

target_link_libraries(my_app PRIVATE my_lib)

还可以设置编译宏:

target_compile_definitions(my_app PRIVATE USE_NEW_FEATURE=1)

这些命令都是在往目标的「盒子」里添加东西。CMake 最终会把这些信息传递给编译器、链接器。

为什么会这样设计?因为目标之间可以形成依赖关系。比如 my_app 依赖 my_lib,那么 my_lib 的头文件路径、编译宏会自动传递给 my_app。这就是 CMake 的「传递性」——你不需要手动管理这些细节。

4.4 目标属性:藏在盒子里的秘密

每个目标都有几十个属性。常用的有这些:

属性名 作用 示例
OUTPUT_NAME 修改输出文件名 set_target_properties(my_app PROPERTIES OUTPUT_NAME "my_app_v2")
VERSION 设置库的版本号 set_target_properties(my_lib PROPERTIES VERSION 1.2.3)
LINK_FLAGS 添加链接器选项 set_target_properties(my_app PROPERTIES LINK_FLAGS "-static")
COMPILE_FLAGS 添加编译器选项 set_target_properties(my_app PROPERTIES COMPILE_FLAGS "-Wall -O2")
FOLDER 在 IDE 中分组 set_target_properties(my_app PROPERTIES FOLDER "Applications")

嗯,这里要注意:COMPILE_FLAGS 和 LINK_FLAGS 是字符串形式的,容易写错。我更推荐用 target_compile_options() 和 target_link_options(),它们更安全、更现代。

核心思想:目标属性是 CMake 的「配置中心」。你不需要在命令行里写一堆 -D 和 -I,只需要在 CMakeLists.txt 里设置好属性,CMake 会自动生成正确的编译命令。

4.5 实战:一个完整的小例子

咱们把上面的知识点串起来,写一个完整的 CMakeLists.txt:

cmake_minimum_required(VERSION 3.10)
project(MyProject VERSION 1.0.0)

# 添加一个库
add_library(math_lib STATIC math.cpp)
target_include_directories(math_lib PUBLIC include/)
target_compile_definitions(math_lib PRIVATE MATH_DEBUG=0)

# 添加一个可执行文件
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE math_lib)
target_compile_options(my_app PRIVATE -Wall -Wextra)
set_target_properties(my_app PROPERTIES
    OUTPUT_NAME "my_super_app"
    VERSION "1.0.0"
)

这个例子展示了:

  • 如何创建库目标和可执行目标
  • 如何设置头文件路径、编译宏、编译选项
  • 如何链接库
  • 如何修改输出文件名和版本号

我在项目中遇到过一个问题:target_include_directories 的 PUBLIC、PRIVATE、INTERFACE 搞混了。结果库的头文件没传递到可执行文件里,编译报错找不到头文件。记住这个口诀:自己的东西用 PRIVATE,给别人用的用 PUBLIC,只给别人用自己不用的用 INTERFACE。

4.6 避坑指南:目标相关的常见错误

我曾经在大型项目里犯过一个低级错误:两个子目录里定义了同名的目标。CMake 报错说目标重复,我找了半天才发现。解决方案很简单:给目标名字加前缀,比如 module_a_my_lib 和 module_b_my_lib。

还有一个坑:add_library() 和 add_executable() 必须在 target_link_libraries() 之前调用。否则 CMake 会报「目标不存在」的错误。这个顺序问题,新手经常遇到。

最后,别忘了 cmake_minimum_required() 和 project() 要写在最前面。没有它们,CMake 会一脸懵逼。

总结一下:
  • 目标是 CMake 的核心抽象,管理源文件、编译选项、链接库
  • add_executable() 生成可执行文件,add_library() 生成库文件
  • 目标属性控制编译、链接、安装等行为
  • 用 target_* 系列命令设置属性,比直接操作变量更安全
  • 注意目标名字唯一性、命令调用顺序、传递性规则

好,这一章就到这里。下一章咱们聊聊「变量与缓存」,看看 CMake 里那些让人头疼的变量作用域问题。