📹 第三章 FFmpeg二次开发采集并预览本地摄像头
📄 一句话概括

槽的定义只有一句:能被调用的东西。 普通函数、成员方法、lambda、绑定了参数的函数,全都行。

📝 本节和前面几节的关系

上一节讲了发出方,这一节讲接收方。 「槽」这个词听着像专有名词,其实是「C++ 里能被取地址调用的东西」, 和 Python 的 callable 是同一件事。 四种形式各有各的场景,尤其第四种(绑定参数)在工程里最常用。

一、四种槽,一张表

D35 把这四种都连上同一个信号,全都能调。

| 形式 | 写法 | 典型用途 | |---|---|---|---| | 普通函数 | connect(fn) | 工具函数、日志 | | 成员方法 | connect(obj.method) | 最常见 | | lambda | connect(lambda x: …) | 临时处理、捕获上下文 | | 绑定参数 | partial(fn, 固定值) | 一个槽服务多个控件 |

第四行是工程里最值钱的一招:十个按钮共用一个处理函数,靠 partial 把「我是几号」粘进去。 Qt 老版本用 QSignalMapper(已废弃),新版本一律用 lambda 捕获或者 sender()。

二、lambda 捕获的隐患

lambda 能捕获外部变量,这是它最大的好处,也是最大的坑。

捕获什么 风险
按值捕获(Python 默认) 安全,但拿到的是旧值
捕获 this / self 对象销毁后就是野指针
捕获局部引用(C++ 的 &) 函数返回后悬空

第二行是 Qt 里最典型的崩法:connect(btn, &Button::clicked, [this]{ this->doSomething(); }), 窗口关了、this 没了,按钮还在发信号 → 崩溃。 Qt5 的解法是 connect 传第三个参数(context):context 对象销毁,连接自动断开。

Python 里也有对应问题但表现不同:捕获的是对象引用,所以对象不会被回收(引用计数不减), 结果是「内存泄漏」而不是「崩溃」。两种都不好。

三、参数匹配规则

这条规则必须记准,它是「连不上」的最常见原因:

信号 槽 结果
(int, int) (int, int) 完全匹配
(int, int) (int) 允许,多的丢掉
(int) (int, int) 接不上

第二行在 Qt 里是允许的:槽可以只关心前几个参数, 这一点在「我只想知道进度,不关心总量」时很有用。 第三行反过来了就不行,因为槽要的参数信号给不出来。

新式写法(&Class::slot)下这是编译错误,看得见; 旧式字符串写法下是运行期静默失败——connect 返回 false,而很多人不看返回值。 所以「连不上」第一件事:看 connect 的返回值。

四、槽里能做什么,不能做什么

槽就是普通函数,技术上什么都能做。但工程上有三条纪律:

纪律 原因
别做耗时操作 直连时会把发送方拖住
别改界面以外的共享状态 跨线程时没有锁
别在槽里 delete 发送者 信号还在调用栈上

第一条在 3.4.4 会量:槽里跑 20 毫秒,发送方就要等 20 毫秒,界面直接卡住。 第三条更阴:在槽里删掉发信号的对象,信号返回后还会继续访问它——这是典型的 use-after-free。 正确做法是投递一个「稍后删除」的消息(Qt 的 deleteLater)。

五、动手 Demo:四种槽连同一个信号,看顺序和绑定效果

普通函数、成员方法、lambda、partial 绑定参数,四种都连到同一个信号上,发一次看谁被调、 按什么顺序。 重点是第四种:它把「固定参数」和「信号参数」拼在一起。

import functools

def plain(x):
    out.append("普通函数(%s)" % x)

class Recv(object):
    def method(self, x):
        out.append("成员方法(%s)" % x)

sig.connect(plain)
sig.connect(Recv().method)
sig.connect(lambda x: out.append("lambda(%s)" % x))
sig.connect(functools.partial(
    lambda tag, x: out.append("绑定(%s,%s)" % (tag, x)), "K"))

实跑输出:

--------------------------------------------------------------------
槽可以是任何能被调用的东西
--------------------------------------------------------------------
**Qt 文档说槽是「普通成员函数」,其实更宽:只要能被调用,就能当槽。**

  槽的形式     写法                       典型用途
  ------------------------------------------------------------
  普通函数     `connect(fn)`              工具函数
  成员方法     `connect(obj.method)`      最常见
  lambda       `connect(lambda x: …)`     临时处理
  绑定参数     `partial(fn, 固定值)`      一个槽处理多按钮

  实际调用顺序:
    普通函数(v)
    成员方法(v)
    lambda(v)
    绑定参数(K,v)

**第四行是工程里最有用的一招**:十个按钮共用一个槽,靠 `partial` 把「我是几号」绑进去。
  **Qt 里对应的写法是 `QSignalMapper`(已废弃)或 lambda 捕获**,**新代码一律用 lambda。**

**但 lambda 有一个坑,必须现在说清:**如果 lambda 捕获了局部变量,而对象的生命周期比这个变量长,
**槽被调用时读到的是野指针。**
  **Qt 的解法是 `connect` 时传第三个参数(context 对象)**,那个对象销毁,连接自动断。
  **这一条在 3.4.4 会专门踩一遍。**

四种槽都按登记顺序被调用,partial 那个把 K 和信号给的 v 拼成了两个参数。 这就是「一个槽服务多个按钮」的实现原理:把按钮编号绑进去,槽里按编号分支。

注意 Recv().method 这一句:我直接用了临时对象的方法。 在 Python 里这样是安全的(绑定方法抓着对象引用), 但 C++ 里就是立即悬空——临时对象出了语句就析构了。 这个差别必须清楚:Python 的坑是「忘不掉」,C++ 的坑是「忘太早」。

functools.partial 是个冷门但好用的工具:它把参数「冻」在前面,剩下的留给调用者。 Qt 里对应的是 lambda 捕获,效果一样。

30 秒实验

  1. 把 partial 的 "K" 换成循环变量, 看十个按钮是不是各自记住自己的编号——注意 Python 的闭包晚绑定问题: 直接在 lambda 里用循环变量会全是最后一个。
  2. 把某个槽换成会抛异常的函数,看后面的槽还执行不执行。
  3. 用 partial 给槽多绑两个参数,看信号传进来的参数落在哪个位置。

常见误区

  1. 以为槽必须是成员函数。 任何可调用的都行,包括 lambda、partial、甚至一个列表的 append。
  2. 以为 lambda 捕获对象是安全的。 捕获 this/self 后对象被销毁,就是野指针。 Qt 的解法是 connect 时传 context 对象。
  3. 以为 C++ 里 Obj().method 也能用。 临时对象当场析构,连接立刻悬空。 Python 里安全是因为引用计数。
自测三问(点开对答案) 1. 槽的参数比信号少,能连上吗? —— 能。多的参数会被丢掉。反过来(槽要的参数比信号多)就连不上。 2. 为什么 lambda 捕获 this 会崩? —— 对象销毁后,捕获的指针还在,槽被调用时访问的是已释放的内存。 Qt 的解法是 connect 传 context 对象,自动断开。 3. `partial` 在槽里解决什么问题? —— 把固定参数绑在前面,剩下的留给信号传。常用于「十个按钮共用一个槽,靠编号区分」。

下一节讲怎么把信号和槽接上:connect 的五种方式,其中线程那两种是重点。

相关笔记