Qt元对象系统 day5
内存管理
- QObject以对象树的形式组织起来,当为一个对象创建子对象时,子对象回自动添加到父对象的children()列表中。父对象拥有子对象所有权,比如父对象可以在自己的析构函数中删除它的孩子对象。使用findChild()或findChildren()通过名字和类型查询孩子对象
cpp
QObject(QObject *parent = nullptr)
-
QObject及其派生类的对象,如果其parent非nullptr,那么其parent析构时会析构该对象。
-
父子关系:父对象、子对象、父子关系。这是Qt中所特有的,与类的继承关系无关,传递参数是与parent有关(基类、派生类,或父类、子类,这是对于派生体系来说的,与parent无关)。
-
在Qt中,最基础和核心的类是:QObject,QObject内部有一个名为children的QObjectList列表,会保存所有子对象,还有一个指针parent,用来指向父对象,当自己析构时,会先把自己从parent列表中删除并且析构所有的children。
cpp
#include <QApplication>
#include <QWidget>
#include <QPushButton>
#include <QRadioButton>
//只有在debug模式下才显示调试窗口,如果在release模式下不显示调试窗口
#ifdef _DEBUG
#pragma comment(linker,"/subsystem:console /entry:mainCRTStartup")
#else
#pragma comment(linker,"/subsystem:windows /entry:mainCRTStartup")
#endif // _DEBUG
int main(int argc, char* argv[])
{
QApplication a(argc, argv);
QWidget w;
{
QRadioButton* rBtn = new QRadioButton("男", &w);
//设置对象名
rBtn->setObjectName("man_rBtn");
auto btn = new QPushButton("小瓜");
//设置对象名
btn->setObjectName("小瓜_大瓜");
//如果指定了父对象,则不需要手动show
btn->setParent(&w);
rBtn->move(100, 0);
QObject::connect(btn, &QPushButton::clicked, []()
{
qDebug() << "小瓜";
});
//获取btn的父对象
auto parentw = dynamic_cast<QWidget*>(btn->parent());
//是否获取成功
if (parentw)
{
qDebug() << parentw;
}
//获取子对象列表
const QObjectList& list = w.children();
for (auto v : list)
{
qDebug() << v;
}
//查找指定类型的子对象
qDebug() << "sub object" << w.findChild<QPushButton*>();
//查找指定的子对象名的子对象
qDebug() << w.findChild<QWidget*>("小瓜_大瓜");
}
//把所有子对象添加到窗口之后再显示窗口
w.show();
return a.exec();
}
- 运行结果
释放内存
- Qt里面有些还是得手动释放
cpp
#include <QApplication>
#include <QWidget>
#include <QPushButton>
#include <QRadioButton>
//只有在debug模式下才显示调试窗口,如果在release模式下不显示调试窗口
#ifdef _DEBUG
#pragma comment(linker,"/subsystem:console /entry:mainCRTStartup")
#else
#pragma comment(linker,"/subsystem:windows /entry:mainCRTStartup")
#endif // _DEBUG
int main(int argc, char* argv[])
{
QApplication a(argc, argv);
auto w = new QWidget;
w->show();
QObject::connect(w, &QObject::destroyed, []() {
qDebug() << "释放成功";
});
int ret = a.exec();
//1.直接使用delete释放(对于直接或间接继承QObject的类对象)
delete w;
//2.使用QOBject提供的安全释放的函数来释放对象(下一次运行到事情循环的时候,才去释放对象)
//w->deleteLater(); //此处场景不能使用,因为事件循环已经结束了,当某个窗口不需要的时候就释放掉用这个
return ret;
}
- 运行结果
Qt中的智能指针
- 为了管理内存等资源,C++程序员通常采用RAII(Resource Acquisition Is Initialization)机制:在类的构造函数中申请资源,然后使用,最后在析构函数中释放资源。
- 如果没有智能指针,程序员必须保证new对象能在正确的时机delete,四处编写异常捕获代码以释放资源,而智能指针则可以在退出作用域时(不管是正常流程离开或是因异常离开)总调用delete来析构在堆上动态分配的对象。
- Qt中的智能指针:
智能指针 | 描述 |
---|---|
QPointer | [QObject专享指针]QObject或子类对象释放时会自动指向nullptr |
QScopedPointer | [独享指针]超出作用域自动释放管理的对象 |
QSharedPointer | [共享指针] |
QWeakPointer | [监视指针] |
QScopedArrayPointer | [独享数组指针]超出作用域自动释放管理的对象数组 |
QSharedDataPointer | [隐式共享指针]读时共享,写时拷贝 |
QExplicitlySharedDataPointer | [显示共享指针]读时共享,写时需要手动拷贝(通过detach()函数) |
QPointer
- 受保护指针QPointer的行为类似于普通c++指针T *,只是当被引用的对象被销毁时它会自动清除(不像普通c++指针,在这种情况下它会变成"悬浮指针")。T必须是QObject的子类。
cpp
#include <QApplication>
#include <QWidget>
#include <QPushButton>
#include <QRadioButton>
#include <QPointer>
//只有在debug模式下才显示调试窗口,如果在release模式下不显示调试窗口
#ifdef _DEBUG
#pragma comment(linker,"/subsystem:console /entry:mainCRTStartup")
#else
#pragma comment(linker,"/subsystem:windows /entry:mainCRTStartup")
#endif // _DEBUG
class Text :public QWidget
{
Q_OBJECT
public:
Text(QWidget* parent = nullptr) :QWidget(parent)
{
text_QPointer();
}
void text_QPointer()
{
//QPointer不会自动释放保存的对象
QPointer btn = new QPushButton("小瓜", this);
if (btn)
{
qDebug() << "小瓜";
}
delete btn;//释放之后,btn会自动变为nullptr
if (!btn)
{
qDebug() << "小瓜不见了";
}
}
};
int main(int argc, char* argv[])
{
QApplication a(argc, argv);
Text w;
w.show();
return a.exec();
}
#include "main.moc"
- 运行结果
QScopedPointer
-
手动管理堆分配对象非常困难而且容易出错,通常的结果是代码泄漏内存并且难以维护。QScopedPointer是一个小型实用程序类,它通过将基于堆栈的内存所有权分配给堆分配(更通常称为资源获取初始化(RAII)),极大地简化了这一点。
-
QScopedPointer保证当当前作用域消失时,所指向的对象将被删除。
QSharedPointer
-
QSharedPointer是c++中的一个自动共享指针。它的行为和普通指针完全一样。
-
如果没有其他QSharedPointer对象引用它,当它超出作用域时,QSharedPointer将删除它所持有的指针。
-
QSharedPointer对象可以从一个普通指针、另一个QSharedPointer对象或通过将QWeakPointer对象提升为强引用来创建。
QWeakPointer
-
在c++中,QWeakPointer是对指针的自动弱引用。它不能用于直接解引用该指针,但可以用于验证该指针是否已在另一个上下文中被删除。
-
QWeakPointer对象只能通过从QSharedPointer赋值来创建。
QScopedArrayPointer
- QScopedArrayPointer是一个QScopedPointer,默认使用delete[]操作符删除它所指向的对象。为了方便,它还提供了操作符[]
QSharedDataPointer
-
QSharedDataPointer类表示指向隐式共享对象的指针。
-
QSharedDataPointer使您可以轻松地编写自己的隐式共享类。QSharedDataPointer实现了线程安全的引用计数,确保将QSharedDataPointer添加到可重入类中不会使它们不可重入。
-
许多Qt类都使用隐式共享,以将指针的速度和内存效率与类的易用性结合起来。有关更多信息,请参见共享类页面。
-
假设您想让Employee类隐式共享。这个过程是:定义Employee类,使其拥有类型为QSharedDataPointer的单个数据成员。
-
定义从QSharedData派生的EmployeeData类,以包含通常放入Employee类中的所有数据成员。
QExplicitlySharedDataPointer
-
QExplicitlySharedDataPointer类表示指向显式共享对象的指针。
-
QExplicitlySharedDataPointer使您可以轻松编写自己的显式共享类。QExplicitlySharedDataPointer实现了线程安全的引用计数,确保将QExplicitlySharedDataPointer添加到可重入类中不会使它们不可重入。
-
除了一个很大的区别,QExplicitlySharedDataPointer就像QSharedDataPointer。最大的区别是,QExplicitlySharedDataPointer的成员函数在允许修改共享数据对象之前,不像QSharedDataPointer的非const成员那样在写操作(detach())时自动复制。有一个detach()函数可用,但如果您真的想要detach(),则必须自己调用它。这意味着QExplicitlySharedDataPointers的行为与常规的c++指针类似,只是通过进行引用计数并且在引用计数为0之前不删除共享数据对象,避免了悬浮指针的问题。
属性系统
-
Qt提供了一个复杂的属性系统,类似于一些编译器供应商提供的属性系统。然而,作为一个独立于编译器和平台的库,Qt不依赖于像__property或[property]这样的非标准编译器特性。Qt解决方案可以在Qt支持的每一个平台上使用任何标准的c++编译器。它基于元对象系统,也通过信号和插槽提供对象间通信。
-
属性的行为类似于类数据成员,但它具有通过元对象系统访问的附加特性。
cpp
#include <QApplication>
#include <QWidget>
#include <QPushButton>
#include <QRadioButton>
#include <QPointer>
//只有在debug模式下才显示调试窗口,如果在release模式下不显示调试窗口
#ifdef _DEBUG
#pragma comment(linker,"/subsystem:console /entry:mainCRTStartup")
#else
#pragma comment(linker,"/subsystem:windows /entry:mainCRTStartup")
#endif // _DEBUG
class Person :public QObject
{
Q_OBJECT
public:
Person()
{
}
};
int main(int argc, char* argv[])
{
QApplication a(argc, argv);
Person xiaogua;
//1.通过方法设置一些成员变量
xiaogua.setObjectName("小瓜");
qDebug() << xiaogua.objectName();
//2.通过属性来设置
xiaogua.setProperty("objectName", "大瓜");
qDebug() << xiaogua.objectName() << xiaogua.property("objectName");
//3.如果设置类里面没有的属性,那么则会添加临时的属性
xiaogua.setProperty("name", "小瓜");
xiaogua.setProperty("age", 21);
qDebug() << xiaogua.property("name") << xiaogua.property("age");
return a.exec();
}
#include "main.moc"
- 运行结果
声明自己的属性
-
除了通过
setProperty
动态添加属性之外,怎样才能在代码中,声明属性呢? -
要声明属性,请在继承 QObject 的类中使用 Q_PROPERTY() 宏。
cpp
Q_PROPERTY(type name
(READ getFunction [WRITE setFunction] |
MEMBER memberName [(READ getFunction | WRITE setFunction)])
[RESET resetFunction]
[NOTIFY notifySignal]
[REVISION int | REVISION(int[, int])]
[DESIGNABLE bool]
[SCRIPTABLE bool]
[STORED bool]
[USER bool]
[BINDABLE bindableProperty]
[CONSTANT]
[FINAL]
[REQUIRED])
-
如果未指定 MEMBER 变量,则需要 READ 访问器函数。 它用于读取属性值。 理想情况下,const 函数用于此目的,它必须返回属性的类型或对该类型的 const 引用。 例如,QWidget::focus 是一个带有 READ 函数的只读属性,QWidget::hasFocus()。
-
WRITE 访问器函数是可选的。 它用于设置属性值。 它必须返回 void 并且必须只接受一个参数,该参数可以是属性的类型,也可以是指向该类型的指针或引用。 例如,QWidget::enabled 具有 WRITE 函数 QWidget::setEnabled()。 只读属性不需要 WRITE 函数。 例如,QWidget::focus 没有 WRITE 功能。
-
如果未指定 READ 访问器函数,则需要 MEMBER 变量关联。 这使得给定的成员变量可读可写,而无需创建 READ 和 WRITE 访问器函数。 如果您需要控制变量访问,除了 MEMBER 变量关联之外,仍然可以使用 READ 或 WRITE 访问器函数。
-
RESET 功能是可选的。 它用于将属性设置回其特定于上下文的默认值。 例如,QWidget::cursor 有典型的 READ 和 WRITE 函数,QWidget::cursor() 和 QWidget::setCursor(),它还有一个 RESET 函数,QWidget::unsetCursor(),因为没有调用 QWidget:: setCursor() 可以表示重置为特定于上下文的光标。 RESET 函数必须返回 void 并且不带任何参数。
-
NOTIFY 信号是可选的。 如果已定义,则应指定该类中的一个现有信号,该信号在属性值更改时发出。 MEMBER 变量的 NOTIFY 信号必须采用零个或一个参数,该参数必须与属性的类型相同。 该参数将采用属性的新值。 NOTIFY 信号只应在属性真正被更改时发出,以避免在 QML 中不必要地重新评估绑定,例如。 当没有显式设置器的 MEMBER 属性需要时,Qt 会自动发出该信号。
-
REVISION 编号或 REVISION() 宏是可选的。 如果包含,它定义了要在 API 的特定修订版中使用的属性及其通知信号(通常用于暴露于 QML)。 如果不包含,则默认为 0。
-
DESIGNABLE 属性指示该属性是否应在 GUI 设计工具(例如 Qt Designer)的属性编辑器中可见。 大多数属性都是可设计的(默认为 true)。 有效值为真和假。
-
SCRIPTABLE 属性指示脚本引擎是否可以访问此属性(默认为 true)。 有效值为真和假。
-
STORED 属性指示该属性是否应该被认为是独立存在的,或者取决于其他值。 它还指示在存储对象的状态时是否必须保存属性值。 大多数属性是 STORED 的(默认为 true),但例如,QWidget::minimumWidth() 的 STORED 为 false,因为它的值只是从属性 QWidget::minimumSize() 的宽度分量中获取的,它是一个 QSize。
-
USER 属性指示该属性是被指定为该类的面向用户的属性还是用户可编辑的属性。 通常,每个类只有一个 USER 属性(默认为 false)。 例如,QAbstractButton::checked 是(可检查)按钮的用户可编辑属性。 请注意,QItemDelegate 获取和设置小部件的 USER 属性。
-
BINDABLE bindableProperty 属性表明该属性支持绑定,并且可以通过元对象系统 (QMetaProperty) 设置和检查与该属性的绑定。 bindableProperty 命名 QBindable 类型的类成员,其中 T 是属性类型。 这个属性是在 Qt 6.0 中引入的。
-
CONSTANT 属性的存在表明属性值是常量。 对于给定的对象实例,常量属性的 READ 方法在每次调用时都必须返回相同的值。 对于对象的不同实例,该常数值可能不同。 常量属性不能有 WRITE 方法或 NOTIFY 信号。
-
FINAL 属性的存在表明该属性不会被派生类覆盖。 这在某些情况下可用于性能优化,但并非由 moc 强制执行。 必须注意永远不要覆盖 FINAL 属性。
-
REQUIRED 属性的存在表明该属性应该由该类的用户设置。 这不是由 moc 强制执行的,并且对于暴露给 QML 的类最有用。 在 QML 中,除非设置了所有 REQUIRED 属性,否则无法实例化具有 REQUIRED 属性的类。
cpp
#include <QApplication>
#include <QWidget>
#include <QPushButton>
#include <QRadioButton>
#include <QPointer>
//只有在debug模式下才显示调试窗口,如果在release模式下不显示调试窗口
#ifdef _DEBUG
#pragma comment(linker,"/subsystem:console /entry:mainCRTStartup")
#else
#pragma comment(linker,"/subsystem:windows /entry:mainCRTStartup")
#endif // _DEBUG
int g_tel=666;
class Person :public QObject
{
Q_OBJECT
//让成员变量暴露给属性
Q_PROPERTY(QString name MEMBER m_name NOTIFY nameChanged)
Q_PROPERTY(int age MEMBER m_age NOTIFY ageChanged)
//直接定义属性
Q_PROPERTY(quint64 tel READ getTel WRITE setTel RESET resetTel NOTIFY telChanged)
public:
Person()
{
}
//提供接口
int age()const{ return m_age;}
void setAge(int age) { m_age = age; }
QString name() const { return m_name; }
void setName(const QString& name) { m_name = name; }
quint64 getTel()const { return g_tel; }
void setTel(quint64 tel)
{
if (g_tel != tel)
{
g_tel = tel;
emit telChanged(tel);
}
}
void resetTel() { setTel(-1); }
signals:
void telChanged(quint64 tel);
void ageChanged(int age);
void nameChanged();
protected:
int m_age{};
QString m_name{};
};
int main(int argc, char* argv[])
{
QApplication a(argc, argv);
Person xiaogua;
xiaogua.setName("小瓜");
xiaogua.setAge(20);
QObject::connect(&xiaogua, &Person::telChanged, [](quint64 tel)
{
qDebug() << "tel changed" << tel;
});
QObject::connect(&xiaogua, &Person::ageChanged, []()
{
qDebug() << "age changed";
});
QObject::connect(&xiaogua, &Person::nameChanged, []()
{
qDebug() << "name changed";
});
//xiaogua.setTel(12345678);
xiaogua.setProperty("tel", QVariant());
xiaogua.setAge(21);
xiaogua.setProperty("name", "ccc");
qDebug() << xiaogua.property("name") << xiaogua.property("age") << xiaogua.property("tel");
return a.exec();
}
#include "main.moc"
- 运行结果
绑定属性
- Qt提供了可绑定属性。可绑定属性是具有值或使用任何c++函数(通常是c++ lambda表达式)指定的属性。如果它们是使用c++函数指定的,那么当它们的依赖项发生变化时,它们就会自动更新。
cpp
#include <QApplication>
#include <QWidget>
#include <QPushButton>
#include <QRadioButton>
#include <QPointer>
#include<QProperty>
#include<QObjectBindableProperty>
//只有在debug模式下才显示调试窗口,如果在release模式下不显示调试窗口
#ifdef _DEBUG
#pragma comment(linker,"/subsystem:console /entry:mainCRTStartup")
#else
#pragma comment(linker,"/subsystem:windows /entry:mainCRTStartup")
#endif // _DEBUG
//1,在Qobject子类中使用绑定属性
struct Circle : public QObject
{
Q_OBJECT
public:
Circle()
{
//添加绑定
area.setBinding([=]
{
return M_PI * radius * radius;
});
}
//QProperty<int> radius{};
//QProperty<qreal> area{};
signals:
void radiusChanged();
void areaCahnged();
public:
Q_OBJECT_BINDABLE_PROPERTY_WITH_ARGS(Circle, int, radius, 0, &Circle::radiusChanged);
Q_OBJECT_BINDABLE_PROPERTY_WITH_ARGS(Circle, qreal, area, 0, &Circle::areaCahnged);
};
//2,如果类没有继承自Qobject
struct Rect
{
Rect()
{
area.setBinding([=]
{
return w * h;
});
}
QProperty<int> w;
QProperty<int> h;
QProperty<int> area;
};
int main(int argc, char* argv[])
{
QApplication a(argc, argv);
Circle c;
QObject::connect(&c, &Circle::areaCahnged, []
{
qDebug() << "areaChange";
});
c.radius = 23;
qDebug() << c.area;
Rect r;
r.w = 5;
r.h = 9;
qDebug() << r.area;
return a.exec();
}
#include "main.moc"
内省机制
- 所谓内省是指面向对象语言的一种在运行期间查询对象信息的能力, 比如如果该语具有运行期间检查对象型别的能力,那么我们称它是型别内省(
type intropection
)的,型别内省可以用来实施多态。 - Qt拓展了C++的内省机制,(实际上,它并没有采用C++的RTTI),而是提供了更为强大的元对象(
meta object
)机制,来实现内省。接下来,就让我们看看,Qt是如何扩展c++内省机制的。 - 要深刻理解Qt的内省机制,首先理解
QObject
,QObject
类是整个Qt对象模型的心脏,Qt对象模型最为核心的功能是提供一种无缝的对象通讯机制,即就是我们所熟知的信号和槽。QObject
主要有三大职责: 内存管理、内省(intropection
)与事件处理。本文将集中在在内省的讨论。以下代码介绍了QObject
类提供的内省方法:
cpp
//判断该类是否继承自指定的类
bool inherits(const char *className) const;
QWidget* w = new QWidget;
w.inherits("QObject"); //true
w.inherits("QWidget"); //false
- 示例
cpp
#include<QApplication>
#include<QWidget>
#include<QMetaEnum>
class Test
{
public:
Test()
{
QWidget w;
qDebug() << w.inherits("QObject");
auto metaObject = w.metaObject();
qDebug()<<metaObject->className();
}
};
//1,在命名空间中使用注册枚举
namespace Maye
{
Q_NAMESPACE
enum Type
{
Player,
Enemy,
Bullet
};
Q_ENUM_NS(Type) //把枚举类型注册到元对象系统
}
//2,在类中注册枚举
struct Person : public QObject
{
Q_OBJECT
public:
enum Identity
{
Student,
Doctor,
Teacher
};
Q_ENUM(Identity)
};
int main(int argc, char* argv[])
{
QApplication a(argc, argv);
Test t;
using namespace Maye;
Type type = Player;
qDebug() << type; //0 Player
Person::Identity id = Person::Doctor;
qDebug() << id;
//获取枚举信息
QMetaEnum me = QMetaEnum::fromType<Person::Identity>();
qDebug() << me.name() << me.keyCount();
qDebug() << me.keyToValue("Teacher");
qDebug() << me.valueToKey(Person::Doctor);
return a.exec();
}
#include "main.moc"
QFlags
cpp
#include<QApplication>
#include<QFlags>
struct xiaogua : public QObject
{
Q_OBJECT
public:
enum AlignMent
{
Top = 1,
Left = 1 << 2,
Bottom = 1 << 3,
Right = 1 << 4,
Center = 1 << 5
};
Q_ENUMS(AlignMent)
Q_DECLARE_FLAGS(AlignMentFlags, AlignMent)
Q_FLAG(AlignMentFlags)
};
int main(int argc, char* argv[])
{
QApplication a(argc, argv);
xiaogua::AlignMentFlags flags(xiaogua::Top | xiaogua::Left);
if (flags.testFlag(xiaogua::Top))
{
qDebug() << "has top";
}
if (flags.testFlag(xiaogua::Left))
{
qDebug() << "has left";
}
return a.exec();
}
#include "main.moc"
- 运行结果