C++ 调试实战(03):查看与修改变量——print、display、STL 容器与调用函数
程序停下来之后,最常做的事就是「看看现在变量是多少」。调试器在这方面比 printf 强太多:随时想看哪个就看哪个,结构体、容器一次全展开,可以按十六进制、二进制显示,可以算表达式,可以每走一步自动打印,甚至可以直接改掉变量的值、调用程序里的函数,看看「如果是这样会怎样」。
这是「C++ 调试实战」系列的第 3 篇。示例程序仍然是第 00 篇的
orders.cpp。前两篇讲了断点和单步,这一篇讲停下来之后怎么看数据。
本篇大部分操作都停在 line_total 的第 16 行 return total;(break orders.cpp:16,然后 run),第一次命中时处理的是 apple(3 件 × 2.5)。
一、print:打印变量和表达式
1.1 基本用法
1 | (gdb) print o |
print(缩写 p)后面可以跟任何合法的 C++ 表达式:变量、成员访问、算术、比较、取地址、解引用、类型转换都行。输出里的几个部分:
| 部分 | 含义 |
|---|---|
$1` | 值历史编号,后面可以用 `$1 引用这个结果 |
|
(const Order &) |
类型:o 是一个 const Order& 引用 |
@0xaaaaaaad3020 |
引用所绑定对象的地址 |
{name = "apple", ...} |
对象内容,std::string 自动显示成字符串 |
1.2 一次看全部参数和局部变量
1 | (gdb) info args |
刚停下来、还不知道该看什么的时候,先 info args 和 info locals 扫一眼,非常高效。
1.3 LLDB:frame variable 与 p
LLDB 有两套查看变量的命令,这一点和 GDB 不同:
1 | (lldb) frame variable |
| 命令 | 作用 | 特点 |
|---|---|---|
frame variable(缩写 v) |
读取变量 | 不执行任何代码,直接按调试信息读内存,快且安全;只支持变量名、成员 .、->、下标 [] 等简单形式 |
p / expression(expr) |
计算表达式 | 真正编译并在目标进程里执行一段代码,功能强,但可能失败(见第九节) |
frame variable 不带参数时,等于 GDB 的 info args + info locals。日常看变量优先用 v,需要算表达式或调函数时再用 p。
二、格式化输出
2.1 GDB:print/格式
1 | (gdb) print/x o.quantity |
| 格式 | 含义 |
|---|---|
/x |
十六进制 |
/d |
有符号十进制 |
/u |
无符号十进制 |
/t |
二进制 |
/o |
八进制 |
/c |
字符 |
/f |
浮点数 |
/a |
地址(顺便显示它指向哪个符号) |
看位标志(flags)、位掩码、网络字节序的时候,/x 和 /t 非常好用。
2.2 LLDB:-f
1 | (lldb) v -f x o.quantity |
-f 的常用取值:x(hex)、d(decimal)、b(binary)、c(char)、f(float)。p 也支持同样的写法,例如 p/x o.quantity。
2.3 结构体换行显示
结构体字段多了挤在一行很难看,GDB 可以打开「漂亮打印」:
1 | (gdb) set print pretty on |
LLDB 默认就是多行显示结构体。
三、查看类型:ptype 与 whatis
1 | (gdb) ptype o |
whatis 只告诉你类型名,ptype 把类型展开,列出所有成员(对类还会列出成员函数)。碰到 auto 推导、模板实例化,想知道「这个变量到底是什么类型」时很有用。
LLDB 用 type lookup,或者 v -T 在输出里带上类型:
1 | (lldb) type lookup Order |
顺带一个有意思的对比:
1 | (gdb) print sizeof(Order) |
1 | (lldb) p sizeof(Order) |
同一个结构体,Linux 上 48 字节、macOS 上 40 字节。原因是 GCC 的 libstdc++ 里 std::string 占 32 字节,而 Clang 的 libc++ 里只占 24 字节。
四、STL 容器
4.1 自动展开
用 up 回到 sum_orders 这一帧(第 04 篇细讲),打印整个 vector:
1 | (gdb) print orders |
1 | (lldb) v orders |
之所以能显示成 std::vector of length 3 而不是一堆内部指针,是因为调试器带了针对标准库的格式化器:GDB 用的是 libstdc++ 附带的 Python pretty printer,LLDB 内置了 libc++ / libstdc++ 的数据格式化器。map、set、unordered_map、optional、variant、shared_ptr 等常见类型都支持。
如果你想看容器的「真实内部结构」(比如调试自定义分配器),GDB 用 print/r orders(raw,不用 pretty printer),LLDB 用 v --raw orders。
4.2 @:把连续内存当数组看
GDB 的 @ 运算符表示「从这个元素开始,连续取 N 个」:
1 | (gdb) print orders[0]@2 |
对于 C 风格数组或 new 出来的指针(int* p = new int[100];),print *p@10 可以看前 10 个元素,这是 @ 最常见的用途。LLDB 对应的是 parray 10 p。
五、值历史与便利变量
每次 print 的结果都会存进 $1`、`$2……,$ 表示上一个结果:
1 | (gdb) print orders.size() |
之前 finish 显示的 Value returned is $2 = 7.5,也是存进了值历史,可以接着用。
LLDB 里要用 expr 才会生成 $0`、`$1 这样的结果变量(较新的 LLDB 里 p 对简单变量走的是 frame variable 的路子,不产生结果变量):
1 | (lldb) expr total * 2 |
GDB 还可以自己定义便利变量,比如 set $limit = 10`,然后在条件断点里用 `break line_total if o.quantity >= $limit。
六、display:每次停下都自动打印
单步调试时,如果每走一步都要手动 print sum,很快就会烦。display 让调试器每次停下都自动打印:
在 sum_orders 的第 22 行下断点,第一次停下时(此时 o 是 apple)加两个自动显示:
1 | (gdb) display sum |
每次停下(不管是单步还是命中断点),两个表达式都自动打印。第二次停下时 o 已经换成了 banana,数量 12 显示成十六进制 0xc。
交互式 GDB 在执行
display的那一刻就会先打印一次当前值。
不需要了就 undisplay 1(删除 1 号),或者 disable display 1 暂时关掉。
LLDB 的 display 其实是用「停止钩子」(stop hook)实现的:
1 | (lldb) display sum |
每次进程停下,钩子就执行一次 expr sum。(这里的 sum 是 100 而不是 7.5,是因为这段 LLDB 会话里先做了下一节要讲的「修改变量」。)
七、修改变量:set var
调试器不仅能看,还能改。假设你怀疑「如果 apple 的小计是 100,后面的逻辑会不会出问题」,不用改代码重新编译,直接改:
1 | (gdb) set var total = 100 |
函数返回了 100 而不是 7.5,程序会带着这个「假数据」继续跑下去。删掉断点后 continue,程序输出的就是 total = 130.8(100 + 10.8 + 20)。
LLDB 直接用表达式赋值:
1 | (lldb) expr total = 100 |
常见用途:
- 验证修复思路:把一个错误的值改成正确的,看后续逻辑是否就对了;
- 强制走某个分支:把条件变量改掉,测试平时难以触发的错误处理代码;
- 跳过耗时操作:把循环计数器直接改到接近结束。
GDB 里
print total = 100也能改值,但推荐用set var:set后面直接跟变量名时,如果变量名恰好和 GDB 的某个设置项同名(比如width),会被当成 GDB 设置。
八、调用程序里的函数
print 里可以直接调用被调试程序的函数:
1 | (gdb) print line_total(orders[2]) |
cherry:5 × 4.0 = 20,没打折。这等于在当前现场「插入」了一次函数调用,特别适合:
- 测试某个函数对特定输入的返回值;
- 调用你专门写的调试辅助函数,比如
dump_tree(root),在调试时打印复杂数据结构。
8.1 坑一:被调用的函数里有断点
如果被调函数里有断点,调用会在断点处停下,GDB 会放弃这次表达式求值:
1 | (gdb) print line_total(orders[2]) |
此时你停在了一个「嵌套」的调用里。解决办法:调用前先 disable 掉相关断点。LLDB 默认在表达式求值时忽略断点,不会遇到这个问题:
1 | (lldb) p line_total(o) |
8.2 坑二:函数被内联了,根本不存在
这是 C++ 调试里最常见、也最让人困惑的错误:
1 | (gdb) print *orders.data()@2 |
orders.size()、orders[1] 都能用,orders.data() 却不行。原因是:std::vector 的成员函数都是模板,只有程序里真正用到的才会被实例化、编译出函数体。我们的代码从来没调用过 data(),程序里根本没有这个函数的机器码,调试器自然调不了。至于 size() 和 operator[] 为什么可以,要么是程序里恰好实例化了,要么是 GDB 用 libstdc++ 提供的 xmethod(见下表)顶替了真正的函数调用。
LLDB 在 macOS 上更明显。libc++ 的很多小函数被标记成了总是内联,即使 -O0 也不会生成独立的函数体:
1 | (lldb) p orders[1] |
p orders[1] 失败,是因为对 vector 用 [] 在表达式里其实是调用 operator[] 这个函数,而这个函数不存在。但同样的写法换成 v 就没问题:
1 | (lldb) v orders[1] |
因为 frame variable 不执行代码,它的 [1] 走的是格式化器提供的「合成子元素」,直接读内存。
遇到「函数不存在」的错误时,按这个顺序尝试:
| 办法 | 说明 |
|---|---|
LLDB 换用 v |
容器下标、成员访问都能用 v 完成 |
| 直接看字段 | 不调 size(),看容器格式化器显示的 length / size= |
| GDB 的 xmethod | 较新的 libstdc++ 为 vector::size()、operator[] 等提供了 Python 实现的「xmethod」,GDB 在函数不存在时会用它顶替 |
| 在代码里显式用一次 | 调试辅助代码里写一句 (void)v.data(); 强制实例化(不推荐常用) |
九、看原始内存:x 与 memory read
有时需要绕过类型,直接看内存里的字节,比如排查越界写、字节序、结构体填充:
1 | (gdb) print &orders[0].quantity |
x/2dw 的意思是:从这个地址开始,显示 2 个单元,按 d(十进制)显示,每个单元 w(word,4 字节)。第一个是 quantity = 3;第二个 4 字节是 quantity 和 price 之间的填充(double 要 8 字节对齐),值没有意义。
x 参数 |
可选值 |
|---|---|
| 数量 | 任意正整数 |
| 格式 | x 十六进制、d 十进制、u 无符号、c 字符、s 字符串、i 指令 |
| 单元大小 | b 1 字节、h 2 字节、w 4 字节、g 8 字节 |
LLDB:
1 | (lldb) memory read -s4 -fd -c2 &o.quantity |
-s4 单元大小 4 字节、-fd 十进制、-c2 两个单元。LLDB 也兼容 GDB 的写法:x/2dw &o.quantity。这里第二个值是 1,同样是填充字节里的残留值,和 GDB 那边的 0 一样没有意义——填充字节的内容是不确定的。
十、小结
| 需求 | GDB | LLDB |
|---|---|---|
| 打印变量 / 表达式 | print x / p a + b |
v x / p a + b |
| 所有参数和局部变量 | info args / info locals |
frame variable / v |
| 十六进制 / 二进制 | p/x x / p/t x |
v -f x x / v -f b x |
| 查看类型 | ptype x / whatis x |
type lookup T / v -T x |
| 结构体换行显示 | set print pretty on |
默认如此 |
| 连续 N 个元素 | p *ptr@N / p arr[0]@N |
parray N ptr |
| 每步自动打印 | display x / undisplay 1 |
display x / undisplay 1 |
| 修改变量 | set var x = 1 |
expr x = 1 |
| 调用函数 | p f(arg) |
p f(arg) |
| 看内存 | x/4xw &x |
memory read -s4 -fx -c4 &x |
最后记住一条经验:LLDB 里 v 看变量,p 算表达式;遇到「函数不存在」「may be inlined」,别怀疑人生,是模板没实例化或被内联了。
C++ 调试实战系列第 3 篇完。下一篇:调用栈与栈帧——backtrace、frame、up / down,搞清楚「我是怎么走到这里的」。








