C++调试注意事项

vscode中调试c++的一些方法和注意事项


0 调试之前

  1. 环境配置
  2. 代码断点
Image

1 调试工具栏

调试工具栏中可用的操作

操作快捷键描述
继续 / 暂停F5继续:恢复正常的程序/脚本执行(直到下一个断点)。 暂停:检查当前行执行的代码并逐行调试。
单步跳过F10执行下一个方法作为单个命令,而不检查或跟随其组成步骤。
单步调试F11进入下一个方法以逐行跟踪其执行。
单步跳出Shift+F11当位于方法或子例程内部时,通过完成当前方法的剩余行(如同将其视为单个命令)来返回到先前的执行上下文。
重启Ctrl+Shift+F5终止当前程序执行并使用当前的运行配置重新开始调试。
停止Shift+F5终止当前程序执行。

另外,可以通过右键某一行,“运行到光标处”。

Image

多进程调试:在一个调试过程中,启动另一个调试,调试工具栏会当前活动的进程,在下拉菜单中选择其他进程:

Image

同时 “调用堆栈” 窗口中可以切换不同进程进行调试:

Image

2 调试控制台

调试控制台主要用于在程序暂停时查看变量、计算表达式,以及直接调用调试器命令。表达式会在当前选中的调用栈帧中求值。

Image
  1. 常用 C++ 表达式示例:
i                         // 直接查看变量
vec.size()                // 向量长度
vec[3]                    // 向量索引
*ptr                      // 查看指针的值
obj.member                // 访问对象成员
this->member              // 指针访问对象成员
static_cast<int>(status)  // 类型转换
  1. GDB 常用命令:
-exec print vec          \\ 打印变量
-exec whatis vec         \\ 显示变量基本类型
-exec break test.cpp:50 \\ 设置断点
-exec watch count       \\ 变量被写入时暂停
-exec info breakpoints   \\ 断点信息
-exec info threads       \\ 线程信息
-exec where             \\ 查看当前执行位置

3 调试侧边栏

3.1 变量

3.1.1 Locals

局部变量名后面的 = 表示“该变量当前的值或摘要”。

Image

对于复杂对象,调试器通常不会直接显示全部内容,而是给出一行概括。部分显示结果说明:

调试显示含义
{...}复杂对象,内部内容被折叠
1当前标量值为 1,具体含义取决于类型
std::vector of length 20, capacity 20vector 有 20 个元素,已分配 20 个元素的空间
std::map with 3 elementsmap 中有 3 个键值对
OptFu::ElemOrder::First当前值是 ElemOrder 枚举中的 First
Eigen::Matrix<double,3,1344,ColMajor>3×1344 的 double 矩阵,按列存储
data ptr: 0x7fff3db00140数据所在内存的起始地址
std::unique_ptr<OptFu::ds::bvh::BVHTree>管理 BVHTree 对象的独占智能指针

注意: _M_ 开头的字段属于标准库内部实现。

this“是由编译器自动提供的指针,指向“调用当前成员函数的那个对象”。

3.1.2 Registers

寄存器是 CPU 内部的高速存储单元,直接保存执行指令所需的数据。调试时通过寄存器窗口可以观察 CPU 的实时状态。

Image

寄存器窗口通常包含以下内容:

  1. CPU / 通用寄存器

这类寄存器是最常见的,主要用于普通算术、逻辑运算、地址运算和函数调用传参。它们的作用可以理解为“程序当前正在处理什么数据”。常见有:

  • rax / rbx / rcx / rdx:通用数据寄存器;
  • rsp:栈指针,决定当前函数调用栈的位置;
  • rbp:栈基址指针,帮助访问当前函数的局部变量;
  • rip:指令指针,表示下一条将执行的机器指令;
  • rsi / rdi:常用于内存操作和函数参数传递。

它们的用途主要是:看程序“现在在算什么、用的是什么地址、当前函数调用栈是不是正常”。

  1. Segs(段寄存器)

这类寄存器用于告诉 CPU 当前代码、数据、栈分别位于哪个内存段。它们的作用更多是“定位内存区域”。常见有:

  • cs:代码段;
  • ds / es / ss:数据段、额外段、栈段;
  • fs / gs:在现代系统中常被用于线程本地存储(TLS)等特殊用途。

在调试时它们通常不是最主要的关注点,但遇到异常访问、线程切换、TLS 相关问题时,仍然可能很有帮助。

  1. FPU(浮点协处理器)

FPU 寄存器用于浮点运算,尤其是老式的 x87 浮点计算。它们的作用是帮助 CPU 处理小数、科学计算、三角函数等运算。常见是 st0 ~ st7

在调试时,如果程序出现浮点异常、NaN、Inf,或者数值计算结果异常,通常会查看这些寄存器。

  1. SSE / AVX(向量寄存器)

这类寄存器用于 SIMD(单指令多数据)并行计算,常用于图形、数值计算、视频处理等场景。它们能一次处理多个数据,提高性能。常见有:

  • xmm0 ~ xmm15:SSE 寄存器,通常 128 位;
  • ymm0 ~ ymm15:AVX 寄存器,通常 256 位;
  • zmm0 ~ zmm31:AVX-512 寄存器,通常 512 位。

调试时,若发现程序在做向量化计算、矩阵运算、图像处理等任务,SSE/AVX 寄存器可以帮助你看“当前是不是在批量处理多个数据”。

  1. Other Registers(其他寄存器)

除了上面几类,还有一些很重要的寄存器:

  • rflags / eflags:标志寄存器,记录比较结果、溢出、进位等状态;
  • mxcsr:控制和状态寄存器,影响浮点异常处理和 SIMD 浮点精度;
  • 调试寄存器、控制寄存器等:用于断点、异常处理和系统级调试。

这些寄存器的作用是帮助判断“当前这一步运算是否成功、是否发生了异常、控制流是否按预期进行”。

3.2 监视

Image

可以输入符合当前调试上下文的 C++ 表达式,例如:

a
a * b
vec.size()
vec[0]
ptr->value
object.member
this->member

也可以在变量窗口中复制变量的表达式,粘贴在监视窗口中,或者直接添加到监视。

但是变量名称是否能直接使用,仍然取决于它在当前代码位置是否可见,常见查看变量的方式如下:

变量类型典型查看方式
当前函数局部变量value
全局变量global_value::global_value
命名空间变量Namespace::value
类静态成员ClassName::value
对象普通成员object.value
指针指向对象的成员ptr->value
当前对象成员valuethis->value
调用者函数的局部变量切换到调用者的调用栈帧
已离开作用域的局部变量通常无法查看

3.3 调用堆栈

调用堆栈展示程序从入口函数到当前暂停位置的函数调用链。

Image

核心思路是“从当前执行点,向上追溯它是怎么被调用到这里的”。例如:

main()
└─ Solver::run()
  └─ run_analysis()
    └─ generate_mesh() ← 当前帧

这表示当前停在 generate_mesh(),它是由 run_analysis() 调用的,而 run_analysis() 又是由 Solver::run() 调用的,最终由 main() 发起。

多进程多线程的场景中,可以把调用堆栈理解成一个分层结构:

[进程 A]
├─ [线程 1]
│ └─ main()
│     └─ Solver::run()
│       └─ run_analysis()
│           └─ generate_mesh() ← 当前帧
├─ [线程 2]
│ └─ worker_thread()
│     └─ process_data()

[进程 B]
└─ [线程 1]
  └─ server_loop()
    └─ handle_request()

也就是说,调试时可以先从“进程”一级选择要看的目标,再从“线程”一级选择具体执行流,最后再挑选某个线程下面的若干“栈帧”来查看对应函数的局部变量和调用关系。

单击任意栈帧,可以切换到该函数的暂停位置,并查看当时可见的局部变量。


3.4 断点

常用断点包括:

类型用途示例
普通断点运行到这里时暂停直接停在某一行代码
条件断点仅在表达式为真时暂停i == 999ptr == nullptr
命中次数断点命中指定次数后暂停第 100 次进入循环时暂停
日志点调试控制台输出信息但不暂停程序输出 i、对象状态或执行路径
触发断点选定断点触发后启用src/RodLattice/rod_lattice.cpp:925
函数断点进入指定函数时暂停Solver::run
内联断点一行内多个可执行语句分别设置断点int a = foo(); int b = bar();
数据断点某块内存被读取或写入时暂停-exec watch count , 追踪成员变量何时被意外修改
异常断点抛出或捕获异常时暂停定位异常最初抛出的位置

命中次数断点在gdb中不支持

一个断点不仅可以设置一种属性,还可以可以叠加多个属性。可以把它理解成:一个断点“既能判断条件,还能记录日志”。

例如:

Image
Image

这类组合使用非常实用,尤其适合高频循环、复杂条件判断或需要观察多次执行结果的场景。右键行号旁的断点,可以编辑条件、命中次数,或者把它改为日志点。日志点适合高频循环:它不会频繁中断程序,比反复点击“继续”更容易观察变量变化。


4 常见调试问题

4.1 变量显示为 <optimized out>

使用带调试符号的低优化构建。例如 CMake 项目可以选择 Debug 配置,并确认实际编译参数中包含 -g。仅在 launch.json 中修改设置,不能替代重新编译。

4.2 断点停在错误的源码行,单步跳过/单步调试不按预期

使用 F10/F11 进行单步操作时,如果没有跳到预期源码行,常见原因包括:

  • 编译器优化导致代码行被重排、内联或合并,源码行与实际执行位置不完全匹配;
  • 当前调试符号已过期,源码改动后未重新编译;
  • 宏展开、模板实例化或复杂表达式造成行号映射不准确;
  • 代码被内联或尾调用优化,F10/F11 的“进入/跳过”行为与源码显示不同;

黄色高亮行是当前 CPU 指令地址所对应的源码行

4.3 多线程程序单步时跳转混乱

单步期间其他线程仍可能运行并触发断点。可以先把线程数设为 1,以判断问题是否与并发有关。


Tips:

  1. 通过启用 debug.showBreakpointsInOverviewRuler 设置,在编辑器右侧的概览标尺中显示断点。
  2. 如果更喜欢在树状视图中查看按文件分组的断点,请将 debug.breakpointsView.presentation 设置配置为 tree
  3. 在聚焦于变量部分时使用键盘快捷键 Ctrl+Alt+F,然后输入搜索词,可按名称或值过滤变量
  4. 调用堆栈可以通过关键词,如blas、omp、mkl、tbb等,判断是哪个库开启的线程

调试优化级别

  1. gcc调试信息等级:
选项描述
-g默认级别,通常等同于 -g2
-g1最少量的调试信息,仅包含行号和最基本的变量/函数信息。
-g2推荐。包含标准调试信息,包括局部变量、函数参数、完整的类型信息等。
-g3包含 -g2 的所有信息,以及宏定义。这会生成最大的调试文件。
  1. gcc 编译优化等级:
选项作用适用场景
-O0默认优化等级,不进行主动优化,保证代码逻辑与源码一致,最适合调试阶段调试、排查逻辑错误
-O1基础优化,适度提高执行效率,同时保留较好的调试能力轻量性能优化
-O2通用优化,平衡执行速度和代码大小,适合大多数场景大多数生产环境
-O3激进优化,追求极致性能,可能增加代码体积,需实测验证高性能场景
-Os优化代码尺寸,适合嵌入式或存储受限场景嵌入式、资源受限
-Ofast-O3 基础上进一步放宽标准兼容性以追求性能,慎用只在确认兼容性可接受时使用
-Og专为调试设计,在提供优化的同时保持较好的调试友好性调试与性能兼顾

在调试阶段,推荐使用 -O0 -g 组合,以便完整生成调试信息并方便断点调试。生产阶段可根据目标平台和性能需求选择 -O2-O3,并结合链接器优化(如 --gc-sections)进一步减少未使用代码。


带参调试

在launch.json的”args” : 中添加值,通过F5或者 “运行和调试” 窗口上方的蓝色三角启动运行,可以传入int mainint argc, char *argv [ ]的argv中,可以根据实际情况调用。

Filed under

Keywords