工程化构建:现代 CMakeLists 编写与 VSCode 单步断点调试

2503 字
13 分钟
工程化构建:现代 CMakeLists 编写与 VSCode 单步断点调试

前面两章我们写代码基本都在单个文件里折腾,编译时直接敲一行 g++ main.cpp -o app 就完事了。
但真实的机器人上位机项目不可能把所有代码塞在一个文件里。相机驱动、通信协议、识别算法、轨迹计算,每个模块都有自己的头文件和实现文件,还会引用很多外部库(比如 OpenCV、串口库、线程库)。
这一章把项目的目录结构规范、CMake 编译脚本和 VS Code 的单步断点调试一次性讲清楚。


一、为什么需要 CMake?#

单文件编译时手敲 g++ 没问题,但如果项目有十几个源文件,而且分布在不同文件夹里:

Terminal window
g++ src/main.cpp src/camera.cpp src/serial.cpp src/detector.cpp \
-Iinclude -I/usr/local/include/opencv4 \
-lopencv_core -lopencv_imgproc -lopencv_highgui -lpthread \
-O3 -std=c++17 -o robot_app

这样手动编译有三个明显问题:

  1. 命令太长,每次改动都要重新敲或者翻历史命令;
  2. 只要动了一个文件,上面的命令会把所有文件全部从头编译一遍,项目变大后编译耗时很长;
  3. 换到车载工控机或者队友电脑上,如果库的安装路径不一样,这串命令就报错了。

CMake 的作用是用一套跨平台的配置脚本(CMakeLists.txt),自动帮你生成底层的编译指令(Makefile)。你只需要维护好这个脚本,编译时敲两行命令或者在 VS Code 里按个快捷键就行。后续学 ROS2 时,它的底层编译规则本质上也是 CMake。


二、C++ 编译过程与两种常见报错#

在写 CMake 之前,先搞清楚 C++ 代码是怎么变成可执行程序的。整个过程主要分两步:

[源文件 .cpp] ──(编译阶段:检查语法、引入头文件)──> [目标文件 .o]
│
(链接阶段:把函数实现和库拼在一起)
↓
[最终程序 robot_app]

平时排查编译错误,只要看懂这两类报错就能定位问题:

1. 编译期错误:找不到头文件#

fatal error: target.hpp: No such file or directory
  • 原因:编译器在预处理时,找不到你 #include 的头文件在哪。
  • 解决方式:检查头文件路径拼写是否正确,或者在 CMake 里添加头文件搜索目录(target_include_directories)。

2. 链接期错误:未定义的引用#

undefined reference to `TargetDetector::detect()'
  • 原因:代码语法没问题,头文件也找到了,但是在最后打包成可执行文件时,找不到这个函数具体的代码实现。
  • 解决方式:检查实现该函数的 .cpp 文件有没有加入编译列表,或者对应的第三方库(.so / .a)有没有加进链接项(target_link_libraries)。

三、规范的工程目录结构#

在机器人上位机项目中,通常使用下面的分层目录:

my_vision_project/
├── CMakeLists.txt # 整个项目的编译配置文件
├── include/ # 存放所有头文件 (.hpp / .h)
│ └── detector.hpp
├── src/ # 存放所有源文件实现 (.cpp)
│ ├── detector.cpp
│ └── main.cpp
└── build/ # 编译生成的中间文件和最终可执行文件都在这里

把编译产物集中放在 build/ 目录里(外部构建,Out-of-source Build):
这样所有的临时文件(.o、Makefile)都在 build/ 内部,源码目录很干净。如果不小心编译出问题想从头来过,直接 rm -rf build/* 即可,不会误删源代码。


四、编写你的第一个 CMakeLists.txt#

在项目根目录下创建一个名为 CMakeLists.txt 的文件(注意大小写完全匹配):

# 1. 规定 CMake 的最低版本要求
cmake_minimum_required(VERSION 3.16)
# 2. 项目名称、版本和语言
project(my_vision_project VERSION 1.0 LANGUAGES CXX)
# 3. 指定 C++ 标准为 C++17
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 4. 生成可执行文件目标(Target)
add_executable(vision_app
src/main.cpp
src/detector.cpp
)
# 5. 为目标绑定头文件搜索路径(现代 CMake 规范:围绕 Target 进行配置)
# PRIVATE 表示该头文件路径仅供 vision_app 自己使用
target_include_directories(vision_app PRIVATE include)

现代 CMake 的跨平台构建命令#

在终端中,推荐使用现代 CMake 的一步式构建命令(无需手动 cd 进子目录,且自动适配底层工具):

Terminal window
# 1. 配置并生成构建目录(-B 自动创建 build 文件夹)
cmake -B build
# 2. 调用底层编译器开始并行构建(-j 自动使用全核)
cmake --build build -j
# 3. 运行生成的可执行程序
./build/vision_app

如果代码修改了,后续只需要执行 cmake --build build -j 即可,CMake 会自动增量编译修改过的文件。


五、引入第三方库(以 OpenCV 为例)#

下一章我们要开始讲图像处理,所以先看在 CMake 里怎么引入外部库。现代 CMake 提供了一个专门查找库的命令:find_package。

修改 CMakeLists.txt:

cmake_minimum_required(VERSION 3.16)
project(my_vision_project VERSION 1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 查找系统里安装的 OpenCV 库(REQUIRED 表示如果找不到就直接报错停止)
find_package(OpenCV REQUIRED)
# 生成可执行文件
add_executable(vision_app
src/main.cpp
src/detector.cpp
)
target_include_directories(vision_app PRIVATE include)
# 链接库文件(OpenCV 4+ 导出的现代目标会自动处理依赖的头文件路径)
target_link_libraries(vision_app PRIVATE ${OpenCV_LIBS})

只要通过 sudo apt install libopencv-dev 安装过 OpenCV,find_package(OpenCV REQUIRED) 就能自动找到其配置,并通过 target_link_libraries 完成库链接。


六、在 VS Code 中一键构建与单步断点调试#

第二章我们在 VS Code 里安装了 CMake Tools 插件。配置好之后,日常开发就不需要频繁在终端里手动敲 cmake 和 make 了。

1. 配置编译器(Kit)#

  1. 用 VS Code 打开项目根目录;
  2. 插件会自动弹出提示扫描编译器,或者按 Ctrl + Shift + P,输入 CMake: Select a Kit;
  3. 在列表里选择系统自带的编译器,通常是 GCC / G++ (x86_64-linux-gnu)。

2. 状态栏快捷操作#

配置完后,VS Code 最底部的状态栏会出现几个按钮:

  • Build(编译):点击一下,或者按快捷键 F7,自动执行 CMake 编译。编译信息会在下方控制台输出;
  • 运行箭头:编译并直接运行可执行文件;
  • 小虫子图标(调试):进入断点调试模式。

3. 断点调试实战#

在遇到逻辑 Bug 或者程序崩溃时,用断点调试比到处加 printf 更直接。

第一步:设置构建类型(切勿盲目写死 Debug)#

构建类型决定了编译器是否开启优化:

  • Debug:关闭所有优化,保留完整调试符号,代码运行较慢;
  • Release:开启 -O3 满血优化,剥离调试符号,运行速度最快;
  • RelWithDebInfo:带有调试信息的优化版本(开启 -O2 优化并保留调试符号)。

极其重要的避坑细节:
切勿在 CMakeLists.txt 中粗暴写死 set(CMAKE_BUILD_TYPE Debug)!
一旦写死 Debug,后续第 6、7、8 章运行 OpenCV 的高斯滤波和轮廓计算时,帧率可能会从 60 FPS 骤降到 15 FPS。且它会覆盖 VS Code 底部状态栏的模式切换器。
推荐在 CMakeLists.txt 中仅在未指定时给一个合理的默认值:

if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "Build type" FORCE)
endif()

第二步:打断点#

在代码编辑窗口中,把鼠标移到某一行代码的行号左侧,会出现一个暗红色小圆点,单击一下变成亮红色圆点,断点就打好了。

第三步:启动调试#

启动调试有两种最平滑的方式:

  1. 直接点击状态栏的小虫子图标(或按快捷键 Ctrl + Shift + P 输入 CMake: Debug),CMake Tools 会自动编译并启动当前目标停在断点处;
  2. 使用标准的 F5 快捷键:VS Code 按 F5 默认需要读取 .vscode/launch.json 配置文件。如果在项目根目录下创建 .vscode/launch.json 并填入以下通用模板,VS Code 就能自动追踪 CMake 当前选中的目标程序:
{
"version": "0.2.0",
"configurations": [
{
"name": "CMake: Debug Current Target",
"type": "cppdbg",
"request": "launch",
// 自动关联 CMake Tools 选中的目标,无需手动写死路径
"program": "${command:cmake.launchTargetPath}",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
}
]
}
调试常用快捷键:
- F5:继续运行(Continue),直到遇到下一个断点;
- F10:单步跳过(Step Over),执行当前行,停在下一行;
- F11:单步进入(Step Into),如果当前行是一个函数调用,会跳进函数内部;
- Shift + F11:单步跳出(Step Out),直接执行完当前函数剩下的部分,返回到上一层调用处;
- Shift + F5:停止调试。

第四步:观察运行状态#

程序停在断点处时,看 VS Code 左侧边栏的调试面板:

  • 变量窗口(Variables):展示当前函数作用域内所有局部变量的值。如果是一个 std::vector,可以展开看到里面的每个元素;如果是一个指针,能看到它存的地址以及解引用后的值;
  • 监视窗口(Watch):点击 + 号输入你想观察的表达式(比如 targets.size() 或 ptr->x),程序每次单步执行都会实时计算并显示最新结果;
  • 调用堆栈(Call Stack):如果程序发生段错误(Segmentation fault)崩溃了,调用堆栈会直接标出崩在具体哪一个文件的哪一行,以及这个函数是被谁调用的。

🎯 本章通关小作业#

请在本地建一个符合规范的最小工程并验证调试流程:

  1. 搭建目录结构:
    demo/
    ├── CMakeLists.txt
    ├── include/
    │ └── math_utils.hpp
    └── src/
    ├── math_utils.cpp
    └── main.cpp
  2. 编写代码:
    • math_utils.hpp:声明一个函数 float calculate_3d_dist(float x, float y, float z);
    • math_utils.cpp:实现该函数(开方计算模长)
    • main.cpp:调用该函数并输出结果
  3. 编写 CMakeLists.txt 并用现代命令(cmake -B build && cmake --build build -j)编译跑通。
  4. 断点调试体验:
    • 在 VS Code 中打开该项目;
    • 在 main.cpp 调用函数的那一行打上断点;
    • 点击底部状态栏的“小虫子”或配置 launch.json 后按 F5 启动调试,按 F11 单步跳进 math_utils.cpp 内部,在左侧变量窗口观察传入的参数值。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

工程化构建:现代 CMakeLists 编写与 VSCode 单步断点调试
https://firefly-7a0.pages.dev/posts/rc_basic/05_cmake_and_debug/
作者
lumivers
发布于
2026-10-08
许可协议
CC BY-NC-SA 4.0
相关文章 智能推荐
1
终端开荒:Ubuntu 基础操作与 VSCode 远程开发
RC上位机入门 专为 Windows 用户设计的 Linux 破冰指南:双系统与 WSL2 抉择、国内换源、高频终端命令、免密 SSH 与 VSCode Remote 远程丝滑编码。
2
见图识物:cv::Mat 内存本质、HSV 色彩提取与 GUI 调参窗口
RC上位机入门 OpenCV 图像处理入门:数字图像与矩阵本质、cv::Mat 内存排布与深浅拷贝、为什么赛场绝不能用 RGB 抠颜色、HSV 色彩空间详解、inRange 二值化提取道具、滤波去噪与搭建实用的动态调参滑动条窗口。
3
几何特征提取:形态学去噪、轮廓分析与旋转矩形筛选
RC上位机入门 从二值掩膜到物理目标:形态学腐蚀膨胀与开闭运算、findContours 轮廓提取、面积与长宽比几何筛选、cv::RotatedRect 旋转矩形与道具角点提取。
4
告别 C 语言:现代 C++ 引用、RAII 思想与高频 STL
RC上位机入门 从 C 语言平滑过渡到现代 C++:引用取代指针传参、类与 RAII 自动释放资源、vector/string/queue/pair 等视觉高频 STL 容器、auto 类型推导、范围 for 循环、Lambda 匿名函数与智能指针入门。
5
从 2D 像素到 3D 现实:小孔成像模型、张氏标定与 solvePnP 解算
RC上位机入门 机器视觉核心分水岭:四大坐标系转换推导、小孔成像数学模型、内参矩阵与畸变系数物理意义、张氏棋盘格标定实操、多边形拟合获取透视角点、鲁棒极角排序法、相机坐标系欧拉角推导与 solvePnP 解算。
随机文章 随机推荐

评论区

Profile Image of the Author
lumivers
Hello, I'm lumivers.
公告
欢迎来到我的博客!
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
182
分类
14
标签
576
总字数
384,899
运行时长
0 天
最后活动
0 天前

目录