3、第一个调试会话:创建Qt项目、设置断点、启动调试、单步执行
说实话,很多初学者学GDB调试,第一步就卡住了——不是不会敲命令,而是不知道从哪开始。我当年也是这样,对着黑乎乎的终端发呆,心想「这玩意儿到底怎么用?」
今天我们就来走一遍完整的流程。从创建一个Qt项目开始,到设置断点、启动调试,再到单步执行。你跟着我走一遍,就会发现——其实没那么玄乎。
3.1 先搭个「实验台」:创建一个简单的Qt项目
调试的前提是你得有个能跑的程序。我们别搞太复杂,一个简单的QWidget窗口加上一个按钮就够了。
我个人习惯用命令行创建项目,这样更干净。你也可以用Qt Creator,但为了配合GDB,我建议你先把项目结构搞清楚。
创建一个目录,比如叫 my_debug_demo,里面放三个文件:
my_debug_demo/
├── main.cpp
├── mainwindow.h
└── mainwindow.cpp
main.cpp 内容如下:
#include <QApplication>
#include "mainwindow.h"
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
MainWindow w;
w.show();
return app.exec();
}
mainwindow.h:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QPushButton>
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
void onButtonClicked();
private:
QPushButton *m_button;
int m_counter;
};
#endif
mainwindow.cpp:
#include "mainwindow.h"
#include <QMessageBox>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent), m_counter(0)
{
m_button = new QPushButton("点击我", this);
m_button->setGeometry(50, 50, 100, 30);
connect(m_button, &QPushButton::clicked,
this, &MainWindow::onButtonClicked);
}
MainWindow::~MainWindow()
{
}
void MainWindow::onButtonClicked()
{
m_counter++;
QString msg = QString("点击次数: %1").arg(m_counter);
QMessageBox::information(this, "提示", msg);
}
嗯,这里要注意:m_counter 这个成员变量,就是我们待会儿要盯着看的东西。调试的时候,你会亲眼看到它怎么从0变成1、变成2……
3.2 编译时别忘了加「-g」
这是新手最容易踩的坑。如果你编译的时候不加 -g 选项,那GDB就看不到源码、看不到变量名,只能看到一堆汇编指令——那调试起来就痛苦了。
用qmake的话,在 .pro 文件里加上:
QMAKE_CXXFLAGS += -g
QMAKE_CFLAGS += -g
用CMake的话:
set(CMAKE_BUILD_TYPE Debug)
或者直接手动编译:
g++ -g -o my_debug_demo main.cpp mainwindow.cpp \
-I/usr/include/qt5 -I/usr/include/qt5/QtWidgets \
-lQt5Widgets -lQt5Core -lQt5Gui
set(CMAKE_BUILD_TYPE Debug)。你想想看,GDB没有符号表,就像你拿着一本地图册但所有地名都被涂掉了——根本没法定位。
3.3 设置断点:让程序在关键位置「停下来」
断点是什么?说白了就是一个「停车标志」。你告诉GDB:程序跑到这一行,你给我停住,我要看看周围的情况。
启动GDB,加载我们的程序:
gdb ./my_debug_demo
进入GDB界面后,设置断点:
(gdb) break mainwindow.cpp:22
Breakpoint 1 at 0x4012a0: file mainwindow.cpp, line 22.
这里的 22 是 onButtonClicked 函数里 m_counter++; 那一行的行号。你也可以用函数名来设断点:
(gdb) break MainWindow::onButtonClicked
Breakpoint 2 at 0x4012a0: file mainwindow.cpp, line 22.
我个人习惯用「文件名:行号」的方式,因为更精确。有时候函数名可能重载,或者有匿名命名空间,用行号不会搞混。
info breakpoints 查看当前所有断点。用 delete 1 删除编号为1的断点。用 disable 2 临时禁用断点2,而不是删除它——这在调试复杂逻辑时特别有用。
3.4 启动调试:让程序跑起来
断点设好了,接下来启动程序:
(gdb) run
程序窗口会弹出来。你点击那个「点击我」按钮——嗯,GDB终端立刻就有反应了:
Breakpoint 1, MainWindow::onButtonClicked (this=0x55555556b280)
at mainwindow.cpp:22
22 m_counter++;
看到没?程序停在了第22行,也就是 m_counter++; 这一行。GDB告诉你:断点1被触发了,当前在 MainWindow::onButtonClicked 函数里,文件是 mainwindow.cpp,第22行。
为什么会停在这里?因为你点击按钮时,Qt框架内部调用了 onButtonClicked 这个槽函数。GDB在函数入口处截住了它。
3.5 单步执行:一步一步看代码怎么走
现在程序停住了,我们可以「单步执行」。常用的命令有三个:
| 命令 | 缩写 | 作用 |
|---|---|---|
| step | s | 步入:进入函数内部 |
| next | n | 步过:不进入函数,直接执行完当前行 |
| finish | fin | 执行到当前函数返回 |
我们先试试 next:
(gdb) next
23 QString msg = QString("点击次数: %1").arg(m_counter);
程序执行了 m_counter++;,现在停在第23行。我们来检查一下 m_counter 的值:
(gdb) print m_counter
$1 = 1
看到了吗?m_counter 从0变成了1。这就是调试的魅力——你亲眼看着变量在变化。
再执行一次 next:
(gdb) next
24 QMessageBox::information(this, "提示", msg);
现在程序准备弹出消息框。如果你用 step 而不是 next,GDB会钻进 QMessageBox::information 这个函数内部——那里面是Qt的源码,几百行,你确定想看吗?
next 跳过库函数。只有当你怀疑Qt库本身有bug时,才用 step 进去看。我在项目中遇到过几次,以为是自己的逻辑错了,结果step进去发现是Qt某个版本的已知问题——嗯,那种感觉挺复杂的。
继续执行 next,消息框弹出来,你点「确定」后,程序会回到事件循环,等待下一次按钮点击。
你再点一次按钮,断点又会触发。这时候 print m_counter 会显示 $2 = 2。每次点击,计数器加1。
3.6 用SVG梳理一下整个流程
下面这张图,把整个调试会话的流程画出来了。你一看就明白:
这张图把整个流程串起来了。你从创建项目开始,到编译、启动GDB、设断点、运行、触发断点、单步执行、检查变量,最后继续执行——形成一个完整的闭环。
3.7 几个实用的小命令
调试过程中,这几个命令你一定会频繁用到:
- list (l):显示当前行附近的源码。比如
list 20,30显示20到30行。 - print (p):打印变量或表达式的值。比如
print m_counter、print &m_button。 - display:每次停顿时自动显示某个变量。比如
display m_counter,之后每次断点触发,GDB都会自动打印它的值。 - info locals:显示当前函数的所有局部变量。
- backtrace (bt):查看函数调用栈。当你不知道程序是怎么跑到当前这行时,用
bt一看就明白了。
run,触发后 display m_counter,然后 continue。这样每次点击按钮,GDB自动打印计数器的值,不用每次都手动敲 print。省事。
3.8 退出调试
调试完了,用 quit 或 q 退出GDB。如果程序还在运行,GDB会问你是否要终止——输入 y 确认即可。
(gdb) quit
A debugging session is active.
Inferior 1 [process 12345] will be killed.
Quit anyway? (y or n) y
好了,第一个调试会话就完成了。你亲手创建了一个Qt项目,设置了断点,启动了调试,还单步执行并观察了变量的变化。这个过程虽然简单,但它是所有复杂调试的基础。以后你遇到再难的问题,本质上都是这个流程的延伸。
记住:调试不是玄学,是一步一步验证你的假设。你怀疑哪里有问题,就在那里设个断点,跑一下,看看变量是不是你期望的值。不是?那就找到了。