Qt中的字符串宏 | 编译期检查和运行期检查 | Qt信号与槽connect写法

一、什么是字符串宏(String Macro)

字符串宏其实来源于 C/C++ 的预处理器宏(Macro)

宏是什么?

在 C++ 代码正式编译之前,编译器会先经过一个预处理阶段, 在这个阶段,所有的 #define 宏定义都会被纯文本替换

例如:

cpp 复制代码
#define PI 3.14159

当你写:

cpp 复制代码
double area = PI * r * r;

预处理器会在编译前把它替换成:

cpp 复制代码
double area = 3.14159 * r * r;

这就是宏展开

那么什么是"字符串宏"?

字符串宏就是返回字符串的宏, 也就是说,它展开后不是数字或表达式,而是 字符串字面量(string literal)

例如:

cpp 复制代码
#define NAME "Landon"

当代码中出现:

cpp 复制代码
std::cout << NAME;

编译器看到的其实是:

cpp 复制代码
std::cout << "Landon";

Qt 中的 SIGNAL() / SLOT() 宏就是字符串宏

在 Qt4 以及早期 Qt5 中,信号和槽连接常写成这样:

cpp 复制代码
connect(spinBox, SIGNAL(valueChanged(int)), label, SLOT(setNum(int)));

表面上看像是函数调用,但其实:

  • SIGNAL()SLOT() 是两个宏;

  • 它们把括号内的内容变成 字符串常量

展开后是这样(伪代码):

cpp 复制代码
connect(spinBox, "2valueChanged(int)", label, "1setNum(int)");

"2""1" 是内部用于区分信号和槽的标识,Qt 的元对象系统(Meta-Object System)会在运行时根据这些字符串去查找信号和槽是否存在

关键点:

SIGNAL 和 SLOT 写法依赖字符串匹配,编译器并不会验证信号或槽的存在

举个例子

cpp 复制代码
connect(spinBox, SIGNAL(valueChaged(int)), label, SLOT(setNum(int)));

你不小心少写了一个 n(changed被你不小心写为chaged),程序依然能编译通过

但运行时会出现:

复制代码
QObject::connect: No such signal QSpinBox::valueChaged(int)

这就是字符串宏的弊端:

  • 编译器无法检查;

  • 错误要到运行时才发现。

Qt5+ 改进:函数指针写法取代字符串宏

Qt5 之后,我们可以写:

cpp 复制代码
connect(spinBox, &QSpinBox::valueChanged, label, &QLabel::setNum);

这种写法不再使用字符串,而是**真正的函数指针,**编译器能在编译期就检查函数是否存在、参数是否匹配,彻底杜绝拼写错误

二、编译期检查与运行期检查

在 C++(以及 Qt)编程中,你可能常听到这两个概念:

"这种写法只能运行期报错,而那种写法能在编译期就检查出来"

那么,究竟什么是 编译期检查(Compile-time Check)运行期检查(Runtime Check) 呢?它们的区别到底是什么?为什么 Qt 推荐我们使用新式connect语法?

这篇文章帮你彻底搞清楚

程序的两个阶段

当我们写一个 C++ 程序,它要经过两个重要阶段:

编译期(Compile-time)------ 程序还没运行,编译器正在"检查并生成机器代码"的阶段

运行期(Runtime)------ 程序已经被加载到内存中,正在实际执行的阶段

编译期检查(Compile-time Checking)

定义:

编译器在"编译阶段"发现问题,并阻止程序生成可执行文件

特点:

  • 错误在编译时就会报出

  • 不需要运行程序

  • 一旦出错,编译器立即停止编译

  • 错误更容易定位,更安全

举个例子:

cpp 复制代码
int x = "hello";   // ❌ 类型不匹配

编译器会直接报错:

cpp 复制代码
error: invalid conversion from 'const char*' to 'int'

这就是 编译期检查

在 Qt 中的例子:

旧版写法(Qt4 风格):

cpp 复制代码
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)

程序在运行过程中,才检测并发现错误

特点:

  • 编译器不会报错

  • 程序能编译通过

  • 错误只在程序执行时才暴露

  • 有时会导致程序崩溃或逻辑错误

  • 调试成本高

举个例子:

cpp 复制代码
int arr[3] = {1, 2, 3};
arr[5] = 10;   // ❌ 越界访问

编译器无法发现这个错误,因为它不知道你运行时会访问哪个索引

只有在运行时才会出错(甚至直接崩溃)

Qt 示例

新式写法(Qt5+ 推荐):

cpp 复制代码
connect(spinBox, 
        QOverload<int>::of(&QSpinBox::valueChanged), 
        label, 
        &QLabel::setNum);

这里使用了 函数指针 + 模板机制 ,编译器能在 编译期 就检查:

  • 这个信号是否存在;

  • 参数类型是否匹配;

  • 槽函数是否可调用。

如果你写错类型:

cpp 复制代码
connect(spinBox,
        QOverload<QString>::of(&QSpinBox::valueChanged),
        label,
        &QLabel::setNum);

编译器立即报错:

复制代码
error: no matching function for call to 'connect'

这就是 编译期检查

三、Qt信号与槽connect写法

Qt 的 connect() 是信号与槽机制的核心函数,从 Qt4 → Qt5 → Qt6connect() 的写法经历了三次重大进化

Qt 的信号槽机制简介

connect() 的作用是:

把一个 信号(signal) 连接到一个 槽(slot)可调用对象(callable)

当信号发出时,槽函数自动被调用

语法通用结构如下:

cpp 复制代码
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 风格)

cpp 复制代码
connect(spinBox, SIGNAL(valueChanged(int)), label, SLOT(setNum(int)));

特点:

  • 使用宏 SIGNAL()SLOT()

  • 宏会把参数变为字符串(如 "2valueChanged(int)"

  • Qt 运行时再用反射机制(MOC 系统)去匹配信号槽

缺点:

  • 运行期检查,拼写错误不会被编译器发现

  • 不支持信号重载

  • Qt5 中仍可用,但已被官方标记为不推荐

② 函数指针写法(Qt5 推荐)

Qt5 引入了 编译期类型检查的函数指针 connect

cpp 复制代码
connect(spinBox, &QSpinBox::valueChanged, label, &QLabel::setNum);

优点:

  • 编译期类型安全

  • 支持重载信号

  • 自动匹配函数签名

  • 拼写错误会编译不通过

  • 支持自动断开(随对象析构自动 disconnect)

原理:

Qt 利用 C++11 函数指针机制,让编译器能在编译时检查信号与槽参数类型是否兼容

③ 重载信号写法(QOverload)

某些 Qt 类的信号是 overload(重载) 的,比如 QSpinBox::valueChanged

cpp 复制代码
void valueChanged(int value);
void valueChanged(const QString &text);

这时候你必须告诉编译器你想连接哪个版本。

QOverload 写法:

cpp 复制代码
connect(spinBox,
        QOverload<int>::of(&QSpinBox::valueChanged),
        label,
        &QLabel::setNum);

或者:

cpp 复制代码
connect(spinBox,
        QOverload<const QString &>::of(&QSpinBox::valueChanged),
        this,
        &MyWidget::onTextChanged);

原理:

QOverload<Args...>::of(&Class::signal) 明确指定了信号参数类型

④ Lambda 槽函数写法(Qt5 新增)

Qt5 开始允许用 Lambda 表达式 作为槽函数,非常方便(如果会用的话)

cpp 复制代码
connect(spinBox, &QSpinBox::valueChanged, this, [=](int value){
    qDebug() << "SpinBox value:" << value;
});

优点:

  • 不需要定义槽函数

  • 支持捕获外部变量

  • 类型安全

  • 写法简洁

用法补充:

你可以省略 receiver,Qt 会自动推导:

cpp 复制代码
connect(spinBox, &QSpinBox::valueChanged, [=](int v){
    qDebug() << v;
});

但要注意:若无 receiver,lambda 的生命周期不受控,最好与 QObject::connect 的返回值配合使用来管理连接

⑤ Qt6 的新写法(自动推导 + 简化)

Qt6 在 Qt5 基础上进一步简化了 connect 语法,支持自动推导、可选参数:

cpp 复制代码
connect(spinBox, &QSpinBox::valueChanged, this, &MyWidget::updateLabel);

甚至:

cpp 复制代码
connect(spinBox, &QSpinBox::valueChanged, [=](int v){ qDebug() << v; });

或者返回连接对象:

cpp 复制代码
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() 断开该对象所有连接
相关推荐
xcyxiner11 小时前
DicomViewer (dcmtk读取dcm文件)5
qt
xcyxiner1 天前
DicomViewer (后台线程处理文件)4
qt
xcyxiner1 天前
DicomViewer (添加模型类)3
qt
xcyxiner2 天前
DicomViewer (目录调整) 2
qt
xcyxiner2 天前
dcmtk vtk vtk-dicom(gdcm) 编译(debug) v2
qt
clint4563 天前
C++进阶(1)——前景提要
c++
夜悊3 天前
C++代码示例:进制数简单生成工具
c++
郝学胜_神的一滴3 天前
CMake 021: IF 条件判据详诠
c++·cmake
_wyt0014 天前
洛谷 B3930 [GESP202312 五级] 烹饪问题 题解
c++·gesp
LDR0064 天前
Type-C 快充全面升级!LDR6601 赋能个人护理便携电机,重塑剃须刀 / 理发器新体验
c语言·开发语言