信号与槽机制
# 07.信号和槽机制
信号与槽是 Qt 最核心的 IPC 机制——观察者模式的 C++ 实现。但很多人只停留在
connect的 API 使用上,不理解moc生成的代码在运行时如何工作、QueuedConnection的事件队列副作用、以及新版语法相比SIGNAL/SLOT宏的本质改进。本章从编译器到运行时,完全拆解这一机制。
# 目录介绍
# 7.0 案例引入
一个新人在嵌入式板子上写了如下代码,程序运行时每隔几秒就崩溃:
class SensorReader : public QObject {
Q_OBJECT
public:
void start() {
m_thread = new QThread(this);
// ❌ 槽函数 doWork 在 SensorReader 线程执行(主线程)
// 但 m_thread 在工作线程——连接不合理
connect(m_thread, &QThread::started, this, &SensorReader::doWork);
m_thread->start();
}
public slots:
void doWork() {
while (m_running) {
qDebug() << QThread::currentThread(); // 主线程!
QThread::msleep(100);
}
}
private:
QThread* m_thread;
bool m_running = true;
};
现象:doWork 在主线程中死循环 → 阻塞事件循环 → UI 冻结。
根因:不理解 connect 的连接类型——默认 AutoConnection 判断信号/槽所在线程,槽对象的线程亲和性决定了槽在哪个线程执行。
# 7.1 基本概念
Qt 的信号与槽机制是其核心特性之一,用于实现对象之间的松耦合通信。这是 Qt 对观察者模式的 C++ 实现,具有类型安全、松耦合和跨线程通信等优点。
# 7.1.1 信号与槽
核心概念:
- 信号(Signal):对象发出的事件通知(如按钮点击)
- 槽(Slot):响应信号的普通成员函数
- 连接(Connect):通过
QObject::connect绑定信号与槽 - 触发(Emit):通过
emit关键字发射信号
// 关键元对象宏
Q_OBJECT // 必须——启用元对象系统
signals: // 声明信号(不实现)
public slots: // 声明槽(需要实现)
emit // 发射信号(非必须,但建议保留以示意图)
# 7.1.2 信号(Signal)
class SensorManager : public QObject {
Q_OBJECT
public:
void onNewReading(float value) {
if (value > m_threshold) {
emit thresholdExceeded(value); // 发射信号
}
}
signals:
void thresholdExceeded(float value); // 只有声明,无需实现
void dataReady(QVector<float> data); // 可带任意参数
};
关键规则:
- 信号函数的实现在
moc生成的moc_*.cpp中——你不写实现 - 信号可以是 virtual(Qt 5.7+)
- 返回值必须是
void
# 7.1.3 槽(Slot)
class Dashboard : public QObject {
Q_OBJECT
public slots:
void onThresholdExceeded(float value) {
qDebug() << "Alert! Value:" << value;
showWarningPopup();
}
void onDataReady(QVector<float> data) {
updateChart(data);
}
};
槽函数的参数数量可以少于信号的参数数量——Qt 会忽略多余的参数。
# 7.1.4 连接(Connect)
// 基本连接
connect(sender, &Sender::signal, receiver, &Receiver::slot);
// 旧式字符串语法(不推荐——编译期不检查类型)
connect(sender, SIGNAL(clicked()), receiver, SLOT(onClicked()));
# 7.2 连接语法详解
# 7.2.1 旧式宏语法 vs 新式成员指针
旧式 SIGNAL/SLOT 宏 | 新式成员指针 &Class::method | |
|---|---|---|
| 类型检查 | 运行时(字符串匹配) | 编译期(模板推导) |
| 重载支持 | 需 QOverload 手动消歧 | 用 qOverload / QOverload::of |
| 性能 | 字符串构造 + 查找 | 偏移量计算(快 2-3 倍) |
| 错误发现 | 运行时 false 返回值 | 编译失败 |
新式语法处理重载:
// 方式 1: QOverload
connect(slider, &QSlider::valueChanged,
this, QOverload<int>::of(&MyWidget::onValueChanged));
// 方式 2: qOverload(Qt 5.7+)
connect(slider, &QSlider::valueChanged,
this, qOverload<int>(&MyWidget::onValueChanged));
// 方式 3: Lambda(最简洁)
connect(slider, &QSlider::valueChanged, this, [this](int v) {
onValueChanged(v);
});
# 7.2.2 Lambda 表达式
Qt 5+ 支持 Lambda 作为槽——最灵活的方式:
// 基本 Lambda
connect(button, &QPushButton::clicked, []() {
qDebug() << "Button clicked";
});
// 带上下文——Lambda 在 receiver (this) 的线程执行
connect(button, &QPushButton::clicked, this, [this]() {
m_counter++; // 操作成员变量
});
// 一次性连接——执行一次后自动断开
connect(button, &QPushButton::clicked, this, [this]() {
doOneTimeSetup();
}, Qt::SingleShotConnection); // Qt 6.0+
// Lambda 中的 this 陷阱
// ❌ 如果 this 被删除,Lambda 变成野指针访问!
// ✅ 使用 receiver 参数:connect(sender, sig, receiver, lambda)
// 当 receiver 被删除时,连接自动断开
# 7.2.3 QMetaObject::Connection
connect 返回 QMetaObject::Connection,可复制、移动,但不可默认构造:
QMetaObject::Connection conn = connect(sender, &Sender::sig, receiver, &Receiver::slot);
if (conn) {
qDebug() << "Connected successfully";
}
disconnect(conn); // 断开这个特定连接
# 7.3 MOC 编译原理
# 7.3.1 信号生成的代码
当 moc 处理带 Q_OBJECT 的头文件时,为每个信号生成实现:
// 你在头文件中写的:
signals:
void valueChanged(int value);
// moc 生成的实现(简化):
void MyClass::valueChanged(int _t1) {
void *_a[] = { nullptr, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) };
QMetaObject::activate(
this,
&staticMetaObject,
1, // 信号的 meta 索引
_a // 参数数组
);
}
QMetaObject::activate 内部做三件事:
- 遍历该信号的连接列表
- 对每个连接,根据连接类型决定执行方式
- 如果是
DirectConnection→ 直接调用槽函数;如果是QueuedConnection→ 投递QMetaCallEvent到目标线程的事件队列
# 7.3.2 运行时调用链路
emit valueChanged(42)
↓
moc 生成的 valueChanged(int) 函数
↓
QMetaObject::activate(this, metaObj, signalIdx, args)
↓
遍历连接列表 → 对每个 receiver:
├── DirectConnection → receiver->qt_metacall(slotIdx, args)
│ ↓ 函数指针跳转(类似虚函数表)
│ onValueChanged(42)
│
└── QueuedConnection → QCoreApplication::postEvent(receiver,
new QMetaCallEvent(slotIdx, args))
↓ 进入接收者线程的事件循环
↓ event() 分发 QMetaCallEvent
↓ receiver->qt_metacall(slotIdx, args)
↓
onValueChanged(42)
性能关键:qt_metacall 使用 switch-case 分派到槽函数——编译期确定的跳转表,比虚函数调用开销稍大(多个 switch),但在 ARM 嵌入式上仍 < 50ns。
# 7.4 连接类型深度分析
# 7.4.1 五种连接类型
| 连接类型 | 槽执行线程 | 槽执行时机 | 是否阻塞 | 用途 |
|---|---|---|---|---|
AutoConnection | 由信号/槽所在线程决定 | 立即或稍后 | 视情况 | 默认值 |
DirectConnection | 信号的线程 | 立即同步执行 | 是 | 同线程通信 |
QueuedConnection | 槽对象的线程 | 下一轮事件循环 | 否 | 跨线程通信 |
BlockingQueuedConnection | 槽对象的线程 | 等待执行完成 | 是(发送线程) | 需要返回结果 |
SingleShotConnection | 同上 | 同上 | 同上 | 连接一次后自动断开 |
AutoConnection 的决策逻辑:
if (sender->thread() == receiver->thread())
type = DirectConnection; // 同线程 → 直接调用
else
type = QueuedConnection; // 不同线程 → 队列调用
# 7.4.2 跨线程通信原理
这是 Qt 信号槽最精巧的设计:
发送线程: 接收线程:
emit signal(42)
↓ (AutoConnection 检测到线程不同)
QCoreApplication::postEvent(
receiver,
new QMetaCallEvent(...)
) ↓ 事件循环
↓ QEventLoop::processEvents()
信号发射完成(不阻塞) ↓
dispatch QMetaCallEvent
↓
receiver->qt_metacall()
↓
onSlot(42) ← 在接收线程执行
关键:QueuedConnection 要求接收者的线程有事件循环(exec())。如果工作线程只是 run() 重写而没有启动事件循环 → QueuedConnection 的槽永远不会执行。
# 7.5 性能与陷阱
# 信号槽调用的开销
在 ARM Cortex-A53 上实测(100 万次调用):
| 方式 | 耗时 | 说明 |
|---|---|---|
| 直接函数调用 | 2ms | 基线 |
DirectConnection 信号槽 | 18ms | ~9ns/次,主要是 activate 遍历开销 |
QueuedConnection 信号槽 | 620ms | 涉及事件分配、入队、出队 |
| 虚函数调用 | 5ms | 作为对比 |
结论:DirectConnection 信号槽比直接调用慢约 9 倍,但在大多数场景可忽略。QueuedConnection 开销显著——高频调用避免用它。
# 常见陷阱
// ❌ 陷阱 1:信号和槽的参数类型不匹配——编译期不报错(旧式语法)
connect(sender, SIGNAL(valueChanged(int)), receiver, SLOT(onValueChanged(QString)));
// 运行时连接失败,返回 false,但没有编译错误!
// ❌ 陷阱 2:信号和槽参数类型不完全匹配
connect(sender, SIGNAL(sig(double)), receiver, SLOT(slot(int)));
// int 和 double 在 moc 看来是不同的签名 → 连接失败
// ❌ 陷阱 3:Lambda 捕获 this,但 receiver 是另一个对象
connect(sender, &Sender::sig, otherObj, [this]() {
m_data++; // this 可能先于 otherObj 被删除!
});
// ✅ 正确:receiver 应和 Lambda 中引用的对象一致
connect(sender, &Sender::sig, this, [this]() {
m_data++; // this 被删除时连接自动断开
});
// ❌ 陷阱 4:在 run() 中使用 QueuedConnection
class MyThread : public QThread {
void run() override {
// 没有事件循环!QueuedConnection 的槽永远不会执行
while (running) { doWork(); }
}
};
// ✅ 正确:要么用 exec() 启动事件循环,要么使用 DirectConnection
# 7.6 调试技巧
// 1. 打印连接状态
QMetaObject::Connection conn = connect(...);
if (!conn)
qWarning() << "Connection failed! Check signal/slot signatures";
// 2. QSignalSpy——单元测试中监控信号
#include <QSignalSpy>
QSignalSpy spy(sender, &Sender::valueChanged);
sender->emit valueChanged(42);
QCOMPARE(spy.count(), 1);
QCOMPARE(spy.takeFirst().at(0).toInt(), 42);
// 3. 运行时查看所有连接
const QMetaObject* meta = object->metaObject();
for (int i = meta->methodOffset(); i < meta->methodCount(); ++i) {
QMetaMethod method = meta->method(i);
if (method.methodType() == QMetaMethod::Signal) {
qDebug() << "Signal:" << method.name() << "index:" << i;
}
}
// 4. 启用 Qt 内部的信号槽调试输出
// 环境变量:QT_LOGGING_RULES="qt.core.signal_slot=true"
QSignalBlocker——临时阻止信号触发:
void MyWidget::updateControls() {
QSignalBlocker blocker(comboBox); // 构造时阻止 comboBox 的信号
comboBox->setCurrentIndex(5); // 不会触发 currentIndexChanged
// ... 其他批量操作
} // blocker 析构 → 恢复信号
# 7.7 综合案例
# 嵌入式传感器采集到 UI 更新的完整信号槽链路
// === SensorReader——工作线程 ===
class SensorReader : public QObject {
Q_OBJECT
public slots:
void startReading() {
m_timer = new QTimer(this);
connect(m_timer, &QTimer::timeout, this, &SensorReader::readSensor);
m_timer->start(100); // 100ms 一次
}
void stopReading() { m_timer->stop(); }
signals:
void sensorDataReady(float temp, float humidity, int pressure);
private:
void readSensor() {
// 模拟读传感器
float temp = readTemperature();
float hum = readHumidity();
int press = readPressure();
emit sensorDataReady(temp, hum, press);
}
QTimer* m_timer;
};
// === DataProcessor——工作线程(数据处理)===
class DataProcessor : public QObject {
Q_OBJECT
public slots:
void onRawData(float temp, float hum, int press) {
// 数据滤波
float filteredTemp = filter(temp);
emit processedData(filteredTemp, hum, press);
}
signals:
void processedData(float temp, float humidity, int pressure);
private:
float filter(float v) { return m_history.appendAndAverage(v); }
};
// === 主线程——Dashboard ===
class Dashboard : public QMainWindow {
Q_OBJECT
public:
Dashboard() {
// 线程 1:传感器读取
m_sensorThread = new QThread(this);
m_sensor.setupUi(this);
m_sensor->moveToThread(m_sensorThread);
connect(m_sensorThread, &QThread::started, m_sensor, &SensorReader::startReading);
m_sensorThread->start();
// 线程 2:数据处理
m_processThread = new QThread(this);
m_processor->moveToThread(m_processThread);
m_processThread->start();
// 连接链路(跨 3 个线程)
// 工作线程 1 → 工作线程 2:数据预处理
connect(m_sensor, &SensorReader::sensorDataReady,
m_processor, &DataProcessor::onRawData,
Qt::QueuedConnection);
// 工作线程 2 → 主线程:更新 UI
connect(m_processor, &DataProcessor::processedData,
this, &Dashboard::updateUI,
Qt::QueuedConnection);
// 主线程 → 工作线程 1:控制命令
connect(this, &Dashboard::stopRequested,
m_sensor, &SensorReader::stopReading,
Qt::QueuedConnection);
}
public slots:
void updateUI(float temp, float humidity, int pressure) {
// 在主线程更新 UI——线程安全
m_tempLabel->setText(QString::number(temp, 'f', 1) + "°C");
m_humLabel->setText(QString::number(humidity, 'f', 1) + "%");
m_presLabel->setText(QString::number(pressure) + " hPa");
}
signals:
void stopRequested();
private:
SensorReader* m_sensor;
DataProcessor* m_processor;
QThread* m_sensorThread;
QThread* m_processThread;
};
# 7.8 速查表
| 概念 | 一句话解释 |
|---|---|
emit signal(value) | 触发信号 → 内部调用 QMetaObject::activate() |
connect(sender, sig, receiver, slot, type) | 建立信号-槽连接 |
DirectConnection | 槽在发射线程同步执行 |
QueuedConnection | 槽在接收线程的事件循环中异步执行 |
AutoConnection | 同线程 → Direct;不同线程 → Queued |
BlockingQueuedConnection | Queued 但发送线程阻塞直到槽执行完 |
SingleShotConnection | 连接触发一次后自动断开 |
QSignalBlocker | RAII 临时阻止信号发射 |
QSignalSpy | 单元测试中监控信号发射 |
disconnect(conn) | 断开特定连接 |
sender() | 槽函数中获取信号发射者(不推荐) |
信号槽铁律:
- 始终用新式成员指针语法——编译期类型检查
- Lambda 必须绑定正确的 receiver,避免野指针
- 跨线程用
Qt::QueuedConnection,且确保接收线程有事件循环 - 高频信号(>100Hz)避免
QueuedConnection——用DirectConnection或批处理 BlockingQueuedConnection的两端不能在同一线程,否则死锁- 槽函数中避免
sender()——破坏了松耦合
# 7.9 断开连接与生命周期
# 7.9.1 多种断开方式
// 方式 1:断开特定信号-槽对
disconnect(sender, &Sender::sig, receiver, &Receiver::slot);
// 方式 2:断开某个信号的所有槽
disconnect(sender, &Sender::sig, nullptr, nullptr);
// 方式 3:断开某个对象的所有信号(作为发送者)
disconnect(sender, nullptr, nullptr, nullptr);
// 方式 4:断开某个对象的所有槽(作为接收者)
disconnect(nullptr, nullptr, receiver, nullptr);
// 方式 5:通过 QMetaObject::Connection
QMetaObject::Connection conn = connect(...);
disconnect(conn);
# 7.9.2 自动断开机制
Qt 的连接在以下情况会自动断开:
- sender 被删除 → 所有以它为发送者的连接自动断开
- receiver 被删除 → 所有以它为接收者的连接自动断开
SingleShotConnection→ 连接触发一次后自动断开
// ✅ 利用自动断开——安全模式
void setupConnection() {
QPushButton* btn = new QPushButton("Click", this);
QLabel* label = new QLabel("Status", this);
// 当 btn 或 label 被删除时,连接自动断开
connect(btn, &QPushButton::clicked, label, [label]() {
label->setText("Clicked");
});
// 不需要手动 disconnect!
}
# 7.9.3 Lambda 与 this 生命周期
// ❌ 危险:QTimer 的生命周期比 this 长
class MyWidget : public QWidget {
void startTimer() {
QTimer* timer = new QTimer(this); // 子对象,随 this 析构
connect(timer, &QTimer::timeout, [this]() {
update(); // this 析构后 → 访问野指针!
});
timer->start(1000);
}
};
// ✅ 安全:使用 receiver 参数
connect(timer, &QTimer::timeout, this, [this]() {
update(); // this 被删除 → 连接断开 → Lambda 不再调用
});
// ✅ 安全:Lambda 中检查 QPointer
QPointer<MyWidget> self(this);
connect(timer, &QTimer::timeout, [self]() {
if (self) { self->update(); }
});
# 7.10 QSignalMapper 与 QSignalTransition
# 7.10.1 QSignalMapper(Qt 5 遗留)
多个信号映射到相同参数:
QSignalMapper* mapper = new QSignalMapper(this);
for (int i = 0; i < 10; ++i) {
QPushButton* btn = new QPushButton(QString::number(i), this);
connect(btn, &QPushButton::clicked, mapper, qOverload<>(&QSignalMapper::map));
mapper->setMapping(btn, i); // 映射 btn → i
}
connect(mapper, &QSignalMapper::mappedInt, [](int value) {
qDebug() << "Button" << value << "clicked";
});
# 7.10.2 QSignalTransition(状态机)
#include <QStateMachine>
#include <QSignalTransition>
QStateMachine machine;
QState* idle = new QState;
QState* running = new QState;
// 当收到 startSignal 时从 idle → running
QSignalTransition* trans = new QSignalTransition(
sender, &Sender::startSignal);
trans->setTargetState(running);
idle->addTransition(trans);
// 更简洁的写法(Qt 6)
idle->addTransition(sender, &Sender::startSignal, running);
# 7.11 训练题
# 题目 1:分析崩溃原因
class Worker : public QObject {
Q_OBJECT
public slots:
void doWork() {
QThread::sleep(10); // 模拟耗时操作
emit finished();
}
signals:
void finished();
};
int main() {
Worker worker;
worker.doWork();
// 问题:这里阻塞了哪个线程?事件循环能处理吗?
}
答案:doWork() 直接调用(不是通过信号触发),在主线程执行 → 阻塞主线程 IO 秒 → UI 冻结。正确做法:将 Worker 移到 QThread,通过信号触发执行。
# 题目 2:Lambda 野指针
void MyWidget::startTimer() {
auto timer = new QTimer;
connect(timer, &QTimer::timeout, [this]() {
m_data->refresh(); // 当 this 被 delete 后?
});
timer->start(1000);
}
// 问题:何时可能导致崩溃?如何修复?
答案:MyWidget 析构时 this 变成野指针,但 timer 仍在运行 → Lambda 访问 m_data 崩溃。修复:connect(timer, &QTimer::timeout, this, [this]{...})——当 this 删除时连接自动断开。
# 题目 3:跨线程连接
// 三个对象分别属于三个线程:
// sender → 线程 A
// filter → 线程 B
// receiver → 线程 C
// 以下连接是否正确?
connect(sender, &Sender::rawData,
filter, &Filter::onRawData,
Qt::BlockingQueuedConnection);
connect(filter, &Filter::filtered,
receiver, &Receiver::onFiltered,
Qt::AutoConnection);
答案:第一个连接正确(跨线程,发送线程阻塞等 filter 执行完);第二个连接也正确(AutoConnection 检测到跨线程 → 自动切换 QueuedConnection)。但注意:线程 B 和 C 都必须有事件循环。