前面学了纯 C++ 普通类,现在学习 Qt 的类。 记住一句话:Qt 类本质就是 C++ 的类,只是 Qt 框架扩展了一套【QObject 对象模型】
本例选用 Qt 最基础的自定义 QObject 子类,只用到信号槽,不涉及 UI 界面(Qt 控制台项目,更容易理解)
cpp
// myphone.h 头文件
#ifndef MYPHONE_H
#define MYPHONE_H
// 引入QObject头文件,QObject是Qt所有对象的根类
#include <QObject>
// ========== 类定义开始 ==========
class MyPhone : public QObject
{
// Q_OBJECT宏,Qt独有,开启元对象系统,支持信号槽
Q_OBJECT
public:
// 构造函数
explicit MyPhone(QObject *parent = nullptr);
// 普通成员函数:发射打电话信号
void startCall(QString number);
// 信号:Qt独有,只声明,不用写函数实现
signals:
void callSignal(QString num);
// 槽函数,接收信号,执行业务逻辑
public:
void onCallReceive(QString num);
};
// ========== 类定义结束 ==========
#endif // MYPHONE_H
cpp
// myphone.cpp
#include "myphone.h"
#include <QDebug>
// 构造函数实现
MyPhone::MyPhone(QObject *parent)
: QObject(parent)
{
}
void MyPhone::startCall(QString number)
{
// emit:发射信号,Qt关键字
emit callSignal(number);
}
void MyPhone::onCallReceive(QString num)
{
qDebug() << "收到来电,号码:" << num;
}
cpp
// main.cpp
#include <QCoreApplication>
#include "myphone.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
MyPhone phone1;
MyPhone phone2;
// connect 信号槽连接函数
connect(&phone1, &MyPhone::callSignal, &phone2, &MyPhone::onCallReceive);
phone1.startCall("10086");
return a.exec();
}
运行输出:
cpp
收到来电,号码:"10086"
第一部分:头文件 class MyPhone : public QObject 逐词拆解
cpp
class MyPhone : public QObject
-
class- C++ 关键字,含义:定义一个类
- 作用:告诉编译器,下面要定义一张 "图纸"。和上一节普通 C++ 类的 class 完全一样。
- 必须写,没有 class,编译器不会识别这是类。
-
MyPhone- 类名,我们自定义的名字,代表这个 Qt 类叫 MyPhone(我的手机)
- 驼峰命名,首字母大写,只是图纸名称,此时没有创建对象,不存储数据,不占对象内存
-
: public QObject:冒号:代表继承,C++ 原生语法public:公有继承QObject:Qt 提供的基类(父类图纸)- 整句含义:MyPhone 类继承自 QObject,自动拥有 QObject 自带全部能力(信号槽、对象树、元对象系统)
普通 C++ 类没有这一段;继承 QObject 是 Qt 类的标志性写法
-
{ }大括号 大括号内部存放这个类的全部内容:宏、成员函数、信号。};结尾分号不能丢。
Q_OBJECT
cpp
Q_OBJECT
Q_OBJECT是 Qt 提供的宏,不是标准 C++ 关键字- 作用:开启 Qt 元对象系统,只有写了这个,当前类才能使用信号、槽。
- 强制规则:
- 必须写在继承 QObject 的类内部;
- 写在头文件里面;
- 添加或者删除 Q_OBJECT 之后,必须执行 qmake,重新构建项目,否则编译报错虚表错误。
普通 C++ 类没有 Q_OBJECT。
explicit MyPhone(QObject *parent = nullptr); 构造函数
cpp
explicit MyPhone(QObject *parent = nullptr);
- explicit:C++ 关键字,禁止隐式类型转换。Qt 继承 QObject 的类,构造函数基本都会加 explicit。
- MyPhone :构造函数,名字和类名完全相同,没有返回值,创建对象自动调用。
- QObject *parent :形参,父对象指针 。
- parent 用来设置 Qt 对象树,父对象销毁时自动删除子对象,自动管理内存。
= nullptr:默认参数。创建对象时,如果不传 parent,parent 默认等于空指针。- nullptr:空指针,代表没有父对象。
void startCall(QString number);
void:返回值类型,函数执行完不返回任何数据startCall:成员函数名字(QString number):参数列表,调用函数时,需要传入一个 QString 字符串,存到局部变量 number 中。;:声明结束(头文件只做声明,函数体写在 cpp 文件)
signals:
- Qt 的标记,不是 C++ 原生关键字
- 含义:下面定义的全部是信号
- 信号特点:只写函数声明,不需要写 {} 函数体,moc 工具自动帮我们生成实现代码。
cpp
void callSignal(QString num);
callSignal 信号函数:可以携带参数,用来向外传递数据。
public:
和普通 C++ 一样,public 下面的函数,外部代码(main)可以调用。
cpp
void onCallReceive(QString num);
槽函数,用来接收信号,收到信号后执行里面代码,函数体在 cpp 实现。
#ifndef / #define / #endif
头文件保护宏,防止头文件被多次重复包含导致编译报错,Qt 头文件默认都带。
cpp
#ifndef MYPHONE_H
#define MYPHONE_H
// 代码
#endif // MYPHONE_H
第二部分 cpp 文件逐词讲解
cpp
MyPhone::MyPhone(QObject *parent)
: QObject(parent)
{
}
MyPhone::MyPhone::作用域解析符。读作 "的"- 含义:MyPhone 类里面的 MyPhone 构造函数
: QObject(parent)初始化列表- 子类构造函数执行前,先调用父类 QObject 的构造函数,把 parent 传给父类
- Qt 继承 QObject 的类,构造函数必须写这个。
cpp
emit callSignal(number);
- emit:Qt 宏,标记 "发射信号"
callSignal(number):触发 callSignal 信号,把 number 的值传递出去
emit 本质是空宏,编译后消失,只是给程序员看的标记。
cpp
void MyPhone::onCallReceive(QString num)
{
qDebug() << "收到来电,号码:" << num;
}
MyPhone::onCallReceive:MyPhone 类里面的 onCallReceive 函数qDebug():Qt 打印输出工具,替代 cout,可以直接打印 QString 类型<<输出运算符,可以拼接多个内容,自动换行。
第三部分 main.cpp 逐词拆解,重点看 phone1.startCall("10086"); 和 connect
cpp
MyPhone phone1;
MyPhone phone2;
MyPhone:Qt 类名(图纸)phone1:对象名,按照 MyPhone 图纸创建出来的实例。 phone1 和 phone2 是两个独立对象,各自拥有自己的成员。 创建的时候,自动调用 MyPhone 构造函数,parent 默认是 nullptr。
cpp
connect(&phone1, &MyPhone::callSignal, &phone2, &MyPhone::onCallReceive);
connect 是 Qt 全局函数,用来把信号和槽绑定在一起。 参数逐个拆解:
&phone1:& 取地址,代表信号发送对象是 phone1(指针)&MyPhone::callSignal:取信号函数地址,phone1 发出的信号 callSignal&phone2:接收信号的对象 phone2&MyPhone::onCallReceive:接收信号后执行的槽函数
一句话:把 phone1 的 callSignal 信号,连接到 phone2 的 onCallReceive 槽函数。只要 phone1 发射 callSignal,phone2 的 onCallReceive 就自动执行。
cpp
phone1.startCall("10086");
- phone1:对象名字,我们操作 phone1 这个实例
.点号(成员访问运算符),读作 "的"。访问 phone1 对象里面的成员函数- startCall:成员函数名称
- **()
圆括号**:代表调用、执行这个函数。括号里面"10086"` 作为参数传给 number ;语句结束标记。
整句含义:调用 phone1 对象的 startCall 成员函数,传入号码 10086。 startCall 内部执行
emit callSignal(number)→ 发射信号。 因为 connect 提前绑定好了,phone2 的 onCallReceive 自动被调用,打印信息。
cpp
return a.exec();
a 是 QCoreApplication 对象,a.exec() 开启 Qt事件循环,监听信号事件。
普通 C++ 控制台程序不需要事件循环;Qt 信号槽程序必须开启事件循环。
第四部分 完整执行流程,一步一步跟着走
QCoreApplication a(argc, argv);创建 Qt 应用程序对象,Qt 程序必须要有这个,管理事件循环。MyPhone phone1;创建 phone1 对象,自动调用 MyPhone 构造函数,parent=nullptr。MyPhone phone2;创建 phone2 对象,自动调用构造函数,parent=nullptr。connect(...)建立连接:phone1 的 callSignal 信号绑定 phone2 的 onCallReceive 槽。此时函数不会执行,只是登记绑定关系。phone1.startCall("10086");调用 phone1 的 startCall 函数,传入参数 "10086" 执行emit callSignal(number)→ 发射 callSignal 信号,附带参数 "10086" Qt 事件系统查找所有绑定这个信号的槽函数 找到 phone2.onCallReceive,自动执行,把 "10086" 传给形参 num qDebug 打印:收到来电,号码:"10086"return a.exec();开启事件循环,等待后续信号事件。本例没有更多事件,程序等待,最后退出。
第五部分 Qt 类 vs 普通 C++ 类对比(复习巩固)
| 项目 | 普通 C++ 类(MobilePhone 例子) | Qt 继承 QObject 的类(MyPhone) |
|---|---|---|
| class 关键字 | 有,定义类 | 有,定义类,底层仍然 C++ |
| 继承 | 可选 | 一般继承 QObject |
| Q_OBJECT | 无 | 需要写,开启元对象系统 |
| signals、emit | 不存在 | Qt 扩展,实现信号槽通信 |
| 内存管理 | new 对象必须手动 delete | 设置 parent,父对象自动释放子对象 |
| 对象拷贝 | 支持 | ❌禁止拷贝,QObject 不能直接赋值复制 |
| 事件循环 | 不需要 | 信号槽程序需要 a.exec () 事件循环 |
新手高频踩坑
- 加了 Q_OBJECT,忘记执行 qmake,报 undefined reference to vtable 虚表错误。
- 信号槽参数:槽的参数不能比信号参数多,否则连接失败。
- QObject 子类不能写
MyPhone p = phone1;拷贝对象,编译报错。 - connect 里要传对象地址,所以要写
&phone1,不要漏掉 &。 - 头文件末尾
};分号不能丢。