Qt中的字符串宏 | 编译期检查和运行期检查 | Qt信号与槽connect写法
一、什么是字符串宏(String Macro)
字符串宏其实来源于 C/C++ 的预处理器宏(Macro)
宏是什么?
在 C++ 代码正式编译之前,编译器会先经过一个预处理阶段,在这个阶段,所有的 #define
宏定义都会被纯文本替换
例如:
#define PI 3.14159
当你写:
double area = PI * r * r;
预处理器会在编译前把它替换成:
double area = 3.14159 * r * r;
这就是宏展开
那么什么是“字符串宏”?
字符串宏就是返回字符串的宏,也就是说,它展开后不是数字或表达式,而是 字符串字面量(string literal)
例如:
#define NAME "Landon"
当代码中出现:
std::cout << NAME;
编译器看到的其实是:
std::cout << "Landon";
Qt 中的 SIGNAL() / SLOT() 宏就是字符串宏
在 Qt4 以及早期 Qt5 中,信号和槽连接常写成这样:
connect(spinBox, SIGNAL(valueChanged(int)), label, SLOT(setNum(int)));
表面上看像是函数调用,但其实:
SIGNAL()
和SLOT()
是两个宏;它们把括号内的内容变成 字符串常量
展开后是这样(伪代码):
connect(spinBox, "2valueChanged(int)", label, "1setNum(int)");
"2"
和 "1"
是内部用于区分信号和槽的标识,Qt 的元对象系统(Meta-Object System)会在运行时根据这些字符串去查找信号和槽是否存在
关键点:
SIGNAL 和 SLOT 写法依赖字符串匹配,编译器并不会验证信号或槽的存在
举个例子
connect(spinBox, SIGNAL(valueChaged(int)), label, SLOT(setNum(int)));
你不小心少写了一个 n(changed被你不小心写为chaged),
程序依然能编译通过
但运行时会出现:
QObject::connect: No such signal QSpinBox::valueChaged(int)
这就是字符串宏的弊端:
编译器无法检查;
错误要到运行时才发现。
Qt5+ 改进:函数指针写法取代字符串宏
Qt5 之后,我们可以写:
connect(spinBox, &QSpinBox::valueChanged, label, &QLabel::setNum);
这种写法不再使用字符串,而是真正的函数指针,编译器能在编译期就检查函数是否存在、参数是否匹配,彻底杜绝拼写错误
二、编译期检查与运行期检查
在 C++(以及 Qt)编程中,你可能常听到这两个概念:
“这种写法只能运行期报错,而那种写法能在编译期就检查出来”
那么,究竟什么是 编译期检查(Compile-time Check) 和 运行期检查(Runtime Check) 呢?它们的区别到底是什么?为什么 Qt 推荐我们使用新式connect语法?
这篇文章帮你彻底搞清楚
程序的两个阶段
当我们写一个 C++ 程序,它要经过两个重要阶段:
编译期(Compile-time)—— 程序还没运行,编译器正在“检查并生成机器代码”的阶段
运行期(Runtime)—— 程序已经被加载到内存中,正在实际执行的阶段
编译期检查(Compile-time Checking)
定义:
编译器在“编译阶段”发现问题,并阻止程序生成可执行文件
特点:
错误在编译时就会报出
不需要运行程序
一旦出错,编译器立即停止编译
错误更容易定位,更安全
举个例子:
int x = "hello"; // ❌ 类型不匹配
编译器会直接报错:
error: invalid conversion from 'const char*' to 'int'
这就是 编译期检查
在 Qt 中的例子:
旧版写法(Qt4 风格):
connect(spinBox, SIGNAL(valueChanged(int)), label, SLOT(setNum(int)));
这行代码中的 SIGNAL()
和 SLOT()
是字符串宏,编译器根本不知道 valueChanged(int)
和 setNum(int)
具体是什么函数!
如果你写成:
connect(spinBox, SIGNAL(valueChanged(QString)), label, SLOT(setNum(int)));
编译器也会 默默通过,但程序运行时会在控制台输出警告:
QObject::connect: No such signal QSpinBox::valueChanged(QString)
这类错误只能 运行时 才被发现,属于运行期检查
运行期检查(Runtime Checking)
程序在运行过程中,才检测并发现错误
特点:
编译器不会报错
程序能编译通过
错误只在程序执行时才暴露
有时会导致程序崩溃或逻辑错误
调试成本高
举个例子:
int arr[3] = {1, 2, 3};
arr[5] = 10; // ❌ 越界访问
编译器无法发现这个错误,因为它不知道你运行时会访问哪个索引
只有在运行时才会出错(甚至直接崩溃)
Qt 示例
新式写法(Qt5+ 推荐):
connect(spinBox, QOverload<int>::of(&QSpinBox::valueChanged), label, &QLabel::setNum);
这里使用了 函数指针 + 模板机制,编译器能在 编译期 就检查:
这个信号是否存在;
参数类型是否匹配;
槽函数是否可调用。
如果你写错类型:
connect(spinBox,QOverload<QString>::of(&QSpinBox::valueChanged),label,&QLabel::setNum);
编译器立即报错:
error: no matching function for call to 'connect'
这就是 编译期检查
三、Qt信号与槽connect写法
Qt 的 connect()
是信号与槽机制的核心函数,从 Qt4 → Qt5 → Qt6,connect()
的写法经历了三次重大进化
Qt 的信号槽机制简介
connect()
的作用是:
把一个 信号(signal) 连接到一个 槽(slot) 或 可调用对象(callable),
当信号发出时,槽函数自动被调用
语法通用结构如下:
connect(sender, signal, receiver, slot);
sender
:发出信号的对象指针signal
:信号函数receiver
:接收信号的对象指针slot
:接收信号后执行的函数(槽)
Qt 各版本 connect 写法概览
Qt 版本 | 写法类型 | 是否类型安全 | 是否支持重载信号 | 示例 |
---|---|---|---|---|
Qt4(老式) | 宏字符串写法 | ❌ 否(运行期检查) | ❌ 否 | connect(obj, SIGNAL(sig(int)), obj2, SLOT(slot(int))); |
Qt5(推荐) | 函数指针写法 | ✅ 是(编译期检查) | ✅ 是 | connect(obj, &Class::signal, obj2, &Class2::slot); |
Qt5(复杂信号) | QOverload 辅助写法 | ✅ 是 | ✅ 是 | connect(obj, QOverload<int>::of(&Class::signal), obj2, &Class2::slot); |
Qt5(Lambda) | Lambda 槽函数 | ✅ 是 | ✅ 是 | connect(obj, &Class::signal, this, [=](int v){ ... }); |
Qt6(简化) | 新式 connect(自动推导) | ✅ 是 | ✅ 是 | connect(obj, &Class::signal, this, &MyClass::slot); 或省略 receiver |
详细讲解每种写法
① 宏字符串写法(Qt4 风格)
connect(spinBox, SIGNAL(valueChanged(int)), label, SLOT(setNum(int)));
特点:
使用宏
SIGNAL()
和SLOT()
宏会把参数变为字符串(如
"2valueChanged(int)"
)Qt 运行时再用反射机制(MOC 系统)去匹配信号槽
缺点:
运行期检查,拼写错误不会被编译器发现
不支持信号重载
Qt5 中仍可用,但已被官方标记为不推荐
② 函数指针写法(Qt5 推荐)
Qt5 引入了 编译期类型检查的函数指针 connect
connect(spinBox, &QSpinBox::valueChanged, label, &QLabel::setNum);
优点:
编译期类型安全
支持重载信号
自动匹配函数签名
拼写错误会编译不通过
支持自动断开(随对象析构自动 disconnect)
原理:
Qt 利用 C++11 函数指针机制,让编译器能在编译时检查信号与槽参数类型是否兼容
③ 重载信号写法(QOverload)
某些 Qt 类的信号是 overload(重载) 的,比如 QSpinBox::valueChanged
:
void valueChanged(int value);
void valueChanged(const QString &text);
这时候你必须告诉编译器你想连接哪个版本。
QOverload 写法:
connect(spinBox,QOverload<int>::of(&QSpinBox::valueChanged),label,&QLabel::setNum);
或者:
connect(spinBox,QOverload<const QString &>::of(&QSpinBox::valueChanged),this,&MyWidget::onTextChanged);
原理:
QOverload<Args...>::of(&Class::signal)
明确指定了信号参数类型
④ Lambda 槽函数写法(Qt5 新增)
Qt5 开始允许用 Lambda 表达式 作为槽函数,非常方便(如果会用的话)
connect(spinBox, &QSpinBox::valueChanged, this, [=](int value){qDebug() << "SpinBox value:" << value;
});
优点:
不需要定义槽函数
支持捕获外部变量
类型安全
写法简洁
用法补充:
你可以省略 receiver
,Qt 会自动推导:
connect(spinBox, &QSpinBox::valueChanged, [=](int v){qDebug() << v;
});
但要注意:若无 receiver,lambda 的生命周期不受控,最好与
QObject::connect
的返回值配合使用来管理连接
⑤ Qt6 的新写法(自动推导 + 简化)
Qt6 在 Qt5 基础上进一步简化了 connect 语法,支持自动推导、可选参数:
connect(spinBox, &QSpinBox::valueChanged, this, &MyWidget::updateLabel);
甚至:
connect(spinBox, &QSpinBox::valueChanged, [=](int v){ qDebug() << v; });
或者返回连接对象:
auto connection = connect(button, &QPushButton::clicked, this, &MyClass::onClick);
disconnect(connection); // 随时解除连接
disconnect() 写法也有几种
写法 | 用法说明 |
---|---|
disconnect(sender, signal, receiver, slot) | 断开指定信号槽连接 |
disconnect(connection) | Qt5 起支持,通过保存 connect 返回值断开 |
disconnect(sender, signal) | 断开该信号的所有槽 |
disconnect() | 断开该对象所有连接 |