信号声明某个对象发生了可供外部观察的变化;连接决定哪些函数接收这次通知。变化可以来自用户输入、定时器、数据计算或网络处理,并不只在点击界面时发生。本篇以 Qt 6 / C++17 实现一个完整的“发送者 → 转发者 → 接收者”例子。
信号、槽与 emit 分别负责什么
跳转到“信号、槽与 emit 分别负责什么”signals: 中声明信号,信号函数的实现由 moc 生成,通常不手写函数体。emit changed(value) 发出通知;emit 本身不决定线程切换,也不把数据自动发送给任意对象,只有建立了适当连接的接收者才会处理它。
槽是可接收信号的函数。使用成员函数指针形式的 connect 时,接收目标可以是普通成员函数或 lambda,不必声明为 slots:。声明在 slots: 下的函数还可以通过元对象系统调用;直接按普通 C++ 方式调用槽时,仍服从 C++ 访问权限与调用规则。Qt 信号与槽
| 环节 | 示例 | 职责 |
|---|---|---|
| 声明 | void valueChanged(int value); | 定义通知的名称和参数 |
| 建立连接 | connect(sender, &Sender::valueChanged, receiver, &Receiver::setValue); | 将具体对象的通知关联到处理函数 |
| 发出 | emit valueChanged(value); | 将本次参数交给连接机制 |
| 接收 | void setValue(int value); | 处理数据,不要求反向知道发送者实现 |
一个信号可以连接多个目标,一个目标也可以接收多个来源。接收函数可以比信号少接收一些尾部参数,但接收的参数必须满足该连接形式的类型兼容规则,不能任意拼接不匹配的签名。
完整例子:信号直接连接另一个信号
跳转到“完整例子:信号直接连接另一个信号”信号可以作为连接的接收目标。当 Source::valueChanged 发出时,Relay::valueChanged 会继续通知它自己的接收者。它适合封装内部组件:外部只关心转发层公开的接口。
将下面保存为 main.cpp:
#include <QCoreApplication>#include <QObject>#include <cassert>
class Source : public QObject { Q_OBJECTpublic: void setValue(int value) { if (value_ == value) return; value_ = value; emit valueChanged(value_); }signals: void valueChanged(int value);private: int value_ = 0;};
class Relay : public QObject { Q_OBJECTsignals: void valueChanged(int value);};
class Receiver : public QObject {public: void accept(int value) { last = value; ++calls; } int last = 0; int calls = 0;};
int main(int argc, char** argv) { QCoreApplication app(argc, argv); Source source; Relay relay; Receiver receiver;
const auto forward = QObject::connect( &source, &Source::valueChanged, &relay, &Relay::valueChanged); const auto handle = QObject::connect( &relay, &Relay::valueChanged, &receiver, &Receiver::accept); assert(forward && handle);
source.setValue(7); assert(receiver.last == 7 && receiver.calls == 1); source.setValue(7); // 状态未改变,不重复通知 assert(receiver.calls == 1); QObject::disconnect(forward); source.setValue(8); assert(receiver.last == 7 && receiver.calls == 1);}
#include "main.moc"对应 CMakeLists.txt:
cmake_minimum_required(VERSION 3.20)project(SignalRelay LANGUAGES CXX)set(CMAKE_CXX_STANDARD 17)set(CMAKE_CXX_STANDARD_REQUIRED ON)set(CMAKE_AUTOMOC ON)find_package(Qt6 REQUIRED COMPONENTS Core)add_executable(signal-relay main.cpp)target_link_libraries(signal-relay PRIVATE Qt6::Core)本例对象都在同一线程,使用默认的自动连接,通知同步执行,所以无需启动事件循环即可断言结果。Receiver 没有声明新的元对象成员,使用普通成员函数接收;声明自定义信号的两个类均有 Q_OBJECT。本例在 Qt 6.10.2 实际编译并运行,断言通过。
参数、寿命与队列连接
跳转到“参数、寿命与队列连接”如果连接使用队列投递,参数需要可按 Qt 元类型机制保存和传递,接收线程也需要可工作的事件循环。自定义类型可能需要 Q_DECLARE_METATYPE 和 qRegisterMetaType;不能把“C++ 函数允许这个参数”直接等同于“它能安全排队传递”。
指针参数即使能被投递,复制的通常也只是指针值,并不自动转移或延长所指对象寿命。lambda 捕获引用时,也必须确保捕获对象在调用期间仍然有效。带接收对象上下文的连接有利于管理断连和线程归属;详细例子见 连接方式、线程与生命周期。
避免形成无条件互相转发的信号环路,或在 setter 中无条件重复发出相同状态;直接连接可能立即递归。例子中的“值未改变就返回”是状态通知的基本保护,但不能代替对整个依赖图的设计。
这里整合了 信号可直接连接信号的原短笔记,并以可运行的转发和断连行为验证该结论。