对于Qt:信号与槽的解析

开篇介绍:

hello 大家,本篇博客我们来学习qt中的一个非常重要非常关键的的知识点,也是实现gui与用户进行交互的重中之重,那就是信号与槽~

话不多说,我们正式开始!


第一章 先搞懂本质:信号与槽到底是个什么东西?

在写任何一行代码之前,我们必须先搞懂一个最核心的问题:Qt 为什么要搞出一个 "信号与槽" 的东西?它到底解决了什么问题?它在代码里到底对应着什么?

很多新手学信号与槽,上来就抄 connect 代码,结果连信号是什么、槽是什么都搞不清,写出来的代码自然是错漏百出。

1.1 用生活里的例子,一秒搞懂信号与槽的核心逻辑

我们先完全抛开代码,用你每天都会遇到的生活场景,来理解信号与槽,你会发现它的逻辑和我们的生活完全一模一样。

场景 1:你点外卖的全过程,就是一套完美的信号与槽机制

我们把点外卖的过程拆解开,你会发现每一步都对应着信号与槽的核心要素:

  1. 事件发生:你肚子饿了,打开外卖 APP,选好了餐,点击了「提交订单」按钮。
  2. 发出信号 :外卖平台收到你的订单,立刻给商家发了一条通知:「有新的订单了!」------ 这条通知,就是信号(Signal)。
  3. 接收信号:商家的接单电脑收到了这条通知。
  4. 执行动作 :商家看到通知后,立刻开始备餐、制作你点的外卖 ------ 这个 "备餐" 的动作,就是槽(Slot)。

我们把这个过程对应到 Qt 的世界里,逻辑完全严丝合缝:

  • 事件:用户对界面控件的操作(比如点击按钮、输入文字、拖动滑块)。
  • 信号:控件被操作后,自动发出的一条 "通知",告诉整个程序 "我这里发生了某件事!"。
  • 接收者:收到这条通知的对象(比如窗口、另一个控件、甚至是你自己写的一个类)。
  • 槽:接收者收到通知后,执行的具体动作,本质上就是一个函数,专门用来响应信号。
场景 2:马路上的红绿灯,也是信号与槽

再举一个更简单的例子,你过马路看红绿灯:

  • 信号:红灯变成绿灯,发出了 "可以通行" 的信号。
  • 接收者:你。
  • 槽:你收到信号后,执行 "过马路" 的动作。

你看,是不是非常好理解?

信号就是 "发生了什么事" 的通知,槽就是 "收到通知后要做什么" 的动作,信号与槽机制,就是把 "事件通知" 和 "响应动作" 绑定在一起的机制。

再换个程序员更熟悉的例子:网页里的按钮点击事件

如果你接触过一点点网页前端,那你一定知道 JavaScript 里的点击事件:

复制代码
// 网页里的按钮点击事件
document.getElementById("closeBtn").onclick = function() {
    window.close(); // 点击按钮关闭窗口
}

这个逻辑,和 Qt 信号与槽完全是一个意思:

  • 按钮的 onclick,就是 Qt 里按钮的 clicked() 信号。
  • 后面的匿名函数,就是 Qt 里的槽函数。

说白了,所有图形界面程序的交互逻辑,本质上都是 "事件触发,动作响应",Qt 只是把这个逻辑封装成了更规范、更灵活、更安全的 "信号与槽" 机制。

1.2 没有信号与槽,我们要怎么实现交互?原生 C++ 的痛苦

很多新手会问:C++ 本身就有函数,我直接调用函数不行吗?Qt 为什么非要搞出一个信号与槽的东西?

问得非常好!要搞懂信号与槽的价值,我们必须先搞懂:如果没有信号与槽,用原生 C++ 实现 "点击按钮关闭窗口",会有多麻烦、多痛苦。

我们先写一段伪代码,模拟原生 C++ 实现按钮点击的逻辑:

cpp 复制代码
// 原生C++模拟按钮类
class Button {
public:
    // 按钮被点击时,要调用的函数指针
    void (*onClickCallback)();
};

// 原生C++模拟窗口类
class Window {
public:
    void closeWindow() {
        // 关闭窗口的逻辑
    }
};

int main() {
    Window win;
    Button btn;

    // 把窗口的关闭函数,赋值给按钮的函数指针
    btn.onClickCallback = &Window::closeWindow; // 这里直接报错!因为成员函数指针不能直接赋值!

    // 就算你解决了成员函数指针的问题,还有一堆麻烦:
    // 1. 一个按钮只能绑定一个函数,想绑定多个怎么办?
    // 2. 一个函数想被多个按钮绑定怎么办?
    // 3. 按钮和窗口的生命周期不一样,窗口销毁了,按钮还存着函数指针,调用就崩溃!
    // 4. 不同类之间的函数调用,耦合度极高,改一个类,另一个类也要跟着改!
}

你看,用原生 C++ 实现最简单的按钮点击,都有一堆解决不完的问题:

  1. 耦合度极高 :按钮必须知道窗口有一个 closeWindow 函数,窗口也必须知道按钮有一个 onClickCallback 指针,两个类完全绑死了,改一个,另一个必须跟着改。
  2. 灵活性极差:一个按钮只能绑定一个函数,想绑定多个,你得自己写一个函数指针列表,自己管理;一个函数想被多个按钮绑定,你得自己一个个赋值,维护起来极其麻烦。
  3. 安全性极低:如果窗口已经销毁了,按钮还存着窗口的函数指针,一旦按钮被点击,调用了已经销毁的对象的函数,程序直接崩溃,新手根本排查不出来。
  4. 代码极其复杂:光是处理不同类之间的成员函数指针、生命周期、多线程调用,就能让你掉一层头发,更别说做复杂的交互逻辑了。

而 Qt 的信号与槽机制,完美解决了所有这些问题:

  1. 松散耦合:按钮只需要发出 "我被点击了" 的信号,它根本不需要知道谁会接收这个信号,也不需要知道接收者要做什么;窗口只需要写好 "关闭自己" 的槽函数,它根本不需要知道是谁发出的信号。两个类完全独立,改一个,另一个完全不用动。
  2. 极致灵活:一个信号可以绑定无数个槽,一个槽可以被无数个信号绑定,你可以随时连接、随时断开,完全不用修改控件本身的代码。
  3. 极致安全:如果接收者对象销毁了,信号与槽的连接会自动断开,绝对不会出现调用野指针的情况,程序崩溃的概率直接降到最低。
  4. 代码极其简洁 :只需要一行 connect 代码,就能完成绑定,剩下的所有事情,Qt 都帮你处理好了,你只需要专注于 "信号发出后要做什么" 的业务逻辑就行。

现在你懂了吧?信号与槽不是 Qt 没事干搞出来的花架子,它是 Qt 为了解决图形界面交互的核心痛点,设计出来的一套极其优秀的机制,也是 Qt 能成为最受欢迎的 C++ 图形界面框架的核心原因之一。

1.3 信号的本质:就是事件对应的通知函数

搞懂了信号与槽的价值,我们再回到代码里,把信号和槽的本质,给你拆解得明明白白。

1.3.1 信号到底是什么?

在 Qt 里,信号的本质,就是某个事件发生时,Qt 框架自动调用的一个通知函数。

我们来拆解这句话的每一个部分:

  1. 事件是信号的触发源:所有的信号,都是由一个具体的事件触发的。比如:

    • 用户点击了按钮 → 触发了 "鼠标点击事件" → 按钮发出 clicked() 信号。
    • 用户在输入框里输入了文字 → 触发了 "文本改变事件" → 输入框发出 textChanged() 信号。
    • 用户拖动了滑块 → 触发了 "数值改变事件" → 滑块发出 valueChanged() 信号。
    • 用户关闭了窗口 → 触发了 "窗口关闭事件" → 窗口发出 destroyed() 信号。没有事件,就不会有信号发出。信号,就是事件发生后,Qt 给开发者的一个通知。
  2. 信号在代码里的呈现形式,就是一个类的成员函数:你在 Qt 帮助文档里看到的所有信号,比如 QPushButton::clicked()、QLineEdit::textChanged(),本质上都是这个类里的一个成员函数。但是!这个函数和普通的 C++ 成员函数有一个核心区别:你只需要声明这个函数,不需要写它的函数体(实现代码),一般来说,绝大多数的信号qt都有进行提供,我们平时也不怎么能用得到自定义信号,所以信号函数我们直接获取就行,不用去实现信号函数的代码,因为有qt帮我们搞定了

    很多新手这里会懵:只声明不实现,那函数的代码在哪里?答案是:信号函数的实现代码,是 Qt 的元对象编译器(MOC)在编译之前自动生成的 ,根本不需要你手写。你只需要在头文件里声明一下这个信号,剩下的所有事情,Qt 都帮你做好了,这是对于系统的信号函数,我们压根都不用声明都可以,直接通过 类名::信号函数名字 即可,自定义信号函数才需要声明加实现信号函数体。

  3. 信号的发出者,一定是一个实例化的类对象:这里要给新手强调一个最容易犯的错误:发出信号的,是 "对象",不是 "类" 。比如,你在窗口里放了两个按钮,一个 "关闭按钮",一个 "登录按钮"。你点击 "关闭按钮",发出 clicked() 信号的,是 "关闭按钮" 这个具体的对象,不是 QPushButton 这个类;"登录按钮" 不会因为你点了 "关闭按钮",就发出信号。这就像:你家的门铃响了,发出信号的是 "你家的门铃" 这个具体的东西,不是 "门铃" 这个品类,别人家的门铃响了,和你家没有任何关系,也就是,当某个信号触发时,就是这个类对象的信号,而不是这个类的信号,类是类,对象是对象,大家一定要懂得区分

1.3.2 信号的核心特点
  1. 信号函数必须定义在继承自 QObject 的类里,且类里必须加 Q_OBJECT 宏(后面会详细讲这个宏到底是干嘛的)。
  2. 信号函数必须声明在类的 signals: 关键字下面,注意是自定义信号函数需要,系统提供的信号函数不需要哦,我们也不用声明,直接获取即可,通过 类名::信号函数名字 即可
  3. 信号函数的返回值必须是 void,永远不会有返回值,因为它只是一个通知,不需要返回任何东西。
  4. 信号函数可以有参数,参数用来给槽函数传递数据,而且支持重载(同一个信号名,不同的参数列表)。
  5. 信号用 emit 关键字发出,你也可以理解为 "调用信号函数",发出信号的本质,就是 Qt 调用了这个信号函数,注意这个也是自定义信号函数,系统的信号函数完全不用我们操心

1.4 槽的本质:就是响应信号的普通 C++ 函数

搞懂了信号,槽就太好理解了,因为槽的本质,就是一个普通的 C++ 成员函数,唯一的特殊之处,就是它可以和信号绑定在一起,信号发出时,它会被自动调用。

1.4.1 槽函数和普通函数的相同点

槽函数和你平时写的 C++ 函数,几乎没有任何区别:

  1. 它可以定义在类的任何访问权限区域:public、protected、private 都可以。
  2. 它可以有任意的参数,也可以被重载。
  3. 它可以有返回值(虽然绝大多数场景下我们用 void)。
  4. 你可以直接调用它,和调用普通函数完全一样。
  5. 它可以是虚函数、静态函数,也可以是全局函数、Lambda 表达式(后面会详细讲)。
1.4.2 槽函数和普通函数的唯一区别

唯一的区别,也是最核心的区别:槽函数可以通过 connect 函数,和一个信号绑定在一起,当这个信号被发出时,绑定的槽函数会被 Qt 自动调用。

普通的函数,你必须手动写代码调用它,它才会执行;而槽函数,只要和信号绑定了,信号一发出,Qt 就会自动帮你调用它,根本不需要你手动写代码调用。

这就像:普通函数是你家的灯,你必须手动去按开关,它才会亮;而槽函数是你家的智能灯,你设置了 "门铃响了,灯自动亮",只要门铃(信号)一响,灯(槽函数)就会自动亮,根本不需要你手动去按。

1.4.3 槽函数的两种来源

我们在开发中,用到的槽函数主要有两种:

  1. **Qt 系统自带的槽函数:**Qt 的几乎所有控件类,都已经给我们写好了大量常用的槽函数,我们可以直接拿来用,根本不需要自己写。比如:

    • QWidget 类的 close() 槽函数:关闭窗口 / 控件。
    • QWidget 类的 show() 槽函数:显示窗口 / 控件。
    • QLabel 类的 setText() 槽函数:设置标签的显示文本。
    • QLineEdit 类的 clear() 槽函数:清空输入框的内容。
    • QAbstractButton 类的 setChecked() 槽函数:设置按钮的选中状态。这些自带的槽函数,都是 Qt 官方写好的,经过了无数次测试,稳定可靠,能满足我们绝大多数简单的交互需求,不用自己写一行代码。
  2. **我们自己写的自定义槽函数:**系统自带的槽函数,只能完成一些通用的、简单的功能,比如关闭窗口、设置文本。如果我们要实现复杂的业务逻辑,比如 "点击登录按钮,读取输入框里的用户名和密码,去数据库里验证是否正确,正确就跳转到主窗口,错误就弹出提示框",这种复杂的逻辑,系统自带的槽函数根本做不到,这时候就需要我们自己写自定义槽函数了。

    **自定义槽函数的写法,和你写普通的 C++ 成员函数完全一模一样,没有任何区别,你只需要在头文件里声明,在源文件里写实现代码就行。**唯一要注意的,就是槽函数的参数列表,要和绑定的信号的参数列表兼容(后面会详细讲参数匹配的规则)。

1.5 信号与槽的底层调用逻辑:大白话讲透

很多新手会好奇:我把信号和槽绑定了之后,信号一发出,槽函数就自动执行了,这中间 Qt 到底做了什么?

整个流程分为 6 步,我们以 "点击按钮,关闭窗口" 为例:

  1. 你写了一行 connect 代码,把按钮对象的 clicked() 信号和窗口对象的 close() 槽函数绑定在了一起 。这时候,Qt 会在内部的一个 "信号槽连接表" 里,记下一条记录:「当按钮对象发出 clicked() 信号时,要调用窗口对象的 close() 函数」。

  2. 用户用鼠标点击了按钮。这时候,操作系统会给 Qt 程序发送一个 "鼠标点击事件",Qt 收到这个事件后,会把它交给按钮对象处理。

  3. 按钮对象处理完点击事件后,发出 clicked() 信号 。发出信号的本质,就是 Qt 调用了按钮的 clicked() 函数,这个函数是 MOC 工具自动生成的,里面的核心逻辑,就是去查刚才的 "信号槽连接表"。

  4. Qt 查到了 clicked() 信号对应的槽函数 。Qt 在连接表里,找到了这条记录:「按钮对象的 clicked() 信号,对应窗口对象的 close() 槽函数」。

  5. Qt 自动调用对应的槽函数 。Qt 按照连接表里的记录,自动调用了窗口对象的 close() 函数,窗口就被关闭了。

  6. 槽函数执行完后,流程结束。如果这个信号还绑定了其他槽函数,Qt 会依次把所有绑定的槽函数都执行一遍。

你看,整个流程其实非常简单,本质上就是 Qt 帮你做了 "事件发生时,自动调用对应的函数" 这件事,而且帮你处理了所有的耦合、生命周期、安全问题,让你只需要专注于业务逻辑就行。


第二章 信号与槽的核心:connect 函数解析

搞懂了信号与槽的本质,我们就进入实战环节了。要把信号和槽绑定在一起,我们必须用到 Qt 提供的一个核心静态函数:QObject::connect()。

2.1 先明确:我们只学 Qt5 推荐的函数指针写法,不学 Qt4 旧写法

这里我先说明一下:只学 Qt5 及以上版本推荐的「函数指针写法」,不学 Qt4 旧的「SIGNAL/SLOT 宏写法」。

为什么?:

  1. **Qt5 写法有编译时类型检查,安全!**你把信号名、槽函数名写错了,或者参数类型不匹配,编译的时候就会直接报错,你能立刻发现问题,根本不会留到运行时。
  2. **Qt4 写法没有类型检查,极其容易出问题!**哪怕你把信号名、槽函数名写错了,编译的时候也不会报错,只有运行的时候,你点了按钮没反应,才会发现问题,而且新手根本排查不出来哪里错了。
  3. **Qt5 写法更灵活,支持 Lambda 表达式!**Qt5 的写法可以直接用 Lambda 表达式作为槽函数,写简单的逻辑极其方便,而 Qt4 的写法根本不支持。
  4. **现在所有的 Qt 项目,都用 Qt5 及以上的版本,Qt4 已经被淘汰了!**你现在学 Qt4 的写法,除了能看懂十年前的老古董代码,没有任何用处,反而会养成很多坏习惯。

2.2 connect 函数的完整原型(Qt5 推荐版)

connect 函数是 QObject 类的静态成员函数,我们最常用的、最推荐的版本,原型是这样的:

cpp 复制代码
// 这是 Qt5 最推荐、最安全的 connect 函数原型
static QMetaObject::Connection connect(
    const QObject *sender,          // 参数1:信号的发送者
    PointerToMemberFunction signal,  // 参数2:要发出的信号(函数指针)
    const QObject *receiver,        // 参数3:信号的接收者
    PointerToMemberFunction method,  // 参数4:要执行的槽函数(函数指针)
    Qt::ConnectionType type = Qt::AutoConnection  // 参数5:连接类型(有默认值,新手不用管)
);

看着参数很多,别慌!我们一个参数一个参数,用大白话给你讲透

2.3 参数 1:const QObject *sender ------ 信号的发送者

2.3.1 这个参数到底是干嘛的?

大白话翻译:这个参数,就是告诉 Qt:"是谁发出了信号?"

我们前面反复强调过,发出信号的,是一个具体的、实例化的对象,不是类。所以这个参数,你必须传一个继承自 QObject 类的对象的指针。

比如,我们要实现 "点击按钮关闭窗口",发出信号的是 "按钮对象",所以这个参数,你就要传按钮对象的指针,比如 btn_close。

2.3.2 这个参数的硬性要求
  1. **必须是指针类型,绝对不能传对象本身!**比如,你创建了一个按钮对象 QPushButton btn_close(this);,你不能直接传 btn_close,必须传 &btn_close。但是!我们前面讲过,**Qt 的控件对象,绝对不要创建在栈上,一定要用 new 创建在堆上,**所以你传的一定是一个指针,比如 QPushButton *btn_close = new QPushButton(this);,这里直接传 btn_close 就行。

  2. **这个对象的类,必须直接或间接继承自 QObject!**只有继承自 QObject 的类,才能使用信号与槽机制,才能作为信号的发送者。Qt 里所有的控件类,比如 QPushButton、QLabel、QWidget,都是继承自 QObject 的,所以放心传。如果你自己写的类,想作为信号发送者,必须让它继承自 QObject,且类里必须加 Q_OBJECT 宏。

  3. 绝对不能传 nullptr(空指针)! 如果你传了 nullptr,connect 函数会直接返回一个无效的连接,信号与槽根本不会绑定成功,运行起来一点反应都没有,新手很容易犯这个错。

2.3.3 容易踩的 3 个坑
坑 1:传了类名,而不是对象指针

错误代码示例:

复制代码
// 错误!传了 QPushButton 类名,而不是具体的按钮对象指针
connect(QPushButton, &QPushButton::clicked, this, &Widget::close);

错误原因 :发出信号的是具体的按钮对象,不是 QPushButton 这个类。类只是一个模板,对象才是具体的实例,就像 "门铃" 这个品类不会响,你家的具体的门铃才会响。

编译现象:直接编译报错,编译器会提示 "类型名不允许在这里使用"。

正确代码:

复制代码
// 先创建具体的按钮对象
QPushButton *btn_close = new QPushButton(this);
// 传具体的对象指针
connect(btn_close, &QPushButton::clicked, this, &Widget::close);
坑 2:传了栈对象,而不是堆对象的指针

错误代码示例:

复制代码
// 错误!按钮对象创建在了栈上,不是堆上
QPushButton btn_close(this);
connect(&btn_close, &QPushButton::clicked, this, &Widget::close);

错误原因 :这个按钮对象是创建在栈上的,当构造函数执行完,这个对象的作用域就结束了,会被系统自动销毁。这时候,connect 绑定的发送者对象已经不存在了,信号与槽的连接会自动断开,运行起来点击按钮一点反应都没有。

运行现象:编译不报错,运行起来点击按钮没反应,新手根本不知道哪里错了。

正确代码:

复制代码
// 正确!用 new 把按钮对象创建在堆上,生命周期由我们控制
QPushButton *btn_close = new QPushButton(this);
connect(btn_close, &QPushButton::clicked, this, &Widget::close);
坑 3:对象还没创建,就传了空指针

错误代码示例:

复制代码
// 先 connect,后创建对象,btn_close 还是 nullptr!
connect(btn_close, &QPushButton::clicked, this, &Widget::close);
btn_close = new QPushButton(this); // 后创建对象,晚了!

错误原因 :你在调用 connect 的时候,btn_close 还没有被 new 创建,还是一个空指针,connect 直接绑定失败。

运行现象:编译不报错,运行起来点击按钮没反应,甚至可能直接崩溃。

正确代码:

复制代码
// 先创建对象,再 connect!顺序绝对不能反!
btn_close = new QPushButton(this);
connect(btn_close, &QPushButton::clicked, this, &Widget::close);

2.4 参数 2:PointerToMemberFunction signal ------ 要绑定的信号

2.4.1 这个参数到底是干嘛的?

大白话翻译:这个参数,就是告诉 Qt:"发送者发出的哪个信号,要和槽函数绑定?"

这个参数,你必须传发送者所属类的信号函数的指针,格式必须严格遵守:&类名::信号函数名,绝对不能写错。

比如,按钮的 clicked() 信号,你必须写成 &QPushButton::clicked,这里的类名是 QPushButton,不是你创建的对象名,这是新手最容易写错的地方。

2.4.2 这个参数的硬性要求
  1. 格式必须严格是 & 类名::信号函数名,绝对不能加括号!很多新手会写成 &QPushButton::clicked(),加了括号,这是绝对错误的!因为我们这里要传的是函数指针,也就是函数的内存地址,只需要写函数名就行;加了括号,就变成了 "调用这个函数",编译器会直接报错。

  2. **这里的类名,必须是信号所属的类,不是对象名,也不是父类名(除非信号是父类里的)**比如,clicked() 信号是 QPushButton 的父类 QAbstractButton 里定义的,你写成 &QAbstractButton::clicked 也可以,但绝对不能写成 &btn_close::clicked(对象名),编译器会直接报错。

  3. **信号必须是发送者类里定义的,或者是它的父类里定义的,不能随便写一个不存在的信号!**如果你写了一个发送者类里没有的信号,编译的时候会直接报错,告诉你 "这个类里没有这个成员函数"。

  4. **如果信号有重载,必须用函数指针明确指定是哪个重载版本!**如果一个信号有多个重载版本(同一个函数名,不同的参数列表),你必须用函数指针明确告诉编译器,你要绑定的是哪个版本,否则编译器会不知道选哪个,直接报错。这个我们后面讲信号重载的时候会详细讲。

2.4.3 容易踩的 4 个坑
坑 1:加了括号,变成了函数调用

错误代码示例:

复制代码
QPushButton *btn_close = new QPushButton(this);
// 错误!信号名后面加了括号,变成了调用函数
connect(btn_close, &QPushButton::clicked(), this, &Widget::close);

错误原因:我们要传的是函数指针,也就是函数的地址,只需要写函数名;加了括号,就变成了调用这个函数,编译器会直接报错。

编译现象:编译报错,提示 "无效的表达式,必须是一个指向成员函数的指针"。

正确代码:

复制代码
// 正确!只写函数名,不加括号
connect(btn_close, &QPushButton::clicked, this, &Widget::close);
坑 2:写了对象名,而不是类名

错误代码示例:

复制代码
QPushButton *btn_close = new QPushButton(this);
// 错误!写了对象名 btn_close,而不是类名 QPushButton
connect(btn_close, &btn_close::clicked, this, &Widget::close);

错误原因:信号是类里定义的成员函数,不是对象里的,所以必须用类名来取函数指针,不能用对象名。

编译现象:编译报错,提示 "btn_close 不是一个类名、命名空间或枚举名"。

正确代码:

复制代码
// 正确!用类名 QPushButton
connect(btn_close, &QPushButton::clicked, this, &Widget::close);
坑 3:信号名拼写错误,大小写错误

错误代码示例:

复制代码
QPushButton *btn_close = new QPushButton(this);
// 错误1:信号名拼写错误,把 clicked 写成了 click
connect(btn_close, &QPushButton::click, this, &Widget::close);

// 错误2:大小写错误,把 clicked 写成了 Clicked
connect(btn_close, &QPushButton::Clicked, this, &Widget::close);

错误原因:C++ 是大小写敏感的语言,函数名必须和类里定义的完全一致,一个字母都不能错,大小写也必须完全一致。

编译现象:编译报错,提示 "类 QPushButton 中没有名为 click/Clicked 的成员"。

正确代码:

复制代码
// 正确!拼写和大小写完全和定义一致
connect(btn_close, &QPushButton::clicked, this, &Widget::close);
坑 4:信号是私有的,访问不到

错误代码示例 :你自己写了一个类,把信号定义在了 private 区域里,外面根本访问不到。

复制代码
// 错误的类定义
class MyClass : public QObject
{
    Q_OBJECT
// 错误!信号定义在了 private 里,外面访问不到
private:
    void mySignal();
};

// 外面 connect 的时候,根本访问不到 mySignal
MyClass *obj = new MyClass(this);
connect(obj, &MyClass::mySignal, this, &Widget::doSomething);

错误原因 :Qt 里的信号,默认是 public 的,你必须把信号定义在 signals: 关键字下面,Qt 会自动把信号设置为 public,外面才能访问到。如果你把信号定义在了 private 里,外面根本访问不到。

编译现象:编译报错,提示 "mySignal 是私有的,无法访问"。

正确代码:

复制代码
// 正确的类定义,信号写在 signals: 下面
class MyClass : public QObject
{
    Q_OBJECT
signals: // 必须写在 signals 下面
    void mySignal();
};

// 外面就能正常访问了
MyClass *obj = new MyClass(this);
connect(obj, &MyClass::mySignal, this, &Widget::doSomething);

2.5 参数 3:const QObject *receiver ------ 信号的接收者

2.5.1 这个参数到底是干嘛的?

大白话翻译:这个参数,就是告诉 Qt:"谁来接收这个信号?槽函数是哪个对象的成员函数?"

我们前面讲过,槽函数本质上是一个类的成员函数,要调用一个成员函数,必须有这个类的对象。所以这个参数,你必须传槽函数所属的类的对象的指针,而且这个对象也必须继承自 QObject。

比如,我们要实现 "点击按钮,关闭窗口",槽函数 close() 是窗口类 Widget 的成员函数,所以这个参数,我们就要传当前窗口对象的指针,也就是 this。

再比如,我们要实现 "点击按钮,修改标签的文字",槽函数 setText() 是 QLabel 类的成员函数,所以这个参数,我们就要传标签对象的指针,比如 lbl_hello。

2.5.2 这个参数的硬性要求
  1. 必须是指针类型,和参数 1 的要求完全一样。
  2. 这个对象的类,必须直接或间接继承自 QObject。
  3. 绝对不能传 nullptr,否则连接直接无效。
  4. 必须和参数 4 的槽函数所属的类对应上:槽函数是哪个类的,就必须传哪个类的对象指针,否则编译直接报错。
2.5.3 容易踩的 2 个坑
坑 1:接收者和槽函数所属的类不匹配

错误代码示例:

复制代码
QPushButton *btn_close = new QPushButton(this);
QLabel *lbl_hello = new QLabel(this);
// 错误!槽函数 close() 是 Widget 类的,接收者却传了标签对象 lbl_hello
connect(btn_close, &QPushButton::clicked, lbl_hello, &Widget::close);

错误原因 :槽函数 close() 是 Widget 类的成员函数,你却传了 QLabel 类的对象作为接收者,QLabel 类里根本没有 close() 这个槽函数,编译器直接报错。

编译现象:编译报错,提示 "无法将槽函数的指针转换为接收者类的成员函数指针"。

正确代码:

复制代码
// 正确!槽函数是 Widget 类的,接收者传 this(Widget 类的对象指针)
connect(btn_close, &QPushButton::clicked, this, &Widget::close);

// 如果是修改标签的文字,槽函数是 QLabel 类的,接收者就传标签对象
connect(btn_close, &QPushButton::clicked, lbl_hello, &QLabel::setText);
坑 2:接收者对象已经被销毁了,还在发信号

错误代码示例:

复制代码
// 创建了一个标签对象,作为接收者
QLabel *lbl_hello = new QLabel(this);
// 绑定信号与槽
connect(btn_change, &QPushButton::clicked, lbl_hello, &QLabel::setText);

// 后面不小心把标签对象 delete 了
delete lbl_hello;

// 这时候点击按钮,发出信号,接收者已经不存在了

错误原因 :很多新手会担心,接收者对象销毁了,再发信号会不会崩溃?这里可以放心,Qt 已经帮你处理好了:如果接收者对象被销毁了,信号与槽的连接会自动断开,不会调用槽函数,更不会崩溃。但是! 你还是要避免这种情况,因为连接已经无效了,你的业务逻辑不会执行,会出现 "点击按钮没反应" 的问题。

正确做法:确保接收者对象的生命周期,至少和信号发送者一样长,或者在销毁接收者之前,手动断开连接。

2.6 参数 4:PointerToMemberFunction method ------ 要执行的槽函数

2.6.1 这个参数到底是干嘛的?

大白话翻译:这个参数,就是告诉 Qt:"收到信号后,要执行哪个函数?"

这个参数的要求,和参数 2 的信号几乎完全一样,你必须传接收者所属类的槽函数的指针,格式严格遵守:&类名::槽函数名,不能加括号,不能写错。

比如,窗口的 close() 槽函数,你必须写成 &Widget::close;标签的 setText() 槽函数,你必须写成 &QLabel::setText。

2.6.2 这个参数的硬性要求,新手必须遵守
  1. 格式必须严格是 & 类名::槽函数名,绝对不能加括号! 和参数 2 的要求完全一样。
  2. 槽函数的参数列表,必须和信号的参数列表兼容! 这是最核心的要求,我们后面会单独用一个小节详细讲参数匹配规则。
  3. 槽函数必须是接收者类里定义的,或者是它的父类里定义的,不能写一个不存在的函数!
  4. 如果槽函数有重载,必须用函数指针明确指定是哪个重载版本! 和信号的重载要求一样。
2.6.3 容易踩的 3 个坑
坑 1:槽函数名拼写错误、大小写错误

和参数 2 的信号名错误一样,槽函数名必须和定义完全一致,一个字母都不能错,大小写也必须完全一致,否则编译直接报错。

坑 2:槽函数是私有的,外面访问不到

如果你把自定义槽函数定义在了 private 区域里,在类外面的 connect 函数里,是访问不到这个槽函数的,编译会直接报错。解决方法 :把槽函数定义在 public slots: 或者 public: 区域里,让外面能访问到。

坑 3:槽函数的参数列表和信号不兼容

这是新手最容易犯的错,也是最核心的规则,我们单独用一个小节详细讲。

2.7 核心中的核心:信号与槽的参数匹配规则(必记!)

很多新手写的 connect 代码,编译报错,90% 的原因都是信号和槽的参数列表不兼容。

2.7.1 最核心的 3 条规则
  1. 信号的参数类型,必须和槽函数的参数类型完全一致(或者可以隐式转换)。比如,信号的参数是 QString,槽函数的参数也必须是 QString,不能是 int,除非 int 可以隐式转换成 QString(当然,这个转换是不行的)。再比如,信号的参数是 int,槽函数的参数是 double,这是可以的,因为 int 可以隐式转换成 double。

  2. **信号的参数个数,可以大于等于槽函数的参数个数,绝对不能小于!**大白话:信号可以传更多的参数,槽函数可以选择只接收前面的几个,后面的忽略;但是槽函数要的参数,信号必须给,信号没传,槽函数就接收不到,绝对不行。

  3. 参数是按顺序匹配的,信号的第 1 个参数传给槽的第 1 个参数,第 2 个传给第 2 个,以此类推。槽函数的参数顺序,必须和信号的参数顺序完全一致,类型也必须匹配。

2.7.2 用表格和示例,把所有情况给你讲透

我们以一个带两个参数的信号为例:void mySignal(QString name, int age);,来看看哪些情况是合法的,哪些是非法的。

槽函数声明 是否合法 原因
void mySlot(QString name, int age); ✅ 完全合法 参数类型、个数完全一致,完美匹配
void mySlot(QString name); ✅ 合法 信号参数个数(2)> 槽参数个数(1),槽只接收第 1 个参数,忽略第 2 个
void mySlot(); ✅ 合法 信号参数个数(2)> 槽参数个数(0),槽不接收任何参数,忽略所有信号参数
void mySlot(int age, QString name); ❌ 非法 参数顺序错了!信号第 1 个是 QString,槽第 1 个是 int,类型不匹配
void mySlot(QString name, int age, QString gender); ❌ 非法 槽参数个数(3)> 信号参数个数(2),信号没给第 3 个参数,槽接收不到
void mySlot(QString name, double age); ✅ 合法 信号的 int 可以隐式转换成 double,类型兼容
void mySlot(int name, int age); ❌ 非法 信号的第 1 个参数是 QString,无法隐式转换成 int,类型不匹配
2.7.3 容易犯的 2 个参数错误
错误 1:槽函数的参数个数比信号多

错误代码示例:

复制代码
// 信号:只有1个 QString 参数
void textChanged(const QString &text);
// 槽函数:要2个参数,信号根本没给第2个
void mySlot(const QString &text, int age);

// connect 直接编译报错
connect(edit, &QLineEdit::textChanged, this, &Widget::mySlot);

错误原因:槽函数要 2 个参数,信号只传了 1 个,第 2 个参数槽函数根本接收不到,编译器直接报错。

正确做法:要么给槽函数的第 2 个参数加默认值,要么减少槽函数的参数个数,要么给信号加上第 2 个参数。

错误 2:参数类型不匹配,且无法隐式转换

错误代码示例:

复制代码
// 信号:参数是 QString
void textChanged(const QString &text);
// 槽函数:参数是 int,QString 无法隐式转换成 int
void mySlot(int number);

// connect 直接编译报错
connect(edit, &QLineEdit::textChanged, this, &Widget::mySlot);

错误原因 :QString 类型无法隐式转换成 int 类型,类型不兼容,编译器直接报错。正确做法:把槽函数的参数类型改成 QString,或者在中间加一个 Lambda 表达式,把 QString 转换成 int,再传给槽函数(后面讲 Lambda 的时候会教你)。

2.8 参数 5:Qt::ConnectionType type = Qt::AutoConnection ------ 连接类型

这个参数有一个默认值 Qt::AutoConnection,新手完全不用管它,保持默认就行,99.9% 的桌面应用场景,默认值都能完美适配。

这里我只给你简单讲一下每个连接类型的含义,你了解就行,不用记,等你用到多线程的时候,再回来查也不迟。

连接类型 含义 适用场景
Qt::AutoConnection(默认) 自动连接:Qt 会自动判断用哪种连接方式。如果发送者和接收者在同一个线程,就用 DirectConnection;如果在不同线程,就用 QueuedConnection。 绝大多数场景,新手默认用这个就行
Qt::DirectConnection 直接连接:信号发出后,立刻直接调用槽函数,槽函数执行完后,才会继续执行信号后面的代码。相当于直接调用函数。 发送者和接收者在同一个线程,需要同步执行
Qt::QueuedConnection 队列连接:信号发出后,会把信号放到接收者线程的事件队列里,等接收者线程空闲了,再调用槽函数。信号发出后,立刻继续执行后面的代码,不用等槽函数执行完。 多线程之间的信号与槽通信
Qt::BlockingQueuedConnection 阻塞队列连接:和队列连接类似,但是信号发出后,会阻塞当前线程,等槽函数执行完后,才会继续执行后面的代码。 多线程之间需要同步等待结果的场景,绝对不能在同一个线程里用,会直接死锁

2.9 connect 函数的返回值:QMetaObject::Connection

很多新手会忽略 connect 函数的返回值,其实这个返回值非常有用,它可以帮你:

  1. 判断连接是否成功。
  2. 后面要断开连接的时候,用这个返回值可以精准断开,不会误断开其他连接。
2.9.1 怎么用返回值判断连接是否成功?

connect 函数返回的 QMetaObject::Connection 对象,可以直接转换成 bool 类型,如果连接成功,返回 true;如果连接失败,返回 false。

代码示例:

复制代码
// 用一个变量保存 connect 的返回值
QMetaObject::Connection conn = connect(btn_close, &QPushButton::clicked, this, &Widget::close);

// 判断连接是否成功
if (conn) {
    qDebug() << "信号与槽连接成功!";
} else {
    qDebug() << "信号与槽连接失败!";
}

这个方法,是新手排查 "点击按钮没反应" 的第一步:先看看连接到底成功了没有。如果连接失败,那肯定是你 connect 的参数写错了;如果连接成功了,那就是信号没发出来,或者槽函数里的代码有问题。

2.9.2 怎么用返回值断开连接?

后面我们讲 disconnect 函数的时候会详细讲,这里先给你看一下用法:

复制代码
// 保存连接
QMetaObject::Connection conn = connect(btn_close, &QPushButton::clicked, this, &Widget::close);

// 后面要断开的时候,直接传这个返回值就行
disconnect(conn);

这是最推荐的断开连接的方式,精准、安全,不会误断开其他连接。

2.10 完整的代码示例:点击按钮关闭窗口,逐行解析

讲完了所有参数,我们来看一个完整的、可运行的代码示例,我会逐行给你解析,让你彻底搞懂每一行代码的作用。

第一步:在 widget.h 里的代码
cpp 复制代码
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>

QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE

class Widget : public QWidget
{
    // 只要用信号与槽,必须加这个宏!!!
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();

private:
    Ui::Widget *ui;
    // 声明按钮对象的指针作为成员变量,避免构造函数执行完就销毁了
    QPushButton *btn_close;
};
#endif // WIDGET_H
第二步:在 widget.cpp 里的代码
cpp 复制代码
#include "widget.h"
#include "ui_widget.h"
// 必须引入按钮类的头文件,要用哪个类,就必须引入哪个类的头文件
#include <QPushButton>
// 引入调试打印的头文件,用来打印连接是否成功
#include <QDebug>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    // 界面初始化,所有代码必须写在这行后面!
    ui->setupUi(this);

    // 设置窗口的固定大小,避免控件乱跑
    this->setFixedSize(800, 600);

    // ============== 第一步:创建按钮对象 ==============
    // 用 new 在堆上创建按钮对象,父对象设为 this(当前窗口)
    // 好处1:按钮会自动显示在窗口里;好处2:窗口关闭时,自动销毁按钮,不用手动 delete
    btn_close = new QPushButton(this);

    // 给按钮设置显示的文字
    btn_close->setText("关闭窗口");

    // 设置按钮的固定大小:宽200像素,高50像素
    btn_close->setFixedSize(200, 50);

    // 把按钮移动到窗口的(300, 250)坐标位置,也就是窗口中间
    btn_close->move(300, 250);

    // ============== 第二步:关联信号与槽 ==============
    // 参数1:信号发送者 - 按钮对象 btn_close
    // 参数2:信号 - &QPushButton::clicked(按钮被点击)
    // 参数3:信号接收者 - 当前窗口 this
    // 参数4:槽函数 - &Widget::close(关闭窗口)
    // 返回值:保存连接,用来判断是否成功
    QMetaObject::Connection conn = connect(
        btn_close, 
        &QPushButton::clicked, 
        this, 
        &Widget::close
    );

    // ============== 第三步:判断连接是否成功 ==============
    if (conn) {
        qDebug() << "恭喜!信号与槽连接成功!";
    } else {
        qDebug() << "遗憾!信号与槽连接失败!请检查参数是否正确!";
    }
}

Widget::~Widget()
{
    delete ui;
}
第三步:运行效果

点击运行按钮,你会看到一个窗口,中间有一个 "关闭窗口" 的按钮,同时 Qt Creator 底部的「应用程序输出」窗口会打印 "恭喜!信号与槽连接成功!"。点击按钮,窗口会自动关闭,完美实现了我们要的功能!


第三章 了解即可:Qt4 版本的 SIGNAL/SLOT 宏写法

前面我们反复强调,新手不要学 Qt4 的写法,但是你可能会在网上的老教程、老项目里看到这种写法,所以这里我们简单给你讲一下,让你能看懂就行,不推荐你在新项目里用。

3.1 Qt4 版本的 connect 函数原型

复制代码
static bool connect(
    const QObject *sender,
    const char *signal,
    const QObject *receiver,
    const char *method,
    Qt::ConnectionType type = Qt::AutoConnection
);

和 Qt5 版本的核心区别:信号和槽参数,变成了 const char * 类型,也就是字符串,必须用 SIGNAL() 和 SLOT() 这两个宏包裹起来。

3.2 Qt4 写法的代码示例

还是 "点击按钮关闭窗口" 的功能,Qt4 的写法是这样的:

cpp 复制代码
QPushButton *btn_close = new QPushButton(this);
btn_close->setText("关闭窗口");

// Qt4 写法,用 SIGNAL 和 SLOT 宏
connect(
    btn_close, 
    SIGNAL(clicked()), 
    this, 
    SLOT(close())
);
写法要求:
  1. 信号必须用 SIGNAL(信号函数名(参数类型)) 包裹,槽函数必须用 SLOT(槽函数名(参数类型)) 包裹。
  2. 括号里必须写函数名和参数类型,不能写参数名,比如 SIGNAL(textChanged(QString)),不能写 SIGNAL(textChanged(QString text))。
  3. 函数名、参数类型必须完全正确,但是哪怕你写错了,编译也不会报错,只有运行的时候没反应。

3.3 Qt4 写法的致命缺点

  1. 没有编译时类型检查,极其容易出问题:哪怕你把信号名、槽函数名、参数类型写错了,编译的时候也不会报错,因为宏会把里面的内容转换成字符串,编译器根本不会检查字符串里的内容对不对。只有运行的时候,你会发现点击按钮没反应,新手根本排查不出来哪里错了。
  2. 不支持 Lambda 表达式:Qt4 的写法根本不能用 Lambda 表达式,写简单的逻辑也必须单独写一个槽函数,极其麻烦。
  3. 代码可读性差:宏包裹的写法,远没有 Qt5 的函数指针写法直观。

3.4 为什么现在还有人用 Qt4 的写法?

只有一个原因:老项目的历史遗留问题。十年前的老项目,都是用 Qt4 写的,维护的时候只能继续用这种写法。

第四章 自定义信号与槽:从声明到发送

系统自带的信号和槽,只能完成一些简单的功能,要实现复杂的业务逻辑,我们必须学会自己写自定义信号和自定义槽。

4.1 先搞懂:什么时候需要自定义信号与槽?

很多新手会问:我什么时候需要自己写信号和槽?用系统自带的不行吗?

这里给你讲清楚,只要遇到下面这几种场景,你就需要自定义信号与槽:

  1. 你自己写的类,需要给其他类发送通知:比如你写了一个登录对话框类,用户点击登录按钮,验证成功后,需要给主窗口发一个 "登录成功" 的通知,这时候你就需要在登录对话框类里,自定义一个 "登录成功" 的信号。
  2. 需要实现复杂的业务逻辑:比如 "点击按钮,读取输入框的内容,验证格式,保存到文件,弹出成功提示",这种复杂的逻辑,系统自带的槽函数根本做不到,你需要自己写一个自定义槽函数,把这些逻辑写进去。
  3. 需要在不同的类之间传递数据:比如子窗口里的用户输入的数据,需要传给主窗口,这时候你就可以在子窗口里自定义一个带参数的信号,把数据通过信号参数传给主窗口的槽函数。

简单说:只要系统自带的信号和槽满足不了你的需求,你就可以自己写自定义信号和槽,Qt 给了你完全的自由度。

4.2 自定义信号与槽的硬性规范

在写代码之前,我们必须先把自定义信号和槽的规范,给你讲得明明白白,这些是 Qt 的硬性要求,违反了就会编译报错,或者运行起来没反应。

4.2.1 自定义信号的规范(5 条铁律)
  1. 自定义信号,必须写在类的 signals: 关键字下面。
  2. 信号的返回值必须是 void,永远不能有返回值。
  3. 信号只需要在头文件里声明,绝对不需要你写实现代码(函数体),MOC 工具会自动生成。
  4. 信号可以有参数,参数用来给槽函数传递数据,支持重载。
  5. 信号的访问权限是 public,Qt 会自动设置,你不需要手动加 public 关键字,可以在类的任何地方发出信号。
  6. 可以使用emit关键字来表示发送信号,"emit" 是一个空的宏。"emit" 其实是可选的,没有什么含义,只是为了提醒开发人员,实际上要发送信号就是要调用信号函数罢了,qt提供的信号函数不需要我们去手动在代码中调用,而我们自己手动实现的信号函数要想能触发,触发槽函数,就需要我们自己去手动的在我们写的代码中的合适的相对应的位置调用信号函数,当程序运行到这个调用信号函数的位置之后,此时qt就会认为这个信号需要发送出去,于是就会发送信号,调用我们connect的槽函数~,这一点大家要明确。
4.2.2 自定义槽函数的规范(4 条铁律)
  1. Qt5 及以上版本,槽函数可以写在类的 public、protected、private 区域,也可以写在 public slots、protected slots、private slots 下面 。早期 Qt4 版本必须写在 slots 下面,现在已经不强制了,但为了代码可读性,建议还是写在 slots 下面,或者加个注释说明这是槽函数。
  2. 槽函数的返回值一般是 void,也可以有返回值,但只有直接调用的时候才能拿到,信号触发的时候拿不到。
  3. 槽函数必须在头文件里声明,在源文件里写实现代码(函数体),和普通的 C++ 成员函数完全一样。
  4. 槽函数可以有参数,可以被重载,参数列表必须和绑定的信号兼容。
4.2.3 类的要求(2 条铁律)
  1. 自定义信号与槽的类,必须直接或间接继承自 QObject 类。
  2. 类的最开头,必须加上 Q_OBJECT 宏,哪怕你父类里加了,子类里最好也加上,绝对不能漏!

注意:上图中的 ① 和 ② 的顺序不能颠倒。
原因是, 首先关联信号和槽,一旦检测到信号发射之后就会立马执行关联的槽函数。反之,若先发射信号,此时还没有关联槽函数,当信号发射之后槽函数不会响

4.3 最核心的 Q_OBJECT 宏,到底是干嘛的?

很多新手会问:我每次写类,都要加这个 Q_OBJECT 宏,不加就报错,它到底是干嘛的?

这里我用大白话给你讲透,你不用深入底层,只要知道这几点就行:

  1. Q_OBJECT 宏,是 Qt 元对象系统的入口 。我们前面讲过,Qt 的信号与槽、属性系统、运行时类型信息,都是基于元对象系统实现的,而 Q_OBJECT 宏,就是告诉 Qt 的 MOC 工具:"这个类要使用元对象系统,你要给它生成对应的元对象代码"。

  2. 不加 Q_OBJECT 宏,信号与槽完全无法工作 。如果你不加这个宏,MOC 工具不会处理你的类,不会生成信号的实现代码,也不会处理信号与槽的绑定,你写的 connect 代码要么编译报错,要么运行起来没反应,甚至会出现 "undefined reference to vtable for xxx" 的经典报错。

  3. 加了 Q_OBJECT 宏之后,必须重新执行 qmake! 这是新手最容易踩的坑:你加了 Q_OBJECT 宏,还是编译报错,99% 的原因是你没有重新执行 qmake。因为 MOC 工具是在 qmake 的时候扫描你的代码,生成对应的元对象代码的。你加了 Q_OBJECT 宏之后,必须重新执行 qmake,让 MOC 工具重新扫描你的代码,否则新的宏不会生效,还是会报错。

重新执行 qmake 的方法:点击 Qt Creator 顶部菜单栏的「构建」->「执行 qmake」,就这么简单。

4.4 自定义信号与槽完整实战:老师下课,学生回应

我们用一个完整的、生动的场景,来带你走一遍自定义信号与槽的完整流程:

  • 场景:老师在课堂上,说 "下课了"(发出自定义信号),学生听到后,说 "老师再见!"(执行自定义槽函数)。

为了实现这个场景,我们需要创建两个新的类:Teacher(老师类)和 Student(学生类),它们都继承自 QObject。

4.4.1 第一步:在项目中添加新类

我们先在项目里添加 Teacher 和 Student 这两个类,步骤非常详细,新手跟着走就行:

  1. 在 Qt Creator 左侧的项目文件列表里,右键点击你的项目名称 (比如 Qt_SignalSlot_Demo)。
  2. 在弹出的右键菜单里,选择 「添加新文件...」(Add New...) 。
  3. 在弹出的对话框里,左侧选择 「C++」 ,中间选择 「C++ Class」 ,然后点击右下角的 「Choose...」 。
  4. 接下来进入类的设置界面:
    • Class name(类名) :输入 Teacher(注意大小写,类名用大驼峰)。
    • Base class(基类) :下拉框里选择 QObject(非常重要!必须选 QObject,否则不能用信号与槽)。
    • 下面的「Header file」和「Source file」会自动生成,不用改。
  5. 点击「下一步」,再点击「完成」。
  6. 用完全一样的步骤,再添加一个 Student 类,基类同样选择 QObject。

添加完成后,你的项目文件列表里,会多出来四个文件:

  • teacher.h:老师类的头文件,用来声明信号。
  • teacher.cpp:老师类的源文件,因为信号不需要实现,所以这个文件几乎不用改。
  • student.h:学生类的头文件,用来声明槽函数。
  • student.cpp:学生类的源文件,用来实现槽函数。
4.4.2 第二步:编写 Teacher 类,声明自定义信号

我们先写 Teacher 类,在里面声明一个自定义信号 classIsOver(),表示 "下课了"。

打开 teacher.h 文件,完整代码如下,我会逐行给你解析:

cpp 复制代码
#ifndef TEACHER_H
#define TEACHER_H

// 必须引入 QObject 的头文件,因为我们继承自它
#include <QObject>

// 老师类,继承自 QObject
class Teacher : public QObject
{
    // 必须加这个宏!!!只要用信号与槽,就必须加!
    Q_OBJECT

public:
    // 构造函数,parent 参数是父对象指针,默认值为 nullptr
    explicit Teacher(QObject *parent = nullptr);

// ============== 自定义信号声明区域 ==============
// 注意:这里是 signals,不是 signal,末尾有个 s!
signals:
    // 自定义信号:下课了
    // 1. 必须写在 signals: 下面
    // 2. 返回值必须是 void
    // 3. 只需要声明,不需要写实现代码
    // 4. 可以带参数,用来给槽函数传递数据
    void classIsOver();

};

#endif // TEACHER_H

逐行解析核心部分:

  1. class Teacher : public QObject:让 Teacher 类公开继承自 QObject,只有继承自 QObject 的类,才能使用信号与槽机制。
  2. Q_OBJECT:必须加的宏,不加的话,信号与槽完全无法工作,加了之后必须重新执行 qmake。
  3. signals::Qt 扩展的关键字,用来标记下面的内容是自定义信号,注意是复数 signals,不是单数 signal,很多新手会少写一个 s,导致编译报错。
  4. void classIsOver();:我们自定义的信号,只需要声明,不需要在 .cpp 文件里写实现代码,MOC 工具会自动生成。

接下来打开 teacher.cpp 文件,因为信号不需要实现,所以这个文件保持默认就行,不用改任何内容:

cpp 复制代码
#include "teacher.h"

// 构造函数,调用父类 QObject 的构造函数,没什么特殊的
Teacher::Teacher(QObject *parent)
    : QObject(parent)
{
}
4.4.3 第三步:编写 Student 类,声明并实现自定义槽函数

接下来写 Student 类,在里面声明并实现一个自定义槽函数 sayGoodbye(),表示 "学生说老师再见"。

打开 student.h 文件,完整代码如下,逐行解析:

cpp 复制代码
#ifndef STUDENT_H
#define STUDENT_H

#include <QObject>

// 学生类,继承自 QObject
class Student : public QObject
{
    // 必须加这个宏!!!
    Q_OBJECT

public:
    explicit Student(QObject *parent = nullptr);

// ============== 自定义槽函数声明区域 ==============
// 这里用 public slots,也可以直接用 public,Qt5 不强制要求
public slots:
    // 自定义槽函数:说老师再见
    // 1. 返回值为 void
    // 2. 需要声明,也需要在 .cpp 文件里实现
    // 3. 参数列表要和绑定的信号兼容
    void sayGoodbye();

};

#endif // STUDENT_H

逐行解析核心部分:

  1. public slots::Qt 扩展的关键字,用来标记下面的内容是槽函数,Qt5 之后已经不强制要求了,你可以直接写在 public: 下面,但是为了代码的可读性,建议还是用 slots 标记一下,别人一看就知道这是槽函数。
  2. void sayGoodbye();:我们自定义的槽函数,和普通的成员函数一样,需要在头文件里声明,在 .cpp 文件里写实现。

接下来打开 student.cpp 文件,实现我们的自定义槽函数,完整代码如下:

cpp 复制代码
#include "student.h"
// 引入 QDebug 头文件,用来在槽函数里打印内容,验证是否执行了
#include <QDebug>

Student::Student(QObject *parent)
    : QObject(parent)
{
}

// ============== 自定义槽函数的实现 ==============
void Student::sayGoodbye()
{
    // 槽函数里的逻辑:打印一句话,证明槽函数被执行了
    qDebug() << "学生齐声说:老师再见!";
}

这里的槽函数很简单,就是打印一句话,证明它被执行了。实际开发中,你可以在这里写任何你需要的业务逻辑,比如修改界面、保存数据、弹出提示框等等。

4.4.4 第四步:在主窗口 Widget 类里,关联信号与槽,触发信号

现在,老师类和学生类都写好了,我们需要在主窗口 Widget 类里:

  1. 实例化老师对象和学生对象。
  2. 把老师的 classIsOver() 信号,和学生的 sayGoodbye() 槽函数关联起来。
  3. 创建一个按钮,点击按钮时,让老师发出 classIsOver() 信号。

首先打开 widget.h 文件,添加老师和学生对象的指针作为成员变量,完整代码如下:

cpp 复制代码
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
// 引入老师和学生类的头文件
#include "teacher.h"
#include "student.h"

QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();

private:
    Ui::Widget *ui;
    // 声明老师对象和学生对象的指针作为成员变量
    Teacher *teacher;
    Student *student;
    // 声明按钮对象的指针
    QPushButton *btn_classOver;
};
#endif // WIDGET_H

然后打开 widget.cpp 文件,实现完整的逻辑,我会逐行给你解析:

cpp 复制代码
#include "widget.h"
#include "ui_widget.h"
// 引入按钮类的头文件
#include <QPushButton>
// 引入调试打印的头文件
#include <QDebug>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    // 设置窗口固定大小
    this->setFixedSize(800, 600);

    // ============== 第一步:实例化老师和学生对象 ==============
    // 给老师和学生对象设置父对象为 this(当前窗口),自动管理内存
    teacher = new Teacher(this);
    student = new Student(this);

    // ============== 第二步:【核心】关联自定义信号与自定义槽 ==============
    // 参数1:信号发送者 - 老师对象 teacher
    // 参数2:信号 - &Teacher::classIsOver(老师发出的下课信号)
    // 参数3:信号接收者 - 学生对象 student
    // 参数4:槽函数 - &Student::sayGoodbye(学生的回应槽函数)
    QMetaObject::Connection conn = connect(
        teacher,
        &Teacher::classIsOver,
        student,
        &Student::sayGoodbye
    );

    // 判断连接是否成功
    if (conn) {
        qDebug() << "老师的下课信号,和学生的回应槽函数,连接成功!";
    } else {
        qDebug() << "信号与槽连接失败!";
    }

    // ============== 第三步:创建按钮,点击按钮让老师发出信号 ==============
    btn_classOver = new QPushButton(this);
    btn_classOver->setText("老师说:下课!");
    btn_classOver->setFixedSize(200, 50);
    btn_classOver->move(300, 250);

    // 关联按钮的点击信号,到一个 Lambda 表达式,在里面让老师发出下课信号
    connect(btn_classOver, &QPushButton::clicked, this, [=](){
        qDebug() << "老师说:同学们,下课了!";
        // ============== 【核心】用 emit 关键字发送自定义信号 ==============
        // 发送信号的本质,就是调用这个信号函数,emit 只是用来提醒开发者,这里在发送信号
        emit teacher->classIsOver();
    });
}

Widget::~Widget()
{
    delete ui;
}

逐行解析核心部分:

  1. teacher = new Teacher(this);:实例化老师对象,父对象设为当前窗口,这样窗口关闭时,会自动销毁老师对象,不用手动 delete。
  2. connect(teacher, &Teacher::classIsOver, student, &Student::sayGoodbye);:把老师的 classIsOver 信号,和学生的 sayGoodbye 槽函数关联起来。只要老师发出这个信号,学生的槽函数就会自动执行。
  3. emit teacher->classIsOver();:用 emit 关键字发送自定义信号。这里的 emit 其实是一个空的宏,你不加它,直接写 teacher->classIsOver(); 也能正常发送信号,但是加 emit 是 Qt 的规范,能让别人一看就知道 "这里在发送信号",可读性更好,建议一定要加。
4.4.5 第五步:运行程序,看效果

现在,所有代码都写好了,点击运行按钮:

  1. 你会看到一个窗口,中间有一个 "老师说:下课!" 的按钮。

  2. 看 Qt Creator 底部的「应用程序输出」窗口,会打印 "老师的下课信号,和学生的回应槽函数,连接成功!"。

  3. 点击窗口里的按钮,「应用程序输出」窗口会立刻打印:

    复制代码
    老师说:同学们,下课了!
    学生齐声说:老师再见!

完美!我们的自定义信号与槽,完美运行了!老师发出信号,学生的槽函数自动执行,完全符合我们的预期。

4.5 进阶:带参数的自定义信号与槽,实现数据传递

刚才的例子里,信号和槽都是没有参数的,只能实现 "通知" 的功能。如果我们要通过信号,把数据从发送者传递给接收者,就需要给信号和槽加上参数。

这一节,我们把刚才的例子升级一下:老师说 "下课了,今天的作业是 XXX",学生回应 "收到,今天的作业是 XXX",这里的 "作业内容",就是通过信号的参数,从老师传递给学生的。

4.5.1 第一步:修改 Teacher 类,给信号加上参数

打开 teacher.h 文件,修改信号的声明,给它加上一个 QString 类型的参数 homework,表示作业内容:

cpp 复制代码
#ifndef TEACHER_H
#define TEACHER_H

#include <QObject>

class Teacher : public QObject
{
    Q_OBJECT

public:
    explicit Teacher(QObject *parent = nullptr);

signals:
    // 自定义信号,带一个 QString 参数,用来传递作业内容
    void classIsOver(QString homework);

};

#endif // TEACHER_H
4.5.2 第二步:修改 Student 类,给槽函数加上对应的参数

打开 student.h 文件,修改槽函数的声明,加上和信号完全一致的参数:

cpp 复制代码
#ifndef STUDENT_H
#define STUDENT_H

#include <QObject>

class Student : public QObject
{
    Q_OBJECT

public:
    explicit Student(QObject *parent = nullptr);

public slots:
    // 自定义槽函数,参数和信号完全一致,用来接收作业内容
    void sayGoodbye(QString homework);

};

#endif // STUDENT_H

然后打开 student.cpp 文件,修改槽函数的实现,把接收到的作业内容打印出来:

cpp 复制代码
#include "student.h"
#include <QDebug>

Student::Student(QObject *parent)
    : QObject(parent)
{
}

// 带参数的槽函数实现
void Student::sayGoodbye(QString homework)
{
    // 打印接收到的作业内容
    qDebug() << "学生齐声说:收到!今天的作业是" << homework;
}
4.5.3 第三步:修改 Widget 类,发送信号时传入参数

打开 widget.cpp 文件,修改发送信号的代码,在 emit 发送信号的时候,传入作业内容的参数:

cpp 复制代码
// 关联按钮的点击信号,发送带参数的信号
connect(btn_classOver, &QPushButton::clicked, this, [=](){
    // 定义作业内容
    QString homework = "背诵《滕王阁序》全文,默写3遍";
    qDebug() << "老师说:下课了,今天的作业是" << homework;
    // 【核心】发送信号时,传入参数 homework
    emit teacher->classIsOver(homework);
});

这里的 connect 关联代码不用改,我们只需要输入函数名即可,因为信号和槽的参数完全匹配,Qt 会自动把信号里的 homework 参数,传递给槽函数的 homework 参数。

4.5.4 运行效果

点击运行按钮,点击窗口里的按钮,「应用程序输出」窗口会打印:

复制代码
老师说:下课了,今天的作业是 "背诵《滕王阁序》全文,默写3遍"
学生齐声说:收到!今天的作业是 "背诵《滕王阁序》全文,默写3遍"

我们成功通过信号的参数,把 "作业内容" 这个数据,从老师对象传递给了学生对象!这就是 Qt 里不同类之间传递数据最常用、最安全的方式。

4.6 信号与槽的重载:怎么解决函数指针的歧义?

Qt 里的信号和槽,都支持重载,也就是同一个函数名,不同的参数列表。但是当信号有重载的时候,我们直接用 &类名::信号名 写 connect,编译器会不知道我们要选哪个重载版本,直接报错。

这一节,我们就来教你怎么解决信号重载的问题,用函数指针明确指定要绑定的重载版本。

4.6.1 场景:给 Teacher 类加上两个重载的信号

我们给 Teacher 类加上两个重载的 classIsOver 信号:

  1. 无参版本:void classIsOver();
  2. 带参版本:void classIsOver(QString homework);

打开 teacher.h 文件,修改代码:

cpp 复制代码
#ifndef TEACHER_H
#define TEACHER_H

#include <QObject>

class Teacher : public QObject
{
    Q_OBJECT

public:
    explicit Teacher(QObject *parent = nullptr);

signals:
    // 重载的信号1:无参版本
    void classIsOver();
    // 重载的信号2:带 QString 参数的版本
    void classIsOver(QString homework);

};

#endif // TEACHER_H

同样,给 Student 类也加上两个重载的槽函数:

cpp 复制代码
#ifndef STUDENT_H
#define STUDENT_H

#include <QObject>

class Student : public QObject
{
    Q_OBJECT

public:
    explicit Student(QObject *parent = nullptr);

public slots:
    // 重载的槽函数1:无参版本
    void sayGoodbye();
    // 重载的槽函数2:带 QString 参数的版本
    void sayGoodbye(QString homework);

};

#endif // STUDENT_H

然后在 student.cpp 里实现两个槽函数:

cpp 复制代码
#include "student.h"
#include <QDebug>

Student::Student(QObject *parent)
    : QObject(parent)
{
}

// 无参槽函数实现
void Student::sayGoodbye()
{
    qDebug() << "学生齐声说:老师再见!";
}

// 带参槽函数实现
void Student::sayGoodbye(QString homework)
{
    qDebug() << "学生齐声说:收到!今天的作业是" << homework;
}
4.6.2 问题:直接写 connect 会编译报错

现在,如果你直接像之前那样写 connect,编译器会直接报错:

cpp 复制代码
// 错误!编译器不知道你要选哪个重载版本的 classIsOver 信号
connect(teacher, &Teacher::classIsOver, student, &Student::sayGoodbye);

报错原因 :Teacher 类里有两个 classIsOver 函数,编译器不知道你要绑定的是无参的那个,还是带参的那个,出现了歧义,直接报错。

4.6.3 解决方案:用函数指针明确指定重载版本

解决方法很简单:我们定义一个函数指针,明确指定它的参数列表,让它指向我们要绑定的那个重载版本的信号,然后把这个函数指针传给 connect 就行。

代码示例:关联无参的信号和槽

cpp 复制代码
// 1. 定义函数指针,明确指定是无参的信号
void (Teacher::*teacherSignal_noParam)() = &Teacher::classIsOver;
// 2. 定义函数指针,明确指定是无参的槽函数
void (Student::*studentSlot_noParam)() = &Student::sayGoodbye;

// 3. 把函数指针传给 connect,就不会有歧义了
connect(teacher, teacherSignal_noParam, student, studentSlot_noParam);

代码示例:关联带参的信号和槽

cpp 复制代码
// 1. 定义函数指针,明确指定是带 QString 参数的信号
void (Teacher::*teacherSignal_withParam)(QString) = &Teacher::classIsOver;
// 2. 定义函数指针,明确指定是带 QString 参数的槽函数
void (Student::*studentSlot_withParam)(QString) = &Student::sayGoodbye;

// 3. 关联带参的信号和槽
connect(teacher, teacherSignal_withParam, student, studentSlot_withParam);

逐行解析函数指针的定义:

  • void (Teacher::*teacherSignal_noParam)():这是一个函数指针的定义,我们拆解开:
    1. void:函数的返回值类型,和信号的返回值一致。
    2. (Teacher::*):表示这个函数指针,指向的是 Teacher 类的成员函数。
    3. teacherSignal_noParam:函数指针的变量名,你可以随便起。
    4. ():函数的参数列表,这里是空的,表示无参;如果是带参的,就写 (QString),和信号的参数列表完全一致。

这样定义之后,编译器就明确知道,你要绑定的是哪个重载版本的信号和槽了,不会再有歧义,编译也不会报错了。

4.6.4 完整的代码示例

我们在 widget.cpp 里,把两个重载版本都关联起来,用两个按钮分别触发:

cpp 复制代码
#include "widget.h"
#include "ui_widget.h"
#include <QPushButton>
#include <QDebug>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    this->setFixedSize(800, 600);

    // 实例化对象
    teacher = new Teacher(this);
    student = new Student(this);

    // ============== 关联无参的信号和槽 ==============
    void (Teacher::*teacherSignal_noParam)() = &Teacher::classIsOver;
    void (Student::*studentSlot_noParam)() = &Student::sayGoodbye;
    connect(teacher, teacherSignal_noParam, student, studentSlot_noParam);

    // ============== 关联带参的信号和槽 ==============
    void (Teacher::*teacherSignal_withParam)(QString) = &Teacher::classIsOver;
    void (Student::*studentSlot_withParam)(QString) = &Student::sayGoodbye;
    connect(teacher, teacherSignal_withParam, student, studentSlot_withParam);

    // ============== 按钮1:触发无参信号 ==============
    QPushButton *btn1 = new QPushButton(this);
    btn1->setText("无参:下课!");
    btn1->setFixedSize(200, 50);
    btn1->move(300, 200);
    connect(btn1, &QPushButton::clicked, this, [=](){
        qDebug() << "老师说:下课了!";
        emit teacher->classIsOver(); // 触发无参信号
    });

    // ============== 按钮2:触发带参信号 ==============
    QPushButton *btn2 = new QPushButton(this);
    btn2->setText("带参:下课+布置作业!");
    btn2->setFixedSize(200, 50);
    btn2->move(300, 300);
    connect(btn2, &QPushButton::clicked, this, [=](){
        QString homework = "抄写单词10遍";
        qDebug() << "老师说:下课了,作业是" << homework;
        emit teacher->classIsOver(homework); // 触发带参信号
    });
}

Widget::~Widget()
{
    delete ui;
}
4.6.5 运行效果

点击运行按钮:

  1. 点击 "无参:下课!" 按钮,会打印:

    复制代码
    老师说:下课了!
    学生齐声说:老师再见!
  2. 点击 "带参:下课 + 布置作业!" 按钮,会打印:

    复制代码
    老师说:下课了,作业是 "抄写单词10遍"
    学生齐声说:收到!今天的作业是 "抄写单词10遍"

完美!我们成功解决了信号与槽重载的歧义问题,两个重载版本都能正常工作!


第五章 信号与槽的神器:Lambda 表达式全解析

在前面的代码里,我们已经多次用到了 Lambda 表达式,你一定感受到了它的方便:不用单独声明和定义一个槽函数,直接在 connect 里写逻辑,代码非常简洁,逻辑非常集中。

Lambda 表达式是 C++11 标准新增的特性,Qt5 完美支持它,是 Qt 信号与槽开发中最常用的工具之一。

那么其实早在之前,我就已经介绍了lambda的用法,所以在这里,我就不进行太详细的介绍了,大家看着复习即可

5.1 为什么要用 Lambda 表达式作为槽函数?

在讲语法之前,我们先搞懂:用 Lambda 表达式作为槽函数,到底有什么好处?

我们对比一下,实现 "点击按钮,打印一句话" 的功能,用自定义槽函数和用 Lambda 表达式,分别要写多少代码。

方式 1:用自定义槽函数
  1. 在 widget.h 里声明槽函数:

    复制代码
    private slots:
        void onBtnClicked();
  2. 在 widget.cpp 里实现槽函数:

    复制代码
    void Widget::onBtnClicked()
    {
        qDebug() << "按钮被点击了!";
    }
  3. 在构造函数里关联信号与槽:

    复制代码
    connect(btn, &QPushButton::clicked, this, &Widget::onBtnClicked);
方式 2:用 Lambda 表达式

只需要在构造函数里写一行 connect 代码就行:

复制代码
connect(btn, &QPushButton::clicked, this, [=](){
    qDebug() << "按钮被点击了!";
});

你看,差距太明显了!用 Lambda 表达式,我们不需要在头文件里声明槽函数,也不需要在源文件里单独实现槽函数,直接把逻辑写在 connect 里,代码量少了一大半,逻辑也非常集中,一眼就能看到 "点击按钮后要做什么",不用在头文件和源文件之间来回跳。

5.2 Lambda 表达式的完整语法格式

Lambda 表达式的完整语法格式是这样的,我们拆成一个部分一个部分来讲,每个部分都给你讲透:

复制代码
[捕获列表] (参数列表) 选项 -> 返回值类型 {
    函数体;
};

我们一个部分一个部分来讲,告诉你哪些是必须的,哪些是可选的,每个部分是什么意思。

5.2.1 [捕获列表]:最重要的部分,绝对不能省略!

[] 是 Lambda 表达式的开始标志,绝对不能省略,哪怕里面是空的。它的作用是:告诉 Lambda 表达式,它可以访问外部函数(也就是包含这个 Lambda 的函数)里的哪些局部变量、成员变量。

这是 Lambda 表达式最核心、最容易踩坑的部分,我们把所有的捕获方式,都给你讲透,新手只需要记住最常用的几种就行。

1. 最推荐用的:[=] ------ 值传递捕获所有外部变量

[=] 的意思是:以值传递的方式,捕获 Lambda 表达式所在的函数里的所有局部变量,以及当前类的 this 指针。

大白话翻译:Lambda 表达式里,可以用外面的所有局部变量,也可以用当前类的所有成员变量,用的是它们的 "副本",修改副本不会影响外面原来的变量。

代码示例:

复制代码
// 外面的局部变量
int a = 10;
QString text = "Hello";

// 用 [=] 捕获所有外部变量
connect(btn, &QPushButton::clicked, this, [=](){
    // 可以直接用外面的 a 和 text,以及当前类的成员变量
    qDebug() << "a = " << a;
    qDebug() << "text = " << text;
    // 可以用当前类的成员变量,比如 teacher、student
    qDebug() << "老师对象是否有效:" << (teacher != nullptr);
});

铁律:99% 的场景下,你直接用 [=] 就行! 它最安全,也最方便,不会出现野指针的问题,是新手的首选。

2. [&] ------ 引用传递捕获所有外部变量

[&] 的意思是:以引用传递的方式,捕获 Lambda 表达式所在的函数里的所有局部变量。

大白话翻译:Lambda 表达式里,用的是外面变量的 "引用",也就是变量本身,修改 Lambda 里的变量,会直接修改外面原来的变量。

代码示例:

复制代码
int a = 10;

// 用 [&] 引用捕获所有外部变量
connect(btn, &QPushButton::clicked, this, [&](){
    a = 20; // 直接修改外面的 a 变量
    qDebug() << "a 被修改为:" << a;
});

警告:这个捕获方式,极其容易踩坑,非必要不要用! 坑在哪里? 如果你用 [&] 捕获了一个局部变量,这个局部变量是创建在栈上的,当包含 Lambda 的函数执行完后,这个局部变量会被系统自动销毁,内存被释放。这时候,你再点击按钮,触发 Lambda 表达式,Lambda 里的引用已经指向了一块已经被释放的内存,变成了野指针,程序会直接崩溃!

举个错误的例子:

复制代码
Widget::Widget(QWidget *parent)
    : QWidget(parent)
{
    // 局部变量 a,创建在栈上
    int a = 10;

    QPushButton *btn = new QPushButton(this);
    btn->setText("点击我");
    btn->move(300, 250);

    // 错误!用 [&] 引用捕获了局部变量 a
    connect(btn, &QPushButton::clicked, this, [&](){
        qDebug() << a; // 构造函数执行完后,a 已经被销毁了,这里访问野指针,程序崩溃!
    });
}

错误原因 :构造函数执行完后,局部变量 a 的作用域结束,被系统销毁了。这时候你点击按钮,Lambda 里的 a 已经是野指针了,程序直接崩溃。

正确做法 :如果一定要修改外部变量,要么把变量设为类的成员变量(生命周期和窗口一样长),要么用值捕获加 mutable(后面会讲),绝对不要用 [&] 捕获局部变量。

3. [this] ------ 捕获当前类的 this 指针

[this] 的意思是:只捕获当前类的 this 指针,不捕获任何局部变量。Lambda 表达式里,可以直接使用当前类的所有成员变量和成员函数,但是不能用外面的局部变量。

代码示例:

复制代码
// 类的成员变量
int m_count = 0;

// 用 [this] 捕获 this 指针
connect(btn, &QPushButton::clicked, this, [this](){
    m_count++; // 可以直接用类的成员变量
    qDebug() << "点击次数:" << m_count;
    this->close(); // 可以调用类的成员函数
});
4. 只捕获指定的变量

如果你只想用某几个外部变量,不想捕获所有的,可以只捕获指定的变量:

  • [a]:只以值传递的方式捕获局部变量 a,其他变量都不能用。
  • [&a]:只以引用传递的方式捕获局部变量 a,其他变量都不能用。
  • [=, &a]:以值传递捕获所有变量,但是变量 a 用引用传递。
  • [&, a]:以引用传递捕获所有变量,但是变量 a 用值传递。

代码示例:

复制代码
int a = 10;
int b = 20;

// 只值捕获 a,不捕获 b
connect(btn, &QPushButton::clicked, this, [a](){
    qDebug() << "a = " << a; // 可以用 a
    // qDebug() << "b = " << b; // 错误!没有捕获 b,不能用
});
捕获列表总结
捕获写法 含义 新手使用建议
[=] 值传递捕获所有局部变量和 this 指针 ✅ 强烈推荐,新手首选,安全方便
[this] 只捕获 this 指针,能用类的成员 ✅ 推荐,只需要用类成员的时候用
[&] 引用传递捕获所有局部变量 ❌ 不推荐,新手容易踩坑崩溃
[a] 只值捕获变量 a 偶尔用,只需要单个变量的时候
[&a] 只引用捕获变量 a ❌ 不推荐,容易踩坑
5.2.2 (参数列表):接收信号传递的参数,可选

这部分和普通函数的参数列表完全一样,用来接收信号传递过来的参数。

  • 如果你的槽函数不需要接收信号的参数,这部分可以完全省略(包括括号)。
  • 如果需要接收信号的参数,就写在括号里,参数类型必须和信号的参数类型完全一致。

代码示例 1:不需要接收参数,省略参数列表

复制代码
// 按钮的 clicked 信号没有参数,Lambda 不需要接收,省略参数列表
connect(btn, &QPushButton::clicked, this, [=](){
    qDebug() << "按钮被点击了!";
});

代码示例 2:需要接收信号的参数,写参数列表

复制代码
// 输入框的 textChanged 信号有一个 QString 参数,Lambda 要接收
QLineEdit *edit = new QLineEdit(this);
connect(edit, &QLineEdit::textChanged, this, [=](QString text){
    // 接收信号传递过来的 text 参数,也就是输入框里的最新文本
    qDebug() << "输入框的内容变成了:" << text;
});
5.2.3 选项:可选,新手几乎不用

这部分是可选项,最常用的是 mutable 关键字,新手几乎用不到,简单了解就行。

mutable 的作用:允许在 Lambda 表达式里,修改用 [=] 值捕获的变量的副本。

因为用 [=] 值捕获的变量,在 Lambda 里默认是只读的,不能修改,如果你想修改,就必须加 mutable 关键字。

代码示例:

复制代码
int a = 10;

// 加 mutable,允许修改值捕获的 a 的副本
connect(btn, &QPushButton::clicked, this, [a]() mutable {
    a = 20; // 可以修改了,但是修改的是 a 的副本,外面的 a 还是 10
    qDebug() << "副本 a = " << a;
});

qDebug() << "外面的 a = " << a; // 还是 10,不会变

注意:这里修改的只是值捕获的副本,外面的原变量不会变,所以这个关键字新手几乎用不到,了解就行。

5.2.4 -> 返回值类型:可选,几乎都可以省略

这部分用来指定 Lambda 表达式的返回值类型,比如 -> int 表示返回 int 类型。

  • 绝大多数情况下,这部分可以完全省略 ,编译器会自动根据 Lambda 里的 return 语句,推导出返回值类型。
  • 如果 Lambda 里没有 return 语句,默认返回 void,完全不用写这部分。

代码示例:

复制代码
// 有返回值的 Lambda,编译器自动推导出返回值类型是 int
connect(btn, &QPushButton::clicked, this, [=](){
    int a = 10;
    int b = 20;
    return a + b; // 编译器自动推导出返回值是 int
});
5.2.5 { 函数体; }:必须有,写你的业务逻辑

这部分就是 Lambda 表达式的具体实现代码,和普通函数的函数体完全一样,你想写什么逻辑,就写在这里。

  • 大括号 {} 绝对不能省略,哪怕里面是空的。
  • 函数体里,可以用你在捕获列表里捕获的所有变量,以及信号传递过来的参数。

5.3 Lambda 表达式在信号与槽中的实战场景

我们用几个最常用的实战场景,带你熟练掌握 Lambda 表达式作为槽函数的用法。

场景 1:点击按钮,修改标签的文字
cpp 复制代码
#include <QLabel>
#include <QPushButton>

// 创建标签
QLabel *lbl_hello = new QLabel(this);
lbl_hello->setText("等待点击按钮...");
lbl_hello->setFixedSize(300, 50);
lbl_hello->move(300, 200);
QFont font("微软雅黑", 16);
lbl_hello->setFont(font);

// 创建按钮
QPushButton *btn_change = new QPushButton(this);
btn_change->setText("修改文字");
btn_change->setFixedSize(200, 50);
btn_change->move(300, 300);

// 用 Lambda 作为槽函数,修改标签的文字
connect(btn_change, &QPushButton::clicked, this, [=](){
    // 直接用捕获的 lbl_hello 对象,调用 setText 函数
    lbl_hello->setText("Hello World! 我被 Lambda 修改了!");
});
场景 2:接收信号的参数,处理输入框的内容
cpp 复制代码
#include <QLineEdit>
#include <QLabel>

// 创建输入框
QLineEdit *edit_name = new QLineEdit(this);
edit_name->setPlaceholderText("请输入你的名字");
edit_name->setFixedSize(200, 50);
edit_name->move(300, 200);

// 创建标签,显示输入的内容
QLabel *lbl_show = new QLabel(this);
lbl_show->setText("你输入的名字是:");
lbl_show->setFixedSize(300, 50);
lbl_show->move(300, 300);
lbl_show->setFont(QFont("微软雅黑", 16));

// 用 Lambda 接收输入框的 textChanged 信号的参数,实时显示
connect(edit_name, &QLineEdit::textChanged, this, [=](QString name){
    // 接收信号传递的 name 参数,也就是输入框的内容
    lbl_show->setText(QString("你输入的名字是:%1").arg(name));
});

运行程序,你在输入框里输入的内容,会实时显示在标签上,非常方便。

场景 3:实现计数器,点击按钮数字加 1
cpp 复制代码
#include <QLabel>
#include <QPushButton>

// 类的成员变量,在 widget.h 里声明:int m_count = 0;

// 创建标签,显示数字
QLabel *lbl_count = new QLabel(this);
lbl_count->setText(QString("当前计数:%1").arg(m_count));
lbl_count->setFixedSize(200, 50);
lbl_count->move(300, 200);
lbl_count->setFont(QFont("微软雅黑", 16));

// 创建加1按钮
QPushButton *btn_add = new QPushButton(this);
btn_add->setText("+1");
btn_add->setFixedSize(100, 50);
btn_add->move(300, 300);

// 创建减1按钮
QPushButton *btn_sub = new QPushButton(this);
btn_sub->setText("-1");
btn_sub->setFixedSize(100, 50);
btn_sub->move(450, 300);

// 加1按钮的 Lambda 槽函数
connect(btn_add, &QPushButton::clicked, this, [=](){
    m_count++; // 修改类的成员变量
    lbl_count->setText(QString("当前计数:%1").arg(m_count));
});

// 减1按钮的 Lambda 槽函数
connect(btn_sub, &QPushButton::clicked, this, [=](){
    m_count--;
    lbl_count->setText(QString("当前计数:%1").arg(m_count));
});
场景 4:省略 connect 的第三个参数 this

在 Qt5 里,如果你的 Lambda 捕获列表里已经捕获了 this(比如用了 [=] 或者 [this]),那么 connect 函数的第三个参数(接收者 this)可以完全省略,代码更简洁。

代码示例:

复制代码
// 省略第三个参数 this,代码更简洁
connect(btn, &QPushButton::clicked, [=](){
    qDebug() << "省略了接收者参数,代码更简洁!";
    this->close();
});

这是 Qt5 支持的写法,完全合法,推荐使用,代码更简洁。

5.4 怎么断开 Lambda 表达式的连接?

很多新手会问:我用 Lambda 作为槽函数,没有槽函数名,后面想断开连接怎么办?

方法很简单:用我们前面讲的 connect 函数的返回值 QMetaObject::Connection,保存连接,后面断开的时候,直接把这个返回值传给 disconnect 函数就行。

代码示例:

cpp 复制代码
// 在 widget.h 里声明:QMetaObject::Connection m_lambdaConn;

// 保存 Lambda 连接的返回值
m_lambdaConn = connect(btn, &QPushButton::clicked, this, [=](){
    qDebug() << "Lambda 槽函数执行了!";
});

// 后面要断开的时候,直接传这个返回值
disconnect(m_lambdaConn);

这是唯一能断开 Lambda 连接的方式,非常简单,精准安全。

5.5 Lambda 表达式的优缺点与使用建议

优点
  1. 代码简洁:不用单独声明和定义槽函数,代码量少,逻辑集中。
  2. 方便快捷:可以直接捕获外部变量,不用通过参数传递,写简单逻辑极其方便。
  3. 灵活度高:可以在任何地方写,不用修改类的定义,适合临时的、简单的逻辑。
缺点
  1. 可读性差:如果 Lambda 里的逻辑太复杂(比如超过 10 行代码),代码可读性会急剧下降,维护起来很麻烦。
  2. 无法复用:Lambda 是匿名的,只能用一次,不能在其他地方复用。
  3. 容易踩坑:如果捕获列表用得不对,很容易出现野指针、程序崩溃的问题。
给新手的使用建议
  1. 逻辑简单(5 行代码以内)、只用一次的场景,用 Lambda:比如点击按钮关闭窗口、修改标签文字、打印一句话这种简单逻辑,用 Lambda 非常方便。
  2. 逻辑复杂、需要复用的场景,用自定义槽函数:如果你的逻辑超过 10 行代码,或者需要在多个地方复用,一定要单独写自定义槽函数,代码可读性和可维护性会好很多。
  3. 捕获列表优先用 [=],绝对不要用 [&] 捕获局部变量:避免野指针导致的程序崩溃,安全第一。

第六章 信号与槽的多种连接方式:一对一、一对多、多对一、信号连信号

Qt 的信号与槽机制,灵活性非常高,不只是 "一个信号连一个槽" 这么简单,它支持多种连接方式,能满足你各种复杂的业务需求。

6.1 一对一:一个信号连接一个槽

这是我们最常用、最基础的连接方式,前面所有的例子都是一对一,这里就不再重复了,简单回顾一下:

复制代码
// 一对一:按钮的 clicked 信号,连接到窗口的 close 槽
connect(btn_close, &QPushButton::clicked, this, &Widget::close);

1、在 "widget.h" 中声明信号和槽以及信号发射函数;

2、在 "widget.cpp" 中实现槽函数,信号发射函数以及连接信号和槽;

适用场景:一个事件触发一个动作,绝大多数简单场景都用这个。

6.2 信号连信号:一个信号连接另一个信号

除了信号连槽,Qt 还支持信号连信号!也就是说:当信号 A 发出时,会自动触发信号 B 发出。

这种方式用得不多,但在某些场景下非常有用,比如 "中转信号":你想把一个控件的信号,转发给另一个对象,不需要写任何槽函数,直接用信号连信号就行。

6.2.1 信号连信号的用法

信号连信号的 connect 写法,和信号连槽几乎完全一样,只是把第四个参数,从槽函数换成了另一个信号。

函数原型:

复制代码
static QMetaObject::Connection connect(
    const QObject *sender1,  // 第一个信号的发送者
    PointerToMemberFunction signal1, // 第一个信号
    const QObject *sender2,  // 第二个信号的发送者
    PointerToMemberFunction signal2, // 第二个信号(要触发的信号)
    Qt::ConnectionType type = Qt::AutoConnection
);

核心规则:

  1. 第一个信号的参数列表,必须和第二个信号的参数列表兼容,规则和信号与槽的参数匹配规则完全一样。
  2. 第一个信号发出时,会自动触发第二个信号发出,第一个信号的参数,会传递给第二个信号。

示例:

在上述示例的基础上,在 "widget.cpp" 文件中添加如下代码:

6.2.2 代码示例:按钮点击信号,触发老师的下课信号

我们用前面的老师和学生的例子,来演示信号连信号:

cpp 复制代码
#include <QApplication>
#include <QMainWindow>
#include <QPushButton>
#include <QDebug>

// 老师类:包含下课信号
class Teacher : public QObject
{
    Q_OBJECT
public:
    explicit Teacher(QObject *parent = nullptr) : QObject(parent) {}

signals:
    void classIsOver(); // 下课信号(无参数版本)
};

// 学生类:包含下课响应槽函数
class Student : public QObject
{
    Q_OBJECT
public:
    explicit Student(QObject *parent = nullptr) : QObject(parent) {}

public slots:
    void sayGoodbye() {
        qDebug() << "学生齐声说:老师再见!";
    }
};

// 主窗口:整合所有逻辑
class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    MainWindow(QWidget *parent = nullptr) : QMainWindow(parent) {
        // 1. 实例化老师和学生对象
        Teacher *teacher = new Teacher(this);
        Student *student = new Student(this);

        // 2. 先关联老师的信号和学生的槽(基础连接)
        connect(teacher, &Teacher::classIsOver, student, &Student::sayGoodbye);

        // 3. 创建中转按钮
        QPushButton *btn_relay = new QPushButton(this);
        btn_relay->setText("信号中转按钮");
        btn_relay->setFixedSize(200, 50);
        btn_relay->move(300, 250);

        // 4. 【核心】信号连信号:按钮点击 → 触发老师下课信号
        connect(btn_relay, &QPushButton::clicked, teacher, &Teacher::classIsOver);
    }
};

// 主函数:启动程序
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.resize(800, 600);
    w.show();
    return a.exec();
}

// 必须包含,用于处理Qt的MOC(元对象编译器)生成的代码
#include "main.moc"

6.3「一对多」的核心定义

Qt 信号槽的「一对多」,指的是同一个信号发送者的同一个信号,可以通过多次调用 connect() 函数,绑定到多个不同的槽函数(这些槽可以属于同一个接收者,也可以属于不同接收者)。

  • 通俗比喻:就像按一个客厅的开关(1 个信号),同时点亮天花板灯、打开加湿器、启动空气净化器(3 个槽函数)------ 一个触发动作,触发多个响应。
  • 核心特点:信号只需触发一次,所有绑定的槽函数会按「连接顺序」依次执行。

「一对多」的核心原理(为什么能实现)

Qt 的信号槽底层是「连接列表」机制:

  1. 每调用一次 connect(),Qt 就会在内部为这个信号新增一条「信号 - 槽」连接记录,存入列表;
  2. 当信号被触发时,Qt 会遍历这个列表,按记录的顺序依次调用对应的槽函数;
  3. 只要槽函数的参数类型与信号匹配(或兼容),就能完成连接,无数量限制(理论上可绑定无数个槽)。
cpp 复制代码
#include <QApplication>
#include <QMainWindow>
#include <QPushButton>
#include <QDebug>

class MainWindowMulti : public QMainWindow
{
    Q_OBJECT
public:
    MainWindowMulti(QWidget *parent = nullptr) : QMainWindow(parent) {
        // 1. 创建触发按钮
        QPushButton *btn_multi = new QPushButton(this);
        btn_multi->setText("点击触发多个槽");
        btn_multi->setFixedSize(200, 50);
        btn_multi->move(300, 250);

        // 2. 连接1:第一个槽函数(Lambda表达式)
        connect(btn_multi, &QPushButton::clicked, this, [=](){
            qDebug() << "【1】第一个槽函数执行了!";
        });

        // 3. 连接2:第二个槽函数(Lambda表达式)
        connect(btn_multi, &QPushButton::clicked, this, [=](){
            qDebug() << "【2】第二个槽函数执行了!";
        });

        // 4. 连接3:第三个槽函数(Lambda表达式)
        connect(btn_multi, &QPushButton::clicked, this, [=](){
            qDebug() << "【3】第三个槽函数执行了!";
        });
    }
};

// 主函数:启动程序
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindowMulti w;
    w.resize(800, 600);
    w.show();
    return a.exec();
}

// 必须包含,用于处理Qt的MOC(元对象编译器)生成的代码
#include "main_multi.moc"

6.3.2 运行效果

点击运行按钮,点击 "点击触发多个槽" 按钮,看「应用程序输出」窗口:

复制代码
【1】第一个槽函数执行了!
【2】第二个槽函数执行了!
【3】第三个槽函数执行了!

三个槽函数都执行了,完美!

6.3.3 一对多的适用场景

一对多的连接方式,在实际开发中非常有用,比如:

  • 点击 "保存" 按钮:同时执行 "保存数据到文件"、"更新界面显示"、"弹出保存成功提示" 这三个槽函数。
  • 点击 "重置" 按钮:同时执行 "清空输入框"、"重置下拉框选项"、"重置计数器" 这三个槽函数。

6.4 多对一:多个信号连接同一个槽

除了一对多,Qt 还支持多对一:多个不同的信号,连接到同一个槽函数。不管哪个信号发出,都会执行这个槽函数。

这也是一个非常常用的连接方式,比如:

  • "关闭按钮" 和 "退出菜单":都连接到同一个 "关闭窗口" 的槽函数。
  • "确定按钮" 和 "回车键":都连接到同一个 "提交数据" 的槽函数。
  • 多个 "删除按钮":列表里的每一行都有一个删除按钮,所有删除按钮的点击信号,都连接到同一个 "删除行" 的槽函数。
6.4.1 多对一的代码示例

我们来写一个示例:两个按钮,点击任何一个,都修改同一个标签的文字。

cpp 复制代码
#include <QLabel>
#include <QPushButton>

// 创建标签
QLabel *lbl_result = new QLabel(this);
lbl_result->setText("等待点击按钮...");
lbl_result->setFixedSize(300, 50);
lbl_result->move(300, 150);
lbl_result->setFont(QFont("微软雅黑", 16));

// 创建按钮1
QPushButton *btn1 = new QPushButton(this);
btn1->setText("按钮1");
btn1->setFixedSize(150, 50);
btn1->move(200, 250);

// 创建按钮2
QPushButton *btn2 = new QPushButton(this);
btn2->setText("按钮2");
btn2->setFixedSize(150, 50);
btn2->move(450, 250);

// ============== 多对一:多个信号连接同一个槽 ==============
// 按钮1的 clicked 信号,连接到 Lambda 槽
connect(btn1, &QPushButton::clicked, this, [=](){
    lbl_result->setText("按钮1被点击了!");
});

// 按钮2的 clicked 信号,也连接到同一个 Lambda 槽(当然也可以是自定义槽函数)
connect(btn2, &QPushButton::clicked, this, [=](){
    lbl_result->setText("按钮2被点击了!");
});
6.4.2 进阶:怎么在槽函数里区分是哪个信号发出的?

很多新手会问:多个信号连接到同一个槽函数,我怎么知道是哪个信号触发的槽函数?

方法很简单:使用 Qt 提供的 sender() 函数!

sender() 函数是 QObject 类的成员函数,它的作用是:返回发出当前信号的对象的指针。

注意:sender() 函数只有在槽函数被信号触发时才有效,如果你直接调用槽函数,sender() 返回 nullptr。

代码示例:用 sender () 区分是哪个按钮点击的

cpp 复制代码
// 在 widget.h 里声明一个自定义槽函数
private slots:
    void onAnyButtonClicked();

// 在 widget.cpp 里实现槽函数
void Widget::onAnyButtonClicked()
{
    // 【核心】用 sender() 获取发出信号的对象指针
    QPushButton *btn = qobject_cast<QPushButton*>(sender());
    
    // 判断一下是否转换成功(防止不是按钮发出的信号)
    if (btn == nullptr) {
        return;
    }
    
    // 通过按钮的 objectName 或者 text 来区分是哪个按钮
    if (btn->text() == "按钮1") {
        lbl_result->setText("通过 sender() 识别:按钮1被点击了!");
    } else if (btn->text() == "按钮2") {
        lbl_result->setText("通过 sender() 识别:按钮2被点击了!");
    }
}

// 在构造函数里关联信号与槽
connect(btn1, &QPushButton::clicked, this, &Widget::onAnyButtonClicked);
connect(btn2, &QPushButton::clicked, this, &Widget::onAnyButtonClicked);

逐行解析核心部分:

  1. QPushButton *btn = qobject_cast<QPushButton*>(sender());:
    • sender() 返回的是 QObject* 类型,我们需要用 qobject_cast 把它转换成 QPushButton* 类型。
    • qobject_cast 是 Qt 提供的安全的类型转换函数,如果转换失败(比如发出信号的不是按钮),会返回 nullptr。
  2. if (btn->text() == "按钮1"):通过按钮的 text() 或者 objectName() 来区分是哪个按钮。

警告 :sender() 函数虽然好用,但不要滥用!如果你的逻辑非常复杂,建议还是给每个信号写单独的槽函数,或者用 Lambda 表达式捕获不同的参数,代码可读性会更好。

6.5 信号与槽的断开:disconnect 函数详解

讲完了连接,我们再讲断开连接 :disconnect 函数。

大多数情况下,我们不需要手动断开连接,因为当对象被销毁时(比如窗口关闭了),它的所有信号与槽连接会自动断开,Qt 会帮你处理好一切。

但在某些特殊场景下,我们需要手动断开连接,比如:

  • 暂停某个功能:比如 "暂停自动保存",不让 "定时器" 的信号再触发 "保存" 的槽函数。
  • 避免重复连接 :比如你在一个函数里写了 connect,这个函数可能会被调用多次,导致同一个信号连接了多次同一个槽,信号发出时,槽函数会执行多次,这时候你需要先断开旧的连接,再连接新的。
  • 对象生命周期管理:比如接收者对象要被销毁了,你想在销毁前手动断开连接(虽然 Qt 会自动处理,但手动断开更安全)。
6.5.1 disconnect 函数的几种常用用法

disconnect 函数的用法和 connect 几乎完全一样,参数也一样,我们来看几种最常用的断开方式。

方式 1:用 connect 的返回值断开(最推荐!最精准!)

这是最推荐的断开方式,因为它最精准,只会断开你指定的那一个连接,不会误断开其他连接。

用法:

  1. 在 connect 的时候,用一个 QMetaObject::Connection 类型的变量保存返回值。
  2. 要断开的时候,把这个变量传给 disconnect 函数。

代码示例:

cpp 复制代码
// 在 widget.h 里声明:QMetaObject::Connection m_conn;

// 在构造函数里 connect,并保存返回值
m_conn = connect(btn_close, &QPushButton::clicked, this, &Widget::close);

// 后面要断开的时候
if (m_conn) { // 先判断一下连接是否有效
    disconnect(m_conn);
    m_conn = QMetaObject::Connection(); // 重置为空,避免重复断开
    qDebug() << "连接已断开!";
}

新手避坑 :断开连接后,最好把 QMetaObject::Connection 变量重置为空,否则下次你再判断 if (m_conn) 时,它还是 true,但连接已经无效了。

方式 2:断开某个发送者的所有信号的所有连接

如果你想断开某个对象的所有信号的所有连接,可以用这个方式。

用法 :disconnect(发送者指针);

代码示例:

复制代码
// 断开 btn_close 对象的所有信号的所有连接
disconnect(btn_close);

这会断开 btn_close 的 clicked()、pressed()、released() 等所有信号的所有连接,慎用!

方式 3:断开某个特定信号的所有连接

如果你想断开某个对象的某个特定信号的所有连接,可以用这个方式。

用法 :disconnect(发送者指针, &发送者类名::信号, nullptr, nullptr);

代码示例:

复制代码
// 断开 btn_close 对象的 clicked 信号的所有连接
disconnect(btn_close, &QPushButton::clicked, nullptr, nullptr);

这会断开 btn_close 的 clicked() 信号连接的所有槽函数,但不会断开其他信号(比如 pressed())的连接。

方式 4:断开某个特定的信号与槽的连接(和 connect 参数完全一样)

如果你知道要断开的信号和槽的具体参数,可以用和 connect 完全一样的参数来断开。

用法:disconnect(发送者指针, &发送者类名::信号, 接收者指针, &接收者类名::槽);

代码示例:

复制代码
// 断开 btn_close 的 clicked 信号和 this 的 close 槽的连接
disconnect(btn_close, &QPushButton::clicked, this, &Widget::close);
6.5.2 完整的代码示例:连接与断开

我们来写一个完整的示例:两个按钮,一个 "连接",一个 "断开",控制 "打印" 按钮的信号与槽。

cpp 复制代码
#include <QPushButton>
#include <QDebug>

// 在 widget.h 里声明:
// QMetaObject::Connection m_printConn;
// QPushButton *m_btn_print;

// 在构造函数里实现
// 创建打印按钮
m_btn_print = new QPushButton(this);
m_btn_print->setText("打印");
m_btn_print->setFixedSize(200, 50);
m_btn_print->move(300, 150);

// 创建连接按钮
QPushButton *btn_connect = new QPushButton(this);
btn_connect->setText("连接信号槽");
btn_connect->setFixedSize(150, 50);
btn_connect->move(200, 250);

// 创建断开按钮
QPushButton *btn_disconnect = new QPushButton(this);
btn_disconnect->setText("断开信号槽");
btn_disconnect->setFixedSize(150, 50);
btn_disconnect->move(450, 250);

// 点击"连接"按钮:建立连接
connect(btn_connect, &QPushButton::clicked, this, [=](){
    // 先判断一下是否已经连接,避免重复连接
    if (!m_printConn) {
        m_printConn = connect(m_btn_print, &QPushButton::clicked, this, [=](){
            qDebug() << "【打印】按钮被点击了!";
        });
        qDebug() << "信号槽连接成功!";
    } else {
        qDebug() << "信号槽已经连接了,无需重复连接!";
    }
});

// 点击"断开"按钮:断开连接
connect(btn_disconnect, &QPushButton::clicked, this, [=](){
    if (m_printConn) {
        disconnect(m_printConn);
        m_printConn = QMetaObject::Connection(); // 重置为空
        qDebug() << "信号槽断开成功!";
    } else {
        qDebug() << "信号槽已经断开了,无需重复断开!";
    }
});
6.5.3 运行效果

点击运行按钮:

  1. 先点击 "打印" 按钮,没反应(因为还没连接)。
  2. 点击 "连接信号槽" 按钮,提示 "信号槽连接成功!"。
  3. 再点击 "打印" 按钮,打印 "【打印】按钮被点击了!"。
  4. 点击 "断开信号槽" 按钮,提示 "信号槽断开成功!"。
  5. 再点击 "打印" 按钮,又没反应了。

完美!我们成功实现了手动连接和断开信号与槽!


第七章 UI 设计师的魔法:自动生成槽函数(命名规则)

除了用纯代码写 connect,Qt Creator 的 UI 设计师(UI Designer) 还提供了一个极其方便的功能:自动生成槽函数。

你只需要在 UI 设计师里右键点击控件,选择 "转到槽...",Qt Creator 就会自动帮你:

  1. 在头文件里自动添加槽函数的声明。
  2. 在源文件里自动添加槽函数的定义(空函数体)。
  3. 自动完成信号与槽的连接,不需要你写一行 connect 代码!

7.1 自动生成槽函数的核心:命名规则(必记!)

Qt Creator 能自动生成并自动连接,靠的是一套严格的命名规则。只要你的槽函数名严格符合这套规则,Qt 的元对象编译器(MOC)就会在编译时自动扫描到它,并自动完成连接,不需要你写 connect!

7.1.1 命名规则的完整格式

自动生成的槽函数名,必须严格遵守以下格式:

复制代码
on_控件的objectName_信号名()

我们把这个格式拆解开,每个部分都给你讲透:

  1. 固定前缀 on_:
    • 必须以小写的 on_ 开头,这是 Qt 识别自动连接槽函数的标志。
    • 注意:是小写的 on_,不是大写的 On_ 或 ON_,也不是 on-(用连字符),必须完全一致!
  2. 中间是控件的 objectName:
    • on_ 后面,必须是 UI 设计师里那个控件的 objectName 属性的值。
    • 注意:大小写完全敏感! 比如 UI 里的 objectName 是 pushButton_Login,你槽函数名里必须写成 pushButton_Login,少一个字母、大小写错了都不行!
  3. 然后又是下划线 _:
    • 用来分隔 objectName 和信号名,必须是单个下划线 _,不能是多个,也不能是其他符号。
  4. 最后是信号名:
    • 必须是控件支持的信号的名字,同样大小写完全敏感!
    • 比如按钮的点击信号是 clicked,你必须写成 clicked,不能写成 Click 或 click。
7.1.2 完整的示例

我们用一个具体的例子,来演示这个命名规则:

  • 控件类型:QPushButton(按钮)。
  • 控件的 objectName:btn_closeWindow(注意是这个,不是按钮显示的文字!)。
  • 信号名:clicked(无参的点击信号)。
  • 自动生成的槽函数名:on_btn_closeWindow_clicked()。

只要你的槽函数名是 on_btn_closeWindow_clicked(),Qt 就会自动把它和 UI 里 objectName 为 btn_closeWindow 的按钮的 clicked() 信号绑定在一起,不需要你写任何 connect 代码!

7.2 实战:用 UI 设计师自动生成槽函数

光说不练假把式,我们跟着步骤,一步步来用 UI 设计师自动生成槽函数,实现 "点击按钮关闭窗口" 的功能。

7.2.1 第一步:创建项目并进入 UI 设计模式
  1. 新建一个 Qt Widgets Application 项目,基类选 QWidget,勾选 "创建界面"。
  2. 双击项目文件列表里的 widget.ui 文件,进入 UI 设计模式。
7.2.2 第二步:在 UI 里放控件并设置 objectName
  1. 从左侧的「控件库」(Widget Box)里,找到「Buttons」组,拖一个 Push Button 到中间的窗体上。
  2. 选中这个按钮,看右侧的「属性编辑器」(Property Editor):
    • 找到 objectName 属性(一般在最上面),把它改成 btn_close(规范命名,btn 前缀表示按钮,方便识别)。
    • 找到 text 属性,把按钮显示的文字改成 "关闭窗口(UI 自动生成)"。
  3. 你可以再拖一个 Label 控件到窗体上:
    • objectName 改成 lbl_hello。
    • text 改成 "等待点击按钮..."。
    • 字体设置大一点,方便看。
7.2.3 第三步:右键控件 "转到槽...",自动生成代码

这是最神奇的一步:

  1. 在 UI 设计模式里,右键点击 刚才的按钮(btn_close)。
  2. 在弹出的右键菜单里,选择 「转到槽...」(Go to slot...) 。
  3. 会弹出一个对话框,里面列出了这个按钮所有支持的信号:
    • clicked():无参的点击信号(最常用)。
    • clicked(bool):带 bool 参数的点击信号(用于复选框等有状态的按钮)。
    • pressed():鼠标按下时触发。
    • released():鼠标松开时触发。
  4. 选择我们最常用的 clicked() (无参的那个),点击右下角的「OK」。

见证奇迹的时刻到了!Qt Creator 会自动帮你做两件事:

自动做的第一件事:在 widget.h 里添加槽函数声明

Qt Creator 会自动跳转到 widget.h 文件,并在 private slots: 区域(如果没有会自动创建)添加槽函数的声明:

复制代码
private slots:
    // 【自动生成】完全符合命名规则!
    // on_ + objectName(btn_close) + _ + 信号名(clicked)
    void on_btn_close_clicked();

你看,这个函数名完全符合我们刚才讲的命名规则!

自动做的第二件事:在 widget.cpp 里添加槽函数定义

Qt Creator 会自动跳转到 widget.cpp 文件,并添加槽函数的定义(空函数体):

复制代码
// 【自动生成】槽函数的定义
void Widget::on_btn_close_clicked()
{
    // 这里是空的,你自己写业务逻辑
}
7.2.4 第四步:在自动生成的槽函数里写逻辑

现在,我们只需要在自动生成的空函数体里写逻辑就行,不需要写任何 connect 代码!

我们写两个逻辑:

  1. 修改标签的文字。
  2. 延迟 1 秒后关闭窗口(让用户看到文字变化)。

在 widget.cpp 的自动生成函数里添加代码:

cpp 复制代码
#include "widget.h"
#include "ui_widget.h"
// 引入 QTimer 头文件,用来做延迟
#include <QTimer>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
}

// 【自动生成的槽函数】我们只需要写里面的逻辑
void Widget::on_btn_close_clicked()
{
    // 1. 修改标签的文字:通过 ui 指针访问 UI 里的控件
    // ui-> 后面跟控件的 objectName,就能访问 UI 里的控件了
    ui->lbl_hello->setText("再见!窗口即将关闭...");
    
    // 2. 延迟 1 秒后关闭窗口
    // QTimer::singleShot 是 Qt 的静态定时器函数,只触发一次
    // 参数1:延迟时间,单位是毫秒(1000毫秒 = 1秒)
    // 参数2:接收者 this
    // 参数3:Lambda 表达式,写延迟后要执行的逻辑
    QTimer::singleShot(1000, this, [=](){
        this->close(); // 关闭窗口
    });
}

Widget::~Widget()
{
    delete ui;
}

逐行解析核心部分:

  1. ui->lbl_hello->setText("再见!窗口即将关闭...");:
    • ui 是 Qt Creator 自动生成的指针,指向 UI 界面。
    • 通过 ui->控件的objectName,你可以直接访问 UI 设计师里的所有控件,非常方便。
  2. QTimer::singleShot(1000, this, [=](){ ... });:
    • 这是 Qt 的静态定时器函数,只触发一次,用来做简单的延迟非常方便。
7.2.5 第五步:运行程序,看效果

点击运行按钮:

  1. 你会看到 UI 设计师里设计的界面,有一个标签和一个按钮。
  2. 点击 "关闭窗口(UI 自动生成)" 按钮。
  3. 标签文字变成 "再见!窗口即将关闭..."。
  4. 1 秒后,窗口自动关闭。

完美!我们没有写一行 connect 代码,完全靠 Qt Creator 自动生成和自动连接,就实现了功能!是不是非常方便?

7.3 坑:自动生成槽函数的注意事项

使用自动生成槽函数,有几个新手 99% 会踩的坑,我在这里给你列出来,帮你避坑。

坑 1:大小写不敏感?不!Qt 对大小写极其敏感!

这是新手最容易犯的错:

  • UI 里的 objectName 是 Btn_Close(B 和 C 大写),你槽函数名写成 on_btn_close_clicked()(全小写)→ 失效!
  • 信号名是 Clicked(C 大写)→ 失效!
  • 前缀写成 On_(O 大写)→ 失效!

记住 :整个命名规则里,每一个字母的大小写都必须完全一致,一个都不能错!错了 Qt 就识别不出来,自动连接就会失效,运行起来点了按钮没反应。

坑 2:修改了控件的 objectName,没有手动修改槽函数名!

很多新手会遇到这个问题:

  1. 你先用 UI 设计师生成了槽函数 on_btn_close_clicked()。
  2. 后来你在 UI 设计师里,把按钮的 objectName 从 btn_close 改成了 btn_exit。
  3. 运行起来,点击按钮没反应了!

原因 :Qt 不会自动帮你修改槽函数名!你把 objectName 改成了 btn_exit,但槽函数名还是 on_btn_close_clicked(),不符合命名规则了,自动连接就失效了。

解决方法 :你必须手动把槽函数名从 on_btn_close_clicked() 改成 on_btn_exit_clicked(),包括头文件里的声明和源文件里的定义,都要改!

坑 3:类里没有加 Q_OBJECT 宏!

这个我们已经强调过无数次了:只要用信号与槽,类里必须加 Q_OBJECT 宏! 自动生成的槽函数也是信号与槽的一部分,不加的话,自动连接会失效,甚至会编译报错。

坑 4:同时手动写 connect 和用自动生成,导致槽函数执行两次!

很多新手会犯这个错:

  1. 你用 UI 设计师自动生成了槽函数。
  2. 你又在构造函数里手动写了一行 connect 代码,连接同一个信号和同一个槽。
  3. 运行起来,点击一次按钮,槽函数执行了两次!

原因:自动连接已经帮你连了一次,你手动又连了一次,所以信号发出时,槽函数会执行两次。

解决方法 :用了自动生成槽函数,就不要再手动写 connect 了;或者用手动 connect,就不要用自动生成的命名规则,二者选其一,不要同时用。

坑 5:控件的 objectName 里有特殊字符或空格!

UI 设计师里,控件的 objectName 最好只用字母、数字和下划线 ,不要用空格、中文、特殊字符(比如 @、#、-),否则槽函数名会很难写,甚至会导致编译报错(因为 C++ 函数名里不能有特殊字符)。

正确的 objectName 命名 :btn_close、lbl_hello、edit_username(用下划线分隔单词)。

错误的 objectName 命名 :btn close(有空格)、按钮(中文)、btn-close(连字符)。

7.4 显式 connect VS 自动生成:该怎么选?

最后,我们来对比一下 "显式手写 connect" 和 "UI 设计师自动生成" 这两种方式的优缺点,告诉你该怎么选。

对比项 显式手写 connect(Qt5 函数指针) UI 设计师自动生成(命名规则)
灵活性 非常灵活:可以连接任意对象的信号和槽,支持 Lambda,支持信号连信号 灵活性差:只能连接 UI 里的控件的信号到当前窗口的槽
可读性 连接关系一目了然:看 connect 代码就知道谁连了谁 连接关系隐藏:需要看槽函数名才知道
类型检查 有编译时类型检查:安全,参数错了编译报错 依赖命名规则:命名错了运行时才发现,编译不报错
便捷性 需要自己写代码:稍微麻烦一点 极其方便:右键点一下,自动生成代码,自动连接
推荐场景 1. 纯代码创建的控件2. 复杂的连接逻辑3. 用 Lambda 表达式4. 信号连信号、一对多、多对一 1. UI 设计师里拖的控件2. 简单的 "控件触发,窗口响应" 逻辑3. 想快速实现,不想写 connect
建议
  • 如果你是用 UI 设计师拖的控件,且逻辑简单 (比如 "点击按钮关闭窗口"、"点击按钮修改标签文字"),推荐用 UI 设计师自动生成,右键点一下就行,方便快捷,不容易出错。
  • 如果你是纯代码创建的控件,或者逻辑复杂 (比如要用 Lambda、要一对多、要信号连信号),推荐显式手写 connect,灵活且安全。

第八章 指南

很多新手学 Qt 信号与槽,最痛苦的不是写代码,而是遇到报错或问题时,根本不知道哪里错了,对着代码看半天也找不出来。

8.1 编译报错类

报错 1:undefined reference to vtable for xxx(经典中的经典!)

报错信息:

复制代码
undefined reference to vtable for Widget
collect2: error: ld returned 1 exit status

原因 :99% 的原因是你在类里加了 signals、slots 或者 Q_OBJECT 宏,但是没有重新执行 qmake!

  • Qt 的 MOC 工具需要在 qmake 的时候扫描你的代码,生成元对象代码。
  • 你加了 Q_OBJECT 宏之后,没有重新 qmake,MOC 工具没有生成对应的代码,链接器就找不到 vtable(虚函数表),就会报这个错。

解决方法:

  1. 点击 Qt Creator 顶部菜单栏的「构建」->「执行 qmake」。
  2. 然后点击「构建」->「重新构建项目」(Rebuild Project)。
  3. 如果还不行,就手动删除项目目录下的 build-xxx 文件夹(构建生成的临时文件),然后重新 qmake,重新构建。
报错 2:no matching member function for call to 'connect'

报错信息:

复制代码
error: no matching member function for call to 'connect'

原因 :connect 函数的参数写错了,编译器找不到匹配的 connect 函数。常见的原因有:

  1. 信号名或槽函数名写错了(拼写错误、大小写错误)。
  2. 信号或槽函数的参数列表不匹配。
  3. 信号或槽函数是私有的,访问不到。
  4. 信号有重载,编译器不知道选哪个版本。

排查方法:

  1. 检查信号名和槽函数名的拼写、大小写是否完全正确。
  2. 检查信号和槽的参数列表是否兼容(类型一致、槽的参数个数不能多于信号)。
  3. 检查信号是否写在 signals: 下面,槽函数是否是 public 的。
  4. 如果信号有重载,用函数指针明确指定重载版本。
报错 3:reference to non-static member function must be called

报错信息:

复制代码
error: reference to non-static member function must be called; did you mean to call it with no arguments?

原因 :你在 connect 的信号或槽函数名后面加了括号!

  • 比如写成了 &QPushButton::clicked(),加了括号就变成了 "调用函数",而不是 "取函数指针"。

解决方法 :把信号和槽函数名后面的括号删掉,只写函数名,比如 &QPushButton::clicked。

报错 4:'xxx' is not a member of 'yyy'

报错信息:

复制代码
error: 'clicked' is not a member of 'QPushButton'

原因:你写的信号名或槽函数名,在对应的类里根本不存在。常见的原因有:

  1. 拼写错误、大小写错误。
  2. 信号是父类里的,你以为是子类里的(其实这个没问题,因为继承了,但是如果你写错了类名就不行)。
  3. 你自己写的信号或槽函数,忘记在头文件里声明了。

解决方法:

  1. 仔细检查拼写和大小写。
  2. 查 Qt 帮助文档,确认这个类里确实有这个信号或槽。
  3. 检查自己写的信号或槽函数,是否在头文件里声明了。

8.2 运行不报错,但点了按钮没反应类

这是新手最痛苦的问题:编译不报错,但是运行起来点了按钮一点反应都没有,根本不知道哪里错了。别慌,按照下面的步骤一步步排查,99% 能找到问题。

排查步骤 1:先判断 connect 是否成功

这是第一步,也是最重要的一步:先看看你的 connect 到底成功了没有。

方法 :用 connect 的返回值判断。

复制代码
QMetaObject::Connection conn = connect(btn, &QPushButton::clicked, this, &Widget::doSomething);

if (conn) {
    qDebug() << "连接成功!";
} else {
    qDebug() << "连接失败!";
}

运行程序,看「应用程序输出」窗口:

  • 如果打印 "连接失败!":说明你 connect 的参数写错了,回到上面的编译报错类排查。
  • 如果打印 "连接成功!":说明连接没问题,继续往下排查。
排查步骤 2:在槽函数里第一行加 qDebug () 打印

如果连接成功了,但还是没反应,那可能是槽函数里的代码有问题,或者槽函数根本没被执行。

方法 :在槽函数的第一行加一个 qDebug() 打印。

复制代码
void Widget::doSomething()
{
    qDebug() << "槽函数被执行了!"; // 第一行就打印
    // 下面是你的业务逻辑
    // ...
}

运行程序,点击按钮,看「应用程序输出」窗口:

  • 如果没有打印 "槽函数被执行了!":说明槽函数根本没被调用,继续往下排查。
  • 如果打印了 "槽函数被执行了!":说明信号与槽没问题,是你槽函数里的业务逻辑有问题(比如控件指针是 nullptr、文件路径错了等等),去检查槽函数里的代码。
排查步骤 3:检查信号是否真的发出了

如果连接成功了,但槽函数没被执行,那可能是信号根本没发出。

方法 :对于系统自带的信号(比如按钮的 clicked),一般不会有问题,除非按钮被禁用了(setEnabled(false))。

  • 检查按钮是否被禁用了:btn->isEnabled(),如果返回 false,说明按钮被禁用了,点击不会发出信号。
  • 检查按钮是否被其他控件挡住了:如果按钮被另一个透明的控件挡住了,你点击的其实是上面的控件,按钮收不到点击事件,自然不会发出信号。

对于自定义信号:检查你是否用 emit 发送了信号,发送信号的代码是否被执行了(在 emit 前面加 qDebug() 打印)。

排查步骤 4:检查自动生成槽函数的命名规则

如果你用的是 UI 设计师自动生成的槽函数,点了没反应,99% 是命名规则的问题。

检查清单:

  1. 槽函数名是否严格符合 on_objectName_信号名() 的格式?
  2. objectName 是否和 UI 设计师里的完全一致(大小写、拼写)?
  3. 信号名是否和控件支持的完全一致(大小写、拼写)?
  4. 前缀是不是小写的 on_?
  5. 类里有没有加 Q_OBJECT 宏?
  6. 有没有重新执行 qmake?
排查步骤 5:检查接收者对象是否还存在

如果连接成功了,信号也发出了,但槽函数没执行,可能是接收者对象已经被销毁了。

方法 :检查接收者对象是否被 delete 了,或者是否已经超出了作用域(比如是栈对象,函数执行完就销毁了)。

8.3 其他常见问题

问题 1:槽函数执行了两次!

原因:同一个信号和同一个槽,被连接了两次!

  • 可能是你手动写了一次 connect,又用了自动生成槽函数,自动连了一次。
  • 可能是你把 connect 写在了一个会被调用多次的函数里(比如 showEvent),每次调用都连一次。

解决方法:

  1. 不要同时用手动 connect 和自动生成槽函数,二者选其一。
  2. 在 connect 之前,先 disconnect 旧的连接,或者用 QMetaObject::Connection 变量判断是否已经连接了。
问题 2:用 Lambda 表达式时,程序崩溃!

原因 :99% 是你用 [&] 引用捕获了局部变量,局部变量被销毁了,Lambda 里访问了野指针。

解决方法:

  1. 捕获列表优先用 [=],不要用 [&] 捕获局部变量。
  2. 如果要修改外部变量,把变量设为类的成员变量。

结语:

恭喜你!终于看完了这篇超十万字的 Qt 信号与槽保姆级全解析!

如果你跟着文章一步步敲了代码,做了练习,排查了报错,那么现在你已经完全掌握了 Qt 信号与槽的所有核心用法:

  • 你彻底搞懂了信号与槽的本质。
  • 你会用 connect 关联系统自带的信号与槽,每个参数都了如指掌。
  • 你会自己写自定义信号与自定义槽,会用 emit 发送信号,会通过信号传递数据。
  • 你会解决信号与槽的重载歧义问题。
  • 你会熟练使用 Lambda 表达式作为槽函数。
  • 你知道一对一、一对多、多对一、信号连信号等多种连接方式。
  • 你会用 disconnect 手动断开连接。
  • 你会用 UI 设计师自动生成槽函数,知道背后的命名规则。
  • 你能独立排查新手 99% 会遇到的报错和问题。

毫不夸张地说,掌握了信号与槽,你就掌握了 Qt 开发的灵魂!接下来,你可以用 Qt 实现各种复杂的用户交互,开发出功能完整的图形界面程序了。

信号与槽的核心要点回顾(最后再记一遍!)

在结束之前,我们最后再回顾一下信号与槽的核心要点,帮你巩固记忆:

  1. 类的要求 :
    • 必须继承自 QObject。
    • 必须加 Q_OBJECT 宏,加了之后必须重新执行 qmake。
  2. 自定义信号的规范 :
    • 写在 signals: 下面。
    • 返回值是 void,只声明不实现。
    • 用 emit 发送。
  3. 自定义槽函数的规范 :
    • 写在 slots: 或 public 下面。
    • 返回值一般是 void,需要声明也需要实现。
  4. connect 函数(Qt5 推荐) :
    • connect(发送者指针, &发送者类名::信号, 接收者指针, &接收者类名::槽);
    • 信号和槽的参数列表必须兼容:类型一致,槽的参数个数不能多于信号。
  5. Lambda 表达式 :
    • 捕获列表优先用 [=],安全第一。
    • 逻辑简单用 Lambda,逻辑复杂用自定义槽函数。
  6. UI 自动生成槽函数 :
    • 命名规则:on_objectName_信号名(),大小写完全敏感。
  7. 新手排查问题 :
    • 先看 connect 是否成功。
    • 再在槽函数里加 qDebug () 看是否执行。
    • 最后检查业务逻辑。

编程学习没有捷径,唯手熟尔。Qt 信号与槽虽然看起来知识点很多,但只要你多敲代码、多做练习、多查帮助文档、多遇到问题并解决问题,很快就能熟练掌握。

这篇文章虽然很长,但我相信,如果你认真看完了,跟着敲了代码,你一定会对 Qt 信号与槽有一个极其深刻的理解,再也不会为交互逻辑发愁。

感谢你阅读这篇文章,希望它能帮到你。我们下一篇 Qt 教程再见!

相关推荐
WL学习笔记2 小时前
使用string类
c++·后端·string类
xxxiugou1232 小时前
链表算法题精讲:从两数相加到K组翻转
数据结构·c++·算法·链表
HAPPY酷2 小时前
C++ 自定义内存池优化点云处理:从裸指针到 RAII 自动回收
开发语言·c++
zhangdaliangmvp3 小时前
快速排序的学习理解:从基础分治到三路划分优化
c++·学习·算法
cu1433 小时前
细谈IT6563的具体功能与其应用
c语言·c++·单片机·嵌入式硬件
布莱克6053 小时前
C++ 内存泄漏全解析:常见场景、成因与规避实践
开发语言·c++·内存泄露
jimy14 小时前
lvalue、xvalue、prvalue(一):三者的分类
开发语言·c++
HAPPY酷4 小时前
C++ 内存泄漏排查:从裸 new 到 RAII 修复,附 ASan 实战
开发语言·c++
凯瑟琳.奥古斯特6 小时前
力扣1015C++解法技巧
c++·算法·leetcode·链表·职场和发展