3. Qt特有类型打印

调试Qt程序时,最头疼的是什么?我个人觉得,就是那些Qt特有的容器和字符串类型。你用print命令一看,满屏都是内部指针和引用计数,根本看不到实际数据。嗯,今天我们就来彻底解决这个问题。

3.1 为什么直接打印Qt类型会“乱码”?

先看个例子。你写了个简单的Qt程序:

QString name = "Hello Qt";
int value = 42;
qDebug() << name << value;  // 输出正常

在GDB里用print name,你看到的可能是:

(gdb) print name
$1 = {d = 0x555555559280}

就一个指针地址?数据呢?

说白了,QString内部用的是隐式共享(写时复制)机制。它不直接存储字符数组,而是存一个指向QStringData的指针。GDB默认不知道该怎么解析这个结构。

我在项目中遇到过这种情况:一个QString明明赋值了,但打印出来就是个地址。当时排查了半天,以为是赋值没成功,后来才发现是GDB不会“翻译”Qt类型。从那以后,我就养成了配置Qt打印脚本的习惯。

3.2 配置Qt打印脚本(一步到位)

Qt官方其实提供了GDB的打印脚本,就在Qt安装目录里。你只需要加载它:

(gdb) source /path/to/Qt/版本号/gcc_64/share/qtcreator/debugger/qttypes.py

加载之后,再试试:

(gdb) print name
$1 = "Hello Qt"

是不是清爽多了?

小技巧: 把这条命令加到 ~/.gdbinit 文件里,每次启动GDB自动加载,省得每次都敲一遍。

3.3 QString的打印技巧

有时候你不想加载整个脚本,或者环境受限。那怎么办?手动打印呗。

QString的内部结构大致是这样的:

struct QStringData {
    QtPrivate::RefCount ref;
    int size;
    uint alloc;
    uint capacity;
    qptrdiff offset; // 到字符数据的偏移
    // 字符数据紧随其后
};

你可以这样手动获取字符串内容:

(gdb) print name.d_ptr->data()
$2 = 0x5555555592a0 "Hello Qt"

或者更直接一点:

(gdb) print name.toStdString()
$3 = "Hello Qt"

我个人习惯用toStdString(),因为GDB能直接调用Qt的成员函数。不过要注意,这需要程序在运行时能正常调用该函数,不能是崩溃后的core dump。

注意: 在core dump文件中调用成员函数可能失败,因为程序已经终止。这时候只能用d_ptr->data()这种原始方式。

3.4 QList的打印技巧

QList的打印比QString复杂一点。直接print会看到:

(gdb) print list
$4 = {d = 0x5555555593c0}

加载Qt脚本后,可以这样:

(gdb) print list
$5 = QList<int>(3, {1, 2, 3})

手动打印的话,可以用:

(gdb) print list.size()
$6 = 3
(gdb) print list[0]
$7 = 1
(gdb) print list[1]
$8 = 2

为什么能直接用下标?因为GDB会调用operator[]。但要注意,如果列表元素是QString,打印出来可能还是地址,需要再套一层toStdString()。

我曾经调试过一个QList<QString>,里面存了上百个文件名。用print list直接打印,终端刷屏了。后来我学会了用print list.size()先看大小,再用print list[0]、print list[1]逐个检查,效率高多了。

3.5 QMap的打印技巧

QMap是键值对容器,打印起来更麻烦。加载脚本后:

(gdb) print map
$9 = QMap<QString, int>(2, {["apple"] = 5, ["banana"] = 3})

手动打印的话,可以这样遍历:

(gdb) print map.size()
$10 = 2
(gdb) print map.keys()
$11 = {"apple", "banana"}
(gdb) print map.values()
$12 = {5, 3}

或者直接查某个键:

(gdb) print map["apple"]
$13 = 5

这里有个坑:如果键不存在,map["key"]会插入一个默认值。所以查之前最好先用contains()确认:

(gdb) print map.contains("orange")
$14 = false

3.6 QVector的打印技巧

QVector和QList类似,但内部是连续内存存储。加载脚本后:

(gdb) print vec
$15 = QVector<double>(4, {3.14, 2.71, 1.41, 1.73})

手动打印:

(gdb) print vec.size()
$16 = 4
(gdb) print vec.data()[0]
$17 = 3.14
(gdb) print vec.data()[1]
$18 = 2.71

data()返回的是底层数组指针,用下标访问效率很高。我在项目中经常用这个技巧来检查大数据量的QVector,比如图像像素数据。

3.7 知识体系一览

下面这张图总结了本章的核心内容,方便你快速回顾:

Qt类型GDB打印技巧 方法一:加载Qt打印脚本 方法二:手动打印技巧 命令: source qttypes.py 自动解析所有Qt类型 推荐:加到 ~/.gdbinit 中 QString: toStdString() QList: list[0], list.size() QMap: map["key"], map.keys() QVector: vec.data()[0] 四种常用Qt类型 QString • 隐式共享 • d_ptr->data() • toStdString() • 注意core dump QList • 连续存储 • list[下标] • list.size() • 避免刷屏 QMap • 键值对 • map["key"] • map.keys() • 先contains() QVector • 连续内存 • vec.data()[下标] • vec.size() • 适合大数据

3.8 避坑指南

最后,分享几个我踩过的坑:

  • core dump里别用成员函数:程序都挂了,还调用toStdString()?大概率会报错。这时候老老实实用d_ptr->data()。
  • QMap用[]要小心:我曾经在调试时用了print map["nonexist"],结果程序里多了一个空值键值对,排查了好久才发现是GDB“帮”我插入的。
  • 大数据容器别直接print:一个QList里存了10万个元素,你直接print,终端卡死别怪我。先看size(),再取前几个看看就够了。
  • 脚本版本要匹配:Qt4和Qt5的qttypes.py不一样,别拿Qt5的脚本去调试Qt4程序,会报错。

嗯,掌握了这些技巧,调试Qt程序时再也不用对着满屏的指针发呆了。下次遇到QString、QList这些类型,直接上手,心里有底。

可可精品资料库