Qt 的信号槽机制(Signals & Slots)是其核心特性之一。它提供了一种松耦合的事件通信方式,极大降低了代码之间的耦合度,同时让我们的代码结构更清晰、可维护性更高。很多初学者只知道 “写个 signals:
slots:
”,再加上 connect()
就能实现事件响应,却不了解这背后是如何运作的。本文将为你揭开信号槽的“神秘面纱”。
signals:
区域,函数声明类似于 void somethingHappened(int value);
public slots:
(或 private slots:
)区块,也可以是任意的普通成员函数(在现代 Qt 中允许任意可调用对象作为槽)信号和槽之间需要通过 connect()
关联。例如:
connect(
sender,
&SenderClass::valueChanged,
receiver,
&ReceiverClass::onValueChanged
);
当 sender->valueChanged(...)
这个信号被发射时,onValueChanged()
槽就会被自动调用。
提示:
- 早期的 Qt 4 风格写法:
SIGNAL(valueChanged(int)), SLOT(onValueChanged(int))
;- 从 Qt 5 开始,推荐使用函数指针的新语法,更安全也更易被编译器检查。
C++ 本身并没有反射机制,也无法原生识别“信号”或“槽”这样的概念。为了给 Qt 提供元对象(Meta Object)支持,官方开发了一套预处理器,也就是 MOC。它在编译前会扫描源码,找到所有 Q_OBJECT
、signals:
、slots:
等关键字,然后生成额外的 C++ 文件(通常命名为 moc_
),把这些额外的代码再与用户代码一同编译、链接。
简单来说,MOC 就是负责把 “Qt 信号槽的特殊语法” 翻译成 “C++ 能理解的普通函数和静态元数据” 的桥梁。
假设你有一个类 MyWidget
,包含如下定义:
class MyWidget : public QWidget
{
Q_OBJECT
public:
explicit MyWidget(QWidget *parent = nullptr);
signals:
void valueChanged(int newValue);
public slots:
void onValueChanged(int value);
private:
int m_value;
};
编译流程如下:
Q_OBJECT
宏,知道这需要调用 MOC 处理moc_mywidget.cpp
文件
qt_metacast
、qt_metacall
等函数valueChanged
的实现(其实是一个普通的成员函数,会调用 QMetaObject::activate()
来通知槽)moc_mywidget.cpp
和你的 mywidget.cpp
、main.cpp
一起交给 C++ 编译器进行编译并链接connect()
、emit
等函数运作起来Q_OBJECT
宏的意义Q_OBJECT
宏表明 “此类使用 Qt 的元对象系统,需要 MOC 进行处理”Q_OBJECT
,则 MOC 不会对其生成元对象信息,导致无法使用信号槽机制,也无法使用一些 qobject_cast
、metaObject()
等高级特性当我们在 C++ 代码里写 emit valueChanged(10)
时,实际等同于:
// 省略了 emit 宏,最终展开类似于:
this->valueChanged(10);
信号函数 MyWidget::valueChanged(int)
的真实实现在 moc_mywidget.cpp
里,里面通常是调用:
QMetaObject::activate(this, &MyWidget::staticMetaObject, signal_index, argv);
QMetaObject::activate()
会做以下事情:
每个槽在连接时,Qt 都会将槽的元信息存储在连接表里。当信号被发射时,Qt 会根据连接索引去调用特定的槽。如果是同一个信号连接了多个槽,它会依次调用所有槽函数。
注意:
- 默认情况下,信号发射和槽函数调用在同一个线程里同步进行。
- 若使用了跨线程连接(
Qt::QueuedConnection
或Qt::AutoConnection
并跨线程),则信号会把调用请求放入目标线程的事件队列,等到那个线程的事件循环空闲时,再异步调用槽函数。
connect(sender, SIGNAL(valueChanged(int)), receiver, SLOT(onValueChanged(int)));
valueChanged(int)
→ “valueChanged(int)”connect(sender, &SenderClass::valueChanged, receiver, &ReceiverClass::onValueChanged);
class Counter : public QObject
{
Q_OBJECT
public:
Counter() { m_value = 0; }
int value() const { return m_value; }
signals:
void valueChanged(int newValue);
public slots:
void setValue(int newValue)
{
if (m_value == newValue)
return;
m_value = newValue;
emit valueChanged(m_value);
}
private:
int m_value;
};
然后在其他地方连接:
Counter a, b;
QObject::connect(&a, &Counter::valueChanged, &b, &Counter::setValue);
a.setValue(10);
// b 的值也会变成 10,因为 b 的 setValue 槽在 a 的 valueChanged 信号触发后被调用
从 Qt 5.2 开始,允许把Lambda 表达式当作槽,极大简化了代码:
QObject::connect(&a, &Counter::valueChanged, [&](int newValue){
qDebug() << "The new value is" << newValue;
});
忘记 Q_OBJECT
vtable for ...
”之类错误Q_OBJECT
,并确保工程能正确调用 MOC信号或槽函数签名不匹配
valueChanged()
”拼写导致连接失败多线程下的连接类型
Qt::AutoConnection
,即跨线程会排队执行,单线程同步执行Qt::QueuedConnection
性能问题
信号槽机制,连同元对象系统与MOC,共同造就了 Qt 的强大与灵活。了解这些原理后,就能更好地编写、调试和优化 Qt 程序。
参考
- Qt 官方文档: Signals & Slots
- Qt 官方文档: Meta-Object System