一、构建目标:从零开始理解 CMake 的核心

说实话,我刚接触 CMake 那会儿,最困惑的就是「构建目标」这个概念。你想想看,我们写代码无非就是编译成可执行文件或者库文件,为什么 CMake 非要搞出个「目标」来?

后来在项目里踩过几次坑才明白——构建目标就是 CMake 世界里的一切基础。它不只是一个文件,而是一个完整的构建单元,包含了源文件、编译选项、依赖关系、链接库等等信息。

1.1 add_executable:生成可执行文件

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

add_executable(my_app main.cpp utils.cpp)

上面这行代码,创建了一个名为 my_app 的构建目标。CMake 会编译 main.cpputils.cpp,然后链接成可执行文件。

我的习惯:目标名用下划线或驼峰,别用空格。我在项目里见过有人用 my app 带空格的,结果在 Windows 上折腾了半天路径问题。

如果你有多个源文件,可以这样写:

add_executable(my_app 
    main.cpp
    utils.cpp
    network.cpp
    database.cpp
)

我个人喜欢把每个源文件单独一行,这样 git diff 看得清楚。你想想看,要是把所有文件写在一行,别人改代码时都不知道你加了哪个文件。

1.2 add_library:生成库文件

库文件分三种:静态库、动态库、模块库。我一个个说。

类型 关键字 生成文件 使用场景
静态库 STATIC .a / .lib 代码直接打进可执行文件
动态库 SHARED .so / .dll 运行时加载,节省空间
模块库 MODULE .so / .dll 插件系统,动态加载
add_library(my_lib STATIC math.cpp io.cpp)
add_library(my_shared SHARED network.cpp protocol.cpp)
add_library(my_plugin MODULE plugin_core.cpp)
我曾经踩过的坑:在 Windows 上编译动态库时,记得导出符号。否则链接时会报一堆 unresolved external symbol。后来我养成了习惯,动态库的接口头文件里一定加 __declspec(dllexport)

1.3 目标属性:给目标加配置

光有目标还不够,你得告诉 CMake 怎么编译它。目标属性就是干这个的。

add_executable(my_app main.cpp)

# 设置 C++ 标准
set_target_properties(my_app PROPERTIES
    CXX_STANDARD 17
    CXX_STANDARD_REQUIRED ON
)

# 设置输出目录
set_target_properties(my_app PROPERTIES
    RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
)

# 设置编译定义
target_compile_definitions(my_app PRIVATE
    USE_OPENSSL
    ENABLE_DEBUG=1
)

常用的目标属性我列几个:

  • CXX_STANDARD:C++ 标准版本
  • POSITION_INDEPENDENT_CODE:位置无关代码,动态库必须开
  • LINKER_LANGUAGE:指定链接器语言
  • FOLDER:在 IDE 里分组显示
重点:目标属性是 CMake 3.0 之后推荐的方式。别再用老式的 add_definitionsinclude_directories 了,那会让你的构建系统变得难以维护。

1.4 目标依赖关系:谁依赖谁

项目大了,目标之间肯定有依赖。比如可执行文件依赖静态库,静态库又依赖另一个库。

add_library(core STATIC core.cpp)
add_library(utils STATIC utils.cpp)
add_executable(my_app main.cpp)

# 链接库
target_link_libraries(my_app PRIVATE core utils)

# 传递头文件路径
target_include_directories(core PUBLIC include)
target_include_directories(utils PRIVATE include/internal)

这里有个关键概念——传递性。PUBLIC 的意思是:我自己的使用者也能看到这个头文件路径。PRIVATE 的意思是:只有我自己能看到。

举个例子:

  • core 库的 include 目录是 PUBLIC,所以 my_app 也能用
  • utils 库的 include/internal 是 PRIVATE,my_app 看不到
我的建议:能用 PUBLIC 就别用 INTERFACE,能用 PRIVATE 就别用 PUBLIC。说白了,接口暴露得越少,以后改起来越轻松。我在重构一个百万行项目时,就是因为之前 PUBLIC 用得太多,改一个头文件路径导致几十个目标重新编译。

知识体系总览

下面这张图,把本章的核心内容串起来了:

构建目标 add_executable 生成可执行文件 如:my_app.exe add_library STATIC / SHARED / MODULE 生成 .a / .so / .lib 目标属性 CXX_STANDARD 编译定义 / 输出目录 目标依赖 target_link_libraries PUBLIC / PRIVATE / INTERFACE 构建目标是 CMake 的核心抽象 一切编译、链接、依赖都围绕它展开

总结一下

构建目标这个概念,说白了就是 CMake 帮你管理的一个「编译任务」。它知道要编译哪些源文件、用什么编译选项、链接哪些库、输出到哪里。

我个人觉得,理解构建目标是学好 CMake 的第一步。你想想看,后面讲的自定义命令、生成器表达式、安装规则,全都是围绕着目标来展开的。

嗯,这一章的内容就到这里。记住三个核心命令:add_executableadd_librarytarget_link_libraries,再加上目标属性的概念,你就能应付大部分项目了。