C++ 调试实战(01):断点——让程序停在你想停的地方
调试的第一步永远是「让程序停下来」。断点(breakpoint)就是你插在代码里的一面小旗:程序跑到这里就暂停,把现场交给你检查。这一篇把断点的各种用法讲全:按函数、按行号、条件断点、临时断点、忽略次数,以及启用、禁用、删除,最后再介绍两种「停下来但不打扰你」的断点——命中时自动执行命令的断点和 dprintf。
这是「C++ 调试实战」系列的第 1 篇。示例程序沿用第 00 篇的
orders.cpp,Linux 版用g++ -std=c++20 -g -O0 orders.cpp -o orders编译,macOS 版编译为orders.mac。
先回顾一下 orders.cpp 的关键行号:
| 行号 | 代码 |
|---|---|
| 11–17 | double line_total(const Order& o):12 行计算小计,14 行打九折,16 行 return total; |
| 19–25 | double sum_orders(const std::vector<Order>& orders):22 行 sum += line_total(o); |
| 27–36 | main():33 行 double total = sum_orders(orders); |
一、先看代码:list
下断点之前,往往想先看看代码、确认行号。不用切回编辑器,调试器里就能看:
1 | (gdb) list line_total |
LLDB 的写法一样(list line_total,完整形式是 source list -n line_total)。常见用法:
| 命令 | 作用 |
|---|---|
list 函数名 |
显示函数附近的代码 |
list 20 / list orders.cpp:20 |
显示第 20 行附近 |
list |
接着上次的位置往下显示 |
二、三种最常用的下断点方式
2.1 按函数名
1 | (gdb) break line_total |
1 | (lldb) b line_total |
注意断点落在第 12 行,而不是函数声明所在的第 11 行:调试器会自动跳过函数的「序言」(prologue,保存寄存器、分配栈空间的那几条指令),停在函数体第一行真正的代码上。
函数名下断点最省心:不怕你改代码导致行号变化,只要函数名不变就能停住。
2.2 按文件名 + 行号
1 | (gdb) break orders.cpp:22 |
1 | (lldb) b orders.cpp:22 |
LLDB 的完整写法是 breakpoint set --file orders.cpp --line 22(缩写 br s -f orders.cpp -l 22)。
2.3 只写行号
程序停下来之后,**只写行号就表示「当前文件」**的那一行:
1 | (lldb) b 16 |
GDB 同样支持 break 16。
2.4 C++ 函数名的写法
C++ 有命名空间、类成员、重载,函数名写法需要多说一句:
| 场景 | 写法示例 |
|---|---|
| 普通函数 | break line_total |
| 类成员函数 | break Account::deposit |
| 命名空间里的函数 | break school_db::open |
| 重载函数只停某一个 | break parse(const std::string&)(GDB) |
对于重载函数,只写名字时调试器会在所有同名函数上都下断点,info breakpoints 里会看到一个断点带多个位置(1.1、1.2……)。想只停其中一个,GDB 里写完整参数列表;LLDB 里可以下完之后用 breakpoint disable 1.2 禁掉不需要的位置。
三、查看断点列表
3.1 GDB:info breakpoints
1 | (gdb) info breakpoints |
每一列的含义:
| 列 | 含义 |
|---|---|
Num |
断点编号,后续启用 / 禁用 / 删除都用它 |
Type |
breakpoint 普通断点;还有 hw watchpoint(观察点)、dprintf 等 |
Disp |
命中后的处置:keep 保留;del 命中一次就删除(临时断点) |
Enb |
是否启用:y / n |
Address |
断点所在的机器指令地址 |
What |
对应的函数和源码位置 |
程序运行、断点命中后再看,会多出命中次数,地址也变成了真实加载地址(程序启动前 GDB 只知道相对偏移):
1 | (gdb) info breakpoints |
3.2 LLDB:breakpoint list
1 | (lldb) breakpoint list |
(这份列表里的条件断点和 one-shot 断点,下面马上会讲到。)LLDB 的每个断点可以有多个位置(location),编号写作 1.1、3.1;unresolved 表示程序还没运行、地址还没最终确定,hit count 是命中次数。缩写:br l。
四、运行到断点
断点下好之后:
| 命令 | 作用 |
|---|---|
run / r |
从头启动程序,跑到第一个命中的断点 |
run arg1 arg2 |
带命令行参数启动(也可以启动调试器时写 gdb --args ./prog arg1 arg2,LLDB 是 lldb -- ./prog arg1 arg2) |
continue / c |
从当前位置继续,跑到下一个命中的断点 |
1 | (gdb) run |
run 之后先停在断点 2(sum_orders 的第 22 行),bt 看一眼调用栈(第 04 篇细讲),continue 之后进入 line_total,停在断点 1。
停下来的那一行还没有执行。 这是初学者最容易搞混的地方:「停在第 12 行」的意思是「第 12 行即将执行」,此时 total 还是未初始化的值。
五、临时断点:只停一次
有时只想停一次,比如「先停在 main 开头,然后再说」。临时断点命中一次后自动删除:
1 | (gdb) tbreak main |
info breakpoints 里它的 Disp 列是 del。LLDB 同样有 tbreak,完整写法是 breakpoint set --one-shot true --name main:
1 | (lldb) tbreak main |
GDB 还有一个更顺手的 start 命令:等价于 tbreak main + run,下一篇会用到。
六、条件断点:只在满足条件时停
line_total 每笔订单都会调用一次。如果只关心「满 10 件打折」的那笔,每次都停就太烦了。条件断点只在表达式为真时才停:
1 | (gdb) break line_total if o.quantity >= 10 |
直接跳过了 apple,停在 banana(数量 12)。info breakpoints 里会显示条件:
1 | 1 breakpoint keep y 0x0000aaaaaaaa19b0 in line_total(Order const&) at orders.cpp:12 |
给已有断点加条件、改条件、去掉条件:
1 | (gdb) condition 1 o.quantity >= 10 # 设置 / 修改条件 |
LLDB 用 -c 选项:
1 | (lldb) br set -n factorial -c 'n == 1' # 下断点时就带条件 |
几个实用的条件写法:
| 场景 | 条件 |
|---|---|
| 某个循环变量到特定值 | i == 999 |
| 指针为空 | p == nullptr 或 p == 0 |
比较 std::string |
GDB:$_streq(o.name.c_str(), "banana")`(`$_streq 是 GDB 内置的便利函数,本文环境实测可用) |
每次经过断点,调试器都要暂停程序、算一遍条件、再恢复运行,放在执行几百万次的热循环里会明显变慢。条件里调用函数(比如上面的
c_str())还有另一个风险:模板库里的小函数经常被内联,程序里根本没有这个函数的实体,调试器调用不了,第 03 篇会专门讲这个坑。能比较普通字段(例如o.quantity == 12)时,优先比较字段。
七、忽略次数:跳过前 N 次
另一个常见需求是「前两次别停,第三次再停」:
1 | (gdb) break line_total |
前两次(apple、banana)被忽略,第三次(cherry)才停。注意命中次数是 3——被忽略的也算命中。
LLDB 用 -i:
1 | (lldb) b line_total |
一个排查技巧:程序在循环第 N 次时出错但不知道 N 是多少,可以先 ignore 1 100000(一个足够大的数),让程序一路跑到出错,然后 info breakpoints 看命中了多少次,下次就能精确地停在出错前一次。
八、禁用、启用、删除
断点多了之后要管理。暂时不想让它停,就禁用;彻底不要了,就删除:
1 | (gdb) disable 1 |
Enb 列从 y 变成 n,条件和命中次数都保留着;delete 之后就彻底没了。
LLDB:
1 | (lldb) breakpoint disable 1 |
| 操作 | GDB | LLDB |
|---|---|---|
| 禁用 1 号 | disable 1 |
breakpoint disable 1(br dis 1) |
| 启用 1 号 | enable 1 |
breakpoint enable 1(br en 1) |
| 删除 1 号 | delete 1(d 1) |
breakpoint delete 1(br del 1) |
| 删除全部 | delete |
breakpoint delete(会要求确认,加 -f 跳过) |
| 只启用一次 | enable once 1 |
—— |
| 禁用 / 删除多个 | disable 1 3、delete 2-4 |
br dis 1 3 |
禁用比删除更常用:你辛辛苦苦配好的条件断点,删掉就得重新敲,禁用随时还能恢复。
九、不停下来的断点
有时候你并不想停,只是想知道「程序经过这里时,某个变量是多少」——也就是在不改代码的情况下临时加一行打印。
9.1 断点命令(commands)
GDB 可以给断点挂一串命令,命中时自动执行。配合 silent(不打印「Breakpoint 1, …」那一行)和 continue(执行完继续跑),就成了一个「打印点」:
1 | (gdb) break line_total |
9.2 dprintf:动态 printf
更简洁的是 dprintf,语法和 C 的 printf 一样:
1 | (gdb) dprintf orders.cpp:16,"total=%f\n", total |
两个一起用,跑一遍:
1 | (gdb) run |
程序全程没有停,但每笔订单的名字和小计都打印出来了。info breakpoints 里能看到挂在断点上的命令:
1 | 1 breakpoint keep y 0x00000000000019b0 in line_total(Order const&) at orders.cpp:12 |
9.3 LLDB 的写法
LLDB 用 breakpoint command add 挂命令,用 -G true(--auto-continue)让断点执行完命令后自动继续:
1 | (lldb) b 16 |
这就是不用改代码、不用重新编译的「printf 调试」。
十、小结
| 需求 | GDB | LLDB |
|---|---|---|
| 按函数下断点 | break line_total |
b line_total |
| 按文件行号 | break orders.cpp:22 |
b orders.cpp:22 |
| 当前文件行号 | break 16 |
b 16 |
| 临时断点 | tbreak main |
tbreak main |
| 条件断点 | break f if x > 0 / condition 1 x > 0 |
br set -n f -c 'x > 0' / br modify -c 'x > 0' 1 |
| 忽略前 N 次 | ignore 1 N |
br modify -i N 1 |
| 查看断点 | info breakpoints |
breakpoint list |
| 禁用 / 启用 / 删除 | disable / enable / delete |
br disable / br enable / br delete |
| 打印点 | dprintf 文件:行,"格式",变量 |
br command add -o '命令' + br modify -G true |
C++ 调试实战系列第 1 篇完。下一篇:运行与单步——next、step、finish、until,一行一行走、钻进函数、跑到函数返回。








