1. 最小可行的 CMakeLists.txt

思考:

我需要告诉 CMake 什么?

  • 它需要知道最低兼容的 CMake 版本是多少?(cmake_minimum_required
  • 我的项目叫什么名字?(project
  • 我想生成什么?(一个可执行文件?一个库?)(add_executable 或 add_library
  • 这个生成目标需要哪些源文件?(add_executable 或 add_library 的参数)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 1. 指定 CMake 最低版本要求
# Ubuntu 22.04 自带的 CMake 版本通常较高 (如 3.22+),但设置一个稍低的版本(如 3.16)可以增加兼容性。
# 这确保了我们使用的 CMake 命令和特性在该版本及以上可用。
cmake_minimum_required(VERSION 3.16)

# 2. 定义项目名称和语言
# 项目名称会用在一些默认变量和 IDE 中。
# 指定 CXX 表示这是一个 C++ 项目,CMake 会自动寻找并配置 C++ 编译器。
project(MyMinimalProject CXX)

# 3. 添加一个可执行文件目标
# 第一个参数 "my_app" 是你想要生成的可执行文件的名字。
# 后续参数是构成这个可执行文件的源文件列表。这里只有一个 main.cpp。
add_executable(my_app main.cpp)

解释:

  • cmake_minimum_required(VERSION 3.16):告诉 CMake 使用至少 3.16 版本的语法和功能。如果系统上的 CMake 版本低于此,它会报错。
  • project(MyMinimalProject CXX):定义项目名为 MyMinimalProject,并声明主要语言是 C++ (CXX)。这会让 CMake 检查 C++ 编译器是否可用。
  • add_executable(my_app main.cpp):指示 CMake 创建一个名为 my_app 的可执行文件,该文件由 main.cpp 编译而来。

构建步骤:

  1. 打开终端,进入 my_minimal_project 目录。
  2. 创建构建目录并进入:mkdir build && cd build (推荐将构建产物与源码分开)
  3. 运行 CMake 配置:cmake .. ( .. 指向包含 CMakeLists.txt 的上级目录)
  4. 编译项目:make (或者 cmake --build .)
  5. 运行可执行文件:./my_app

2. 更多源文件的处理

思考:

  1. 如何告诉 add_executable 所有需要的 .cpp 文件?
    • 直接在 add_executable 命令中列出所有 .cpp 文件?
    • 用一个变量来存储源文件列表,然后传递给 add_executable?(更整洁)
    • 让 CMake 自动查找目录下的 .cpp 文件?(aux_source_directory 或 file(GLOB ...)不推荐用于源文件,因为新增/删除文件时 CMake 可能不会自动检测到变化)
  2. 如果源文件分散在不同目录(如 src/),CMake 如何找到它们?
    • 在文件名中包含相对路径(例如 src/main.cpp)。
  3. 如果头文件放在单独的目录(如 include/),编译器如何找到它们?
    • 需要告诉 CMake 头文件的搜索路径。(target_include_directories

代码示例:

假设项目结构为:

1
2
3
4
5
6
7
8
my_multi_file_project/
├── CMakeLists.txt
├── include/
│ └── helper.h
└── src/
├── main.cpp
└── helper.cpp

include/helper.h:

1
2
3
4
5
6
#ifndef HELPER_H
#define HELPER_H

void print_message();

#endif // HELPER_H

src/helper.cpp:

1
2
3
4
5
6
#include "helper.h" // 注意这里可以直接 include,因为我们会告诉 CMake 头文件路径
#include <iostream>

void print_message() {
std::cout << "Message from helper!" << std::endl;
}

src/main.cpp:

1
2
3
4
5
6
7
8
#include "helper.h" // 包含我们自己的头文件
#include <iostream>

int main() {
std::cout << "Hello from main!" << std::endl;
print_message(); // 调用来自 helper 的函数
return 0;
}

CMakeLists.txt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
cmake_minimum_required(VERSION 3.16)
project(MyMultiFileProject CXX)

# 1. 使用变量存储源文件列表 (推荐方式)
# 将所有需要编译的 .cpp 文件列在这里。使用相对路径。
set(SOURCES
src/main.cpp
src/helper.cpp
)

# 2. 添加可执行文件目标,使用变量
# 将 ${SOURCES} 变量的内容展开作为源文件列表。
add_executable(my_app ${SOURCES})

# 3. 指定头文件搜索路径
# 告诉 CMake,当编译 my_app 这个目标时,
# 应该到 "include" 目录下查找头文件 (#include "...").
# PUBLIC 意味着如果其他目标链接到 my_app,它们也会自动获得这个 include 目录。
# 对于可执行文件,PRIVATE 通常也足够。
target_include_directories(my_app PUBLIC include)

解释:

  • set(SOURCES ...):创建了一个名为 SOURCES 的 CMake 变量,存储了所有 .cpp 文件的列表。这比在 add_executable 中写一长串文件名更清晰,易于维护。
  • add_executable(my_app ${SOURCES}):使用 ${VAR_NAME} 语法来引用变量 SOURCES
  • target_include_directories(my_app PUBLIC include):这是关键一步。它告诉编译器在编译 my_app 的源文件时,去 include 目录下查找 #include 的头文件。没有这一行,#include "helper.h" 会失败。PUBLIC 关键字表示这个包含目录不仅 my_app 自己用,如果将来有其他 CMake 目标链接到 my_app,它们也会继承这个包含目录(对于可执行文件,通常用 PRIVATE 即可,表示仅 my_app 内部使用)。

3. 第三方库如何使用(以OpenCV为例)

思考:

  1. 如何让 CMake 找到已安装的 OpenCV 库?
    • 使用 find_package 命令。这是 CMake 查找外部库的标准方式。
  2. 我需要 OpenCV 的哪些部分(模块)?
    • OpenCV 是模块化的(如 coreimgprochighgui 等)。明确指定需要的模块可以减少不必要的依赖和链接。
    • find_package 允许通过 COMPONENTS 参数指定所需模块。
  3. find_package 找到库后,如何将它链接到我的目标(my_app)?
    • 需要告诉 CMake 两件事:
      • 编译器在哪里找到 OpenCV 的头文件
      • 链接器在哪里找到 OpenCV 的库文件并将它们链接到我的可执行文件
    • 现代 CMake 方式(推荐): 使用 target_link_libraries 配合 find_package 提供的 “Imported Target” (例如 OpenCV::opencv_core)。这种方式会自动处理头文件路径和库链接。
    • 旧式 CMake 方式(了解即可): find_package 会设置一些变量(如 OpenCV_INCLUDE_DIRS 和 OpenCV_LIBS),然后手动使用 target_include_directories 和 target_link_libraries

代码示例:

假设项目结构不变,我们修改 main.cpp 来使用 OpenCV。

src/main.cpp (示例:读取并显示一张图片):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include "helper.h"
#include <iostream>

// 包含 OpenCV 头文件
#include <opencv2/core.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/highgui.hpp>

int main(int argc, char** argv) {
if (argc != 2) {
std::cout << "Usage: " << argv[0] << " <Image_Path>" << std::endl;
return -1;
}

std::cout << "Hello from main!" << std::endl;
print_message();

// 读取图片
cv::Mat image = cv::imread(argv[1], cv::IMREAD_COLOR);

if (image.empty()) {
std::cout << "Could not read the image: " << argv[1] << std::endl;
return -1;
}

// 显示图片
cv::imshow("Display window", image);
int k = cv::waitKey(0); // 等待按键

return 0;
}

CMakeLists.txt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
cmake_minimum_required(VERSION 3.16)
project(MyOpenCVProject CXX)

set(SOURCES
src/main.cpp
src/helper.cpp
)

# --- OpenCV Integration ---
# 1. 查找 OpenCV 包
# REQUIRED: 如果找不到 OpenCV,CMake 会报错停止。
# COMPONENTS: 指定我们需要的模块。
find_package(OpenCV REQUIRED COMPONENTS core imgcodecs highgui)

# 检查是否成功找到 (可选,但推荐)
if(OpenCV_FOUND)
message(STATUS "Found OpenCV version: ${OpenCV_VERSION}")
else()
message(FATAL_ERROR "OpenCV not found!")
endif()
# --- End OpenCV Integration ---

add_executable(my_app ${SOURCES})

target_include_directories(my_app PUBLIC include)

# --- Link OpenCV to the target ---
# 2. 将 OpenCV 链接到我们的可执行文件 (现代方式)
# OpenCV 的 find_package 脚本会创建所谓的 "Imported Targets"。
# 通常格式是 `OpenCV::module_name` (例如 OpenCV::core, OpenCV::imgcodecs)。
# 链接这些 Imported Targets 会自动处理包含目录和库文件。
# PRIVATE 表示链接的库仅 my_app 内部使用,不会传递给链接到 my_app 的其他目标。

# 推荐这样做,但我实际操作找不到
target_link_libraries(my_app PRIVATE
OpenCV::core
OpenCV::imgcodecs
OpenCV::highgui
)
# 最后是通过这个成功运行的
target_include_directories(my_app PUBLIC include)

# --- End Link OpenCV ---

解释:

  • find_package(OpenCV REQUIRED COMPONENTS ...):指示 CMake 查找 OpenCV。REQUIRED 确保找不到时构建失败。COMPONENTS 列出了我们代码中实际用到的 OpenCV 模块 (core 对应 cv::Matimgcodecs 对应 cv::imreadhighgui 对应 cv::imshowcv::waitKey)。
  • message(STATUS ...):在 CMake 配置阶段打印信息,方便调试。
  • target_link_libraries(my_app PRIVATE OpenCV::core ...):这是最关键的一步。它将 my_app 链接到 find_package 找到的 OpenCV 模块。使用 OpenCV::module_name 这种 Imported Target 是现代 CMake 的推荐做法,它比旧方法更简洁、更健壮,CMake 会自动管理头文件路径 (target_include_directories 不需要再为 OpenCV 手动添加) 和库文件链接。