VSCode 调试 C++

在编写 C++ 代码时,如果遇到了语法错误,编译器会直接指出是哪一行写错了。但在实际开发中,我们还会遇到另一类更棘手的问题:程序能够顺利编译通过,最终算出来的结果却不对,或者直接发生段错误半路闪退。

遇到这类问题时,光靠肉眼盯着代码猜很难找出原因,这时就可以借助调试器(Debugger)。它就像给程序装上了一个 “慢放遥控器”,能让程序在指定位置暂停下来,方便我们一步步观察每个变量的实时数值。

准备调试代码与编译参数

为了方便小伙伴们跟着动手操作,我们先准备一段计算订单应付金额的程序。假设商品单价是 100 元,购买数量是 5 件,原价小计就是 500 元。现在活动打 8 折(即减免 20%),正常情况下最终的应付金额应该是 400 元。

示例 1:计算结果有误的订单程序

#include <iostream>

// 计算打 8 折后的金额
int applyDiscount(int subtotal) {
    int discount = subtotal * 20 / 100;
    return subtotal + discount;
}

// 计算订单总价
int calculateTotal(int unitPrice, int count) {
    int subtotal = unitPrice * count;
    return applyDiscount(subtotal);
}

int main() {
    int total = calculateTotal(100, 5);
    std::cout << "应付金额:" << total << '\n';
    return 0;
}

运行结果如下。

应付金额:600

分析:

在这个例子中,我们调用 calculateTotal(100, 5) 计算 500 元商品打 8 折后的金额。本该算出 400,程序却输出了 600,越打折反而越贵了。现在先假装不知道是哪一行写错了,看看怎么使用 VSCode 调试器把问题找出来。

在 VSCode 中按下 F5 启动调试时,编辑器会在后台自动附带调试参数并编译程序。如果平时在终端手动调用 g++ 编译代码,启动调试器前需要在命令中加上 -g 和 -O0 两个参数,具体写法如下:

g++ -std=c++20 -O0 -g main.cpp -o main

这两个编译参数对单步调试非常关键,各自的作用如下:

  • -g(生成调试信息):让编译器把源代码的行号、变量名等信息一起写进可执行文件里。如果不加 -g,调试器就只认识底层的机器指令,不知道当前对应的是第几行 C++ 代码。
  • -O0(关闭编译优化):注意开头是英文 Optimization(优化)的首字母大写 O,后面紧跟数字 0,不要错敲成两个零。它用来关闭编译器的自动代码优化,确保程序严格按代码顺序逐行执行,防止调试时光标乱跳。

设置断点与启动调试

默认情况下,计算机执行上面那二十几行代码只需要不到一毫秒,一眨眼就跑完了。如果想看清中间的计算过程,我们就需要先在可疑的代码行上放一个 “路障”,让程序运行到这里时自动暂停下来。这个路障就叫做 “断点(Breakpoint)”。

代码默认瞬间执行与设置断点暂停的对比

在 VSCode 中,设置断点并启动调试只需按顺序执行以下 2 步:

  • 设置断点(F9):打开 .cpp 文件,找到 main() 里的 int total = calculateTotal(100, 5); 这一行。在行号左侧空白处单击,或按下 F9 键,出现醒目的 “小红点” 即表示断点设置成功。
  • 启动调试(F5):保持光标停留在 .cpp 文件中,直接按 F5 键,或点击左侧【运行和调试】图标后再点击顶部绿色箭头。程序启动后会自动暂停在打断点的那一行,并以黄色高亮显示。

提示 当程序暂停在黄色高亮行时,表示这一行代码 “正准备执行,但还没有真正执行”。第一次按 F5 调试时如果顶部弹出环境选择框,依次选择【C++ (GDB/LLDB)】和【g++.exe - 生成和调试活动文件】即可。

3 种常用的单步调试操作

当程序在断点处停住后,VSCode 顶部正上方会浮现出一排调试控制按钮。除了最左侧用来继续跳往下一个断点的【继续】(F5)按钮外,最常用的就是旁边的 3 个 “单步执行” 按钮,它们的分工如下表所示。

VSCode 单步调试操作
操作按钮 快捷键 作用说明 适用场景
【单步跳过】 F10 执行完当前行,不进入子函数内部 确定子函数没问题,顺着往下走
【单步调试】 F11 进入当前行调用的子函数第 1 行 怀疑问题在子函数内,进去排查
【单步跳出】 Shift + F11 执行完当前子函数并返回上一层 确认当前子函数无误,快速退回

弄清楚这 3 个按键的区别后,我们就按顺序一步步跟踪刚才的订单程序:

  • 按 F11 进入外层函数:程序当前停在 int total = calculateTotal(100, 5);。怀疑金额是在 calculateTotal() 里算错的,按下 F11 执行【单步调试】,进入该函数内部。
  • 按 F10 执行当前行:光标停在 int subtotal = unitPrice * count;。这只是普通乘法,按下 F10 执行【单步跳过】跑完当前行,光标移到下一行 return applyDiscount(subtotal);。
  • 再按 F11 进入内层函数:此时准备调用 applyDiscount(subtotal) 计算折扣。为了检查打折逻辑,再次按下 F11 执行【单步调试】,进入 applyDiscount() 内部。

查看变量、监视与调用堆栈

进入 applyDiscount() 函数后,我们先按一下 F10 执行完 int discount = subtotal * 20 / 100; 这一行,让程序停在 return subtotal + discount; 上。

此时把目光移到 VSCode 左侧的【运行和调试】侧边栏,这里有 3 个排查问题的重点面板:

  • 【变量】面板:英文界面显示为【VARIABLES】,会自动列出当前函数内所有变量的名字和实时数值,不用写打印语句就能看清内存数据。
  • 【监视】面板:英文界面显示为【WATCH】,支持手动添加自定义表达式。比如点击面板右上角的【+】输入 subtotal - discount,调试器就会实时算出结果。
  • 【调用堆栈】面板:英文界面显示为【CALL STACK】,从上往下展示程序是怎么一层层调用函数走到当前位置的,最顶上一行就是当前暂停所在的函数。

当程序停在 applyDiscount() 的 return subtotal + discount; 这一行时,左侧面板显示的状态如下:

[变量 / VARIABLES 面板]
subtotal : 500
discount : 100

[监视 / WATCH 面板]
subtotal - discount : 400

[调用堆栈 / CALL STACK 面板]
applyDiscount(int subtotal)(当前暂停位置)
calculateTotal(int unitPrice, int count)
main()

观察【变量】面板里的数值可以清楚地看到:原价小计 subtotal 是 500,算出来的折扣金额 discount 是 100,这两步都算对了。

问题恰恰出在当前暂停的 return subtotal + discount; 这一行:打 8 折本该从原价 500 里 “减去” 折扣 100,代码里却写成了 “加上” 折扣 100。同时,【监视】面板里试算的 subtotal - discount 也验证了正确结果是 400。

下方的【调用堆栈】面板在排查多层调用时也很实用。假如你在底层函数里发现传进来的参数一开始就是错的,直接单击面板里的上一层函数 calculateTotal(),就能切换过去查看上一层传参时的变量状态。

找到原因后,小伙伴们要注意:在调试暂停期间直接修改代码,并不会改变当前正在运行的旧程序。我们需要先点击顶部工具栏上的红色方块按钮【停止】(Shift + F5)结束调试,把代码里的加号改成减号并保存(Ctrl + S),再重新运行验证一次。

示例 2:修复折扣逻辑并验证

#include <iostream>

// 修复:打折应使用减法
int applyDiscount(int subtotal) {
    int discount = subtotal * 20 / 100;
    return subtotal - discount;
}

int calculateTotal(int unitPrice, int count) {
    int subtotal = unitPrice * count;
    return applyDiscount(subtotal);
}

int main() {
    int total = calculateTotal(100, 5);
    std::cout << "应付金额:" << total << '\n';
    return 0;
}

运行结果如下。

应付金额:400

分析:

将 return subtotal + discount; 修改为 return subtotal - discount; 并重新运行后,输出结果变回了正确的 400。日常使用调试器排错的基本流程就是:复现错误结果、打断点单步观察数据、停止调试修改代码,最后重新运行验证。

使用调试器定位段错误

前面排查计算错误时,我们是提前在可疑代码行打上断点。但如果遇到空指针或数组越界引发的段错误闪退,程序一运行就崩了,事先并不知道应该在哪一行打断点。

遇到这种情况时,不需要提前打断点,直接在 .cpp 窗口按下 F5 启动调试即可。一旦程序读写了非法内存,调试器会自动拦截崩溃,直接暂停在触发段错误的那一行代码上。此时查看左侧【变量】面板里的指针或下标数值,再点击【调用堆栈】里的上层函数逐层回看,就能快速查出错误数据的源头。

使用条件断点调试循环

在调试循环代码时,还有一个非常省力的实用功能。假设我们在调试一个执行 100 次的 for 循环,而且已知只有当循环变量 i 等于 50 时才会算错数据。

for (int i = 0; i < 100; i++) {
    processOrder(i);    // 只有当 i == 50 时才会出错
}

如果在 processOrder(i); 这一行只打一个普通断点,每次循环程序都会停一下,就得连续按 50 次 F5 才能走到 i == 50 的位置。

遇到这种情况,我们可以使用 “条件断点”,让断点只在满足指定条件时才触发暂停。具体设置只需按顺序执行以下 2 步:

  • 右键编辑断点:先在 processOrder(i); 行号左侧单击打上一个普通的小红点断点,再用鼠标右键单击这个小红点,在弹出菜单中选择【编辑断点...】。
  • 输入触发条件:在弹出的输入框中保持默认的【表达式】模式,输入条件 i == 50 并按下 Enter 键确认。

设置完成后,小红点中间会多出一个等号小图标。此时再按 F5 启动调试,前 50 次循环都会快速跑过。直到 i 变成 50 的那一刻,调试器才会自动停下来供我们检查。检查完毕后如果想把剩下的循环直接跑完,再按一次 F5(【继续】)即可。

常见问题

1. 为什么在 VSCode 中打了小红点断点,运行后程序却没有停下来?

遇到断点不生效的情况,我们可以优先排查以下 2 个常见原因:

启动方式不对:检查是不是按成了 “只运行不调试” 的快捷键(Ctrl + F5),或者点成了编辑器右上角其他插件的普通运行按钮。只有按下 F5 键,或从左侧【运行和调试】面板启动调试,断点才会生效。

编译参数缺少 -g:打开项目下的 .vscode/tasks.json 配置文件检查 args 列表,确认里面包含 "-g" 参数。如果缺少该配置,可以对照前面的 “VSCode 配置 C++ 环境” 一节核对补齐。

2. 按下 F5 启动调试时编译报错,提示找不到文件或无法生成程序,应该怎么解决?

很多小伙伴常踩的一个坑是:刚刚查看完 .vscode/tasks.json 或 launch.json 配置文件,鼠标光标还停留在 .json 窗口里就直接按下了 F5。

由于默认配置编译的是 “当前活动文件”,VSCode 会误把 .json 配置文件交给 g++ 编译。我们只需要先点击编辑器上方标签切回 main.cpp 代码窗口,再按 F5 启动调试就可以了。

3. 在 Windows 下按 F5 启动调试时弹窗报错 “Unable to start debugging”,是什么原因?

这通常是因为项目文件夹路径或 .cpp 文件名中包含了中文字符。Windows 环境下的 GDB 调试器对中文路径兼容性较差,建议大家将项目路径改为像 D:/cpp-tutorial/main.cpp 这样纯英文、无空格的路径,保存后再按 F5 即可正常调试。

上一篇:

下一篇:

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号