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
我曾经犯过一个低级错误: 项目编译通过了,但GDB里死活看不到源码。折腾了半天才发现,是CMakeLists.txt里忘了加 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梳理一下整个流程

下面这张图,把整个调试会话的流程画出来了。你一看就明白:

第一个调试会话流程图 1. 创建Qt项目 main.cpp + 窗口类 2. 编译加 -g 生成调试符号 3. 启动GDB gdb ./my_debug_demo 4. 设置断点 break mainwindow.cpp:22 5. 启动程序 run 6. 触发断点 点击按钮 → 停在22行 7. 单步执行 next / step / finish 8. 检查变量 print m_counter 9. 继续执行 continue / 再次点击 可重复触发断点,观察变量变化

这张图把整个流程串起来了。你从创建项目开始,到编译、启动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项目,设置了断点,启动了调试,还单步执行并观察了变量的变化。这个过程虽然简单,但它是所有复杂调试的基础。以后你遇到再难的问题,本质上都是这个流程的延伸。

记住:调试不是玄学,是一步一步验证你的假设。你怀疑哪里有问题,就在那里设个断点,跑一下,看看变量是不是你期望的值。不是?那就找到了。

可可精品资料库