QT 中的元对象系统(三):QObject深入理解

目录
1.简介
2.特性
2.1.对象树与内存管理
2.2.信号与槽机制
2.3.事件处理
2.4.属性系统
2.4.1.Q_PROPERTY配置的属性
2.4.2.动态属性
2.4.3.实现原理
2.5.国际化支持
2.6. 定时器支持
3.类设计(q和d指针)
4.总结
1.简介
QObject这个 class 是 QT 对象模型的核心,它是所有 Qt 对象的基类。它为对象间通信(信号与槽机制)、事件处理、定时器支持以及对象树管理等功能提供了基础。
QObject 类的实现文件一共有四个:
1) qobject.h,QObject class 的基本定义,也是我们一般定义一个类的头文件。
2) qobject.cpp,QObject class 的实现代码基本上都在这个文件。
3) qobjectdefs.h,这个文件中最重要的东西就是定义了 QMetaObject class,这个class是为了实现 signal、slot、properties,的核心部分。
4) qobject_p.h,这个文件中的 code 是辅助实现QObject class 的,这里面最重要的东西是定义了一个 QObjectPrivate 类来存储 QOjbect 对象的成员数据。

2.特性
2.1.对象树与内存管理
当你创建一个 QObject 并使用其它对象作为父对象时,这个对象会自动添加到父对象的children() list 中。父对象拥有这个对象,比如,它将在它的析构函数中自动删除它所有的 child对象。你可以通过 findChild() 或者findChildren()函数来查找一个对象。每个对象都有一个对象名称(objectName())和类名称(class name), 他们都可以通过相应的 metaObject 对象来获得。你还可以通过 inherits() 方法来判断一个对象的类是不是从另一个类继承而来。当对象被删除时,它发出destroyed()信号。你可以捕获这个信号来避免对QObject的无效引用。QObject可以通过event()接收事件并且过滤其它对象的事件。详细情况请参考installEventFilter()和eventFilter()。对于每一个实现了信号、槽和属性的对象来说,Q_OBJECT 宏都是必须要加上的。

总结:
QObject支持对象树的概念,即父对象自动管理子对象的生命周期。- 当父对象被销毁时,其所有子对象也会自动被销毁。
2.2.信号与槽机制
QT 中的元对象系统(一):元对象和元数据-CSDN博客
QT 中的元对象系统(二):元对象实现原理QMetaObject_public: static struct qmetaobject const qcustomplo-CSDN博客
QT框架里面最大的特色就是在C++的基础上增加了元对象系统(Meta-Object System),而元对象系统里面最重要的内容就是信号与槽机制,这个机制是在C++语法的基础上实现的,使用了函数、函数指针、回调函数等概念。当然与我们自己去写函数所不同的是槽与信号机制会自动帮我们生成部分代码,比如我们写的信号函数就不需要写它的实现部分,这是因为在我们编译程序的时候,编译器会自动生成这一部分代码,当我们调用connect函数的时候,系统会自动将信号函数与槽函数相连接,于是当我们调用信号函数的时候,系统就会自动回调槽函数,不管你是在同一线程下调用或者在不同线程下调用,系统都会自动评估,并在合理的时候触发函数,以此来保证线程的安全。信号与槽机制是线程安全的,这可以使得我们在调用的时候不用再额外的增加过多保证线程同步的代码,为了实现元对象系统,QT把所有相关实现写在了QObject类中,所以当你想使用元对象系统的时候,你所写的类需要继承自QObject,包括QT自带的所有类都是继承自QObject。
static QMetaObject::Connection connect(const QObject *sender, const char *signal,const QObject *receiver, const char *member, Qt::ConnectionType = Qt::AutoConnection);static QMetaObject::Connection connect(const QObject *sender, const QMetaMethod &signal,const QObject *receiver, const QMetaMethod &method,Qt::ConnectionType type = Qt::AutoConnection);inline QMetaObject::Connection connect(const QObject *sender, const char *signal,const char *member, Qt::ConnectionType type = Qt::AutoConnection) const;#ifdef Q_CLANG_QDOCtemplate<typename PointerToMemberFunction>static QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, const QObject *receiver, PointerToMemberFunction method, Qt::ConnectionType type = Qt::AutoConnection);template<typename PointerToMemberFunction, typename Functor>static QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor);template<typename PointerToMemberFunction, typename Functor>static QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, const QObject *context, Functor functor, Qt::ConnectionType type = Qt::AutoConnection);
#else//Connect a signal to a pointer to qobject member functiontemplate <typename Func1, typename Func2>static inline QMetaObject::Connection connect(const typename QtPrivate::FunctionPointer<Func1>::Object *sender, Func1 signal,const typename QtPrivate::FunctionPointer<Func2>::Object *receiver, Func2 slot,Qt::ConnectionType type = Qt::AutoConnection){typedef QtPrivate::FunctionPointer<Func1> SignalType;typedef QtPrivate::FunctionPointer<Func2> SlotType;Q_STATIC_ASSERT_X(QtPrivate::HasQ_OBJECT_Macro<typename SignalType::Object>::Value,"No Q_OBJECT in the class with the signal");//compilation error if the arguments does not match.Q_STATIC_ASSERT_X(int(SignalType::ArgumentCount) >= int(SlotType::ArgumentCount),"The slot requires more arguments than the signal provides.");Q_STATIC_ASSERT_X((QtPrivate::CheckCompatibleArguments<typename SignalType::Arguments, typename SlotType::Arguments>::value),"Signal and slot arguments are not compatible.");Q_STATIC_ASSERT_X((QtPrivate::AreArgumentsCompatible<typename SlotType::ReturnType, typename SignalType::ReturnType>::value),"Return type of the slot is not compatible with the return type of the signal.");const int *types = nullptr;if (type == Qt::QueuedConnection || type == Qt::BlockingQueuedConnection)types = QtPrivate::ConnectionTypes<typename SignalType::Arguments>::types();return connectImpl(sender, reinterpret_cast<void **>(&signal),receiver, reinterpret_cast<void **>(&slot),new QtPrivate::QSlotObject<Func2, typename QtPrivate::List_Left<typename SignalType::Arguments, SlotType::ArgumentCount>::Value,typename SignalType::ReturnType>(slot),type, types, &SignalType::Object::staticMetaObject);}//connect to a function pointer (not a member)template <typename Func1, typename Func2>static inline typename std::enable_if<int(QtPrivate::FunctionPointer<Func2>::ArgumentCount) >= 0, QMetaObject::Connection>::typeconnect(const typename QtPrivate::FunctionPointer<Func1>::Object *sender, Func1 signal, Func2 slot){return connect(sender, signal, sender, slot, Qt::DirectConnection);}//connect to a function pointer (not a member)template <typename Func1, typename Func2>static inline typename std::enable_if<int(QtPrivate::FunctionPointer<Func2>::ArgumentCount) >= 0 &&!QtPrivate::FunctionPointer<Func2>::IsPointerToMemberFunction, QMetaObject::Connection>::typeconnect(const typename QtPrivate::FunctionPointer<Func1>::Object *sender, Func1 signal, const QObject *context, Func2 slot,Qt::ConnectionType type = Qt::AutoConnection){typedef QtPrivate::FunctionPointer<Func1> SignalType;typedef QtPrivate::FunctionPointer<Func2> SlotType;Q_STATIC_ASSERT_X(QtPrivate::HasQ_OBJECT_Macro<typename SignalType::Object>::Value,"No Q_OBJECT in the class with the signal");//compilation error if the arguments does not match.Q_STATIC_ASSERT_X(int(SignalType::ArgumentCount) >= int(SlotType::ArgumentCount),"The slot requires more arguments than the signal provides.");Q_STATIC_ASSERT_X((QtPrivate::CheckCompatibleArguments<typename SignalType::Arguments, typename SlotType::Arguments>::value),"Signal and slot arguments are not compatible.");Q_STATIC_ASSERT_X((QtPrivate::AreArgumentsCompatible<typename SlotType::ReturnType, typename SignalType::ReturnType>::value),"Return type of the slot is not compatible with the return type of the signal.");const int *types = nullptr;if (type == Qt::QueuedConnection || type == Qt::BlockingQueuedConnection)types = QtPrivate::ConnectionTypes<typename SignalType::Arguments>::types();return connectImpl(sender, reinterpret_cast<void **>(&signal), context, nullptr,new QtPrivate::QStaticSlotObject<Func2,typename QtPrivate::List_Left<typename SignalType::Arguments, SlotType::ArgumentCount>::Value,typename SignalType::ReturnType>(slot),type, types, &SignalType::Object::staticMetaObject);}//connect to a functortemplate <typename Func1, typename Func2>static inline typename std::enable_if<QtPrivate::FunctionPointer<Func2>::ArgumentCount == -1, QMetaObject::Connection>::typeconnect(const typename QtPrivate::FunctionPointer<Func1>::Object *sender, Func1 signal, Func2 slot){return connect(sender, signal, sender, std::move(slot), Qt::DirectConnection);}//connect to a functor, with a "context" object defining in which event loop is going to be executedtemplate <typename Func1, typename Func2>static inline typename std::enable_if<QtPrivate::FunctionPointer<Func2>::ArgumentCount == -1, QMetaObject::Connection>::typeconnect(const typename QtPrivate::FunctionPointer<Func1>::Object *sender, Func1 signal, const QObject *context, Func2 slot,Qt::ConnectionType type = Qt::AutoConnection){typedef QtPrivate::FunctionPointer<Func1> SignalType;const int FunctorArgumentCount = QtPrivate::ComputeFunctorArgumentCount<Func2 , typename SignalType::Arguments>::Value;Q_STATIC_ASSERT_X((FunctorArgumentCount >= 0),"Signal and slot arguments are not compatible.");const int SlotArgumentCount = (FunctorArgumentCount >= 0) ? FunctorArgumentCount : 0;typedef typename QtPrivate::FunctorReturnType<Func2, typename QtPrivate::List_Left<typename SignalType::Arguments, SlotArgumentCount>::Value>::Value SlotReturnType;Q_STATIC_ASSERT_X((QtPrivate::AreArgumentsCompatible<SlotReturnType, typename SignalType::ReturnType>::value),"Return type of the slot is not compatible with the return type of the signal.");Q_STATIC_ASSERT_X(QtPrivate::HasQ_OBJECT_Macro<typename SignalType::Object>::Value,"No Q_OBJECT in the class with the signal");const int *types = nullptr;if (type == Qt::QueuedConnection || type == Qt::BlockingQueuedConnection)types = QtPrivate::ConnectionTypes<typename SignalType::Arguments>::types();return connectImpl(sender, reinterpret_cast<void **>(&signal), context, nullptr,new QtPrivate::QFunctorSlotObject<Func2, SlotArgumentCount,typename QtPrivate::List_Left<typename SignalType::Arguments, SlotArgumentCount>::Value,typename SignalType::ReturnType>(std::move(slot)),type, types, &SignalType::Object::staticMetaObject);}
#endif //Q_CLANG_QDOCstatic bool disconnect(const QObject *sender, const char *signal,const QObject *receiver, const char *member);static bool disconnect(const QObject *sender, const QMetaMethod &signal,const QObject *receiver, const QMetaMethod &member);inline bool disconnect(const char *signal = nullptr,const QObject *receiver = nullptr, const char *member = nullptr) const{ return disconnect(this, signal, receiver, member); }inline bool disconnect(const QObject *receiver, const char *member = nullptr) const{ return disconnect(this, nullptr, receiver, member); }static bool disconnect(const QMetaObject::Connection &);#ifdef Q_CLANG_QDOCtemplate<typename PointerToMemberFunction>static bool disconnect(const QObject *sender, PointerToMemberFunction signal, const QObject *receiver, PointerToMemberFunction method);
#elsetemplate <typename Func1, typename Func2>static inline bool disconnect(const typename QtPrivate::FunctionPointer<Func1>::Object *sender, Func1 signal,const typename QtPrivate::FunctionPointer<Func2>::Object *receiver, Func2 slot){typedef QtPrivate::FunctionPointer<Func1> SignalType;typedef QtPrivate::FunctionPointer<Func2> SlotType;Q_STATIC_ASSERT_X(QtPrivate::HasQ_OBJECT_Macro<typename SignalType::Object>::Value,"No Q_OBJECT in the class with the signal");//compilation error if the arguments does not match.Q_STATIC_ASSERT_X((QtPrivate::CheckCompatibleArguments<typename SignalType::Arguments, typename SlotType::Arguments>::value),"Signal and slot arguments are not compatible.");return disconnectImpl(sender, reinterpret_cast<void **>(&signal), receiver, reinterpret_cast<void **>(&slot),&SignalType::Object::staticMetaObject);}template <typename Func1>static inline bool disconnect(const typename QtPrivate::FunctionPointer<Func1>::Object *sender, Func1 signal,const QObject *receiver, void **zero){// This is the overload for when one wish to disconnect a signal from any slot. (slot=nullptr)// Since the function template parameter cannot be deduced from '0', we use a// dummy void ** parameter that must be equal to 0Q_ASSERT(!zero);typedef QtPrivate::FunctionPointer<Func1> SignalType;return disconnectImpl(sender, reinterpret_cast<void **>(&signal), receiver, zero,&SignalType::Object::staticMetaObject);}
在Qt5版本上对槽函数的支持也是多种多样的,其中包括C语言函数、类函数、仿函数、lambda表达式等等。
C++可调用Callable类型的总结_c++ callable-CSDN博客
信号与槽的连接方式也是多种多样的,如下图所示:
enum ConnectionType {AutoConnection,DirectConnection,QueuedConnection,BlockingQueuedConnection,UniqueConnection = 0x80};
它们的实现方式和区别将在后面讲解。
总结:
QObject是实现 Qt 中信号 (signals) 和槽 (slots) 机制的基础。- 信号和槽是一种类型安全的回调机制,用于对象之间的通信。
- 使用
Q_OBJECT宏可以启用信号和槽功能。
2.3.事件处理
QObject可以通过event()接收事件并且过滤其它对象的事件。详细情况请参考installEventFilter()和eventFilter()。
virtual bool event(QEvent *event);
virtual bool eventFilter(QObject *watched, QEvent *event);...void installEventFilter(QObject *filterObj);
void removeEventFilter(QObject *obj);
示例如下:
事件过滤是另一个常见的应用场景。通过重写QObject的eventFilter函数,我们可以在事件到达目标对象之前对其进行拦截和处理。
// 示例:使用事件过滤
bool MyClass::eventFilter(QObject *watched, QEvent *event)
{if (watched == targetObject && event->type() == QEvent::KeyPress) {// 拦截并处理键盘事件return true;}return false;
}
在这个示例中,MyClass通过eventFilter函数拦截了目标对象targetObject的键盘事件。
总结:
QObject提供了事件处理机制,允许子类重写event()方法来处理事件。- 常见的事件包括鼠标事件、键盘事件等。
2.4.属性系统
2.4.1.Q_PROPERTY配置的属性
Q_PROPERTY(type nameREAD getFunction[WRITE setFunction][RESET resetFunction][ NOTIFY notifySignal][ DESIGNABLE bool ][ SCRIPTABLE bool ][ STORED bool ][ USER bool ][ CONSTANT ][ FINAL ])
参数说明:
type:属性的数据类型。
name:属性的名称。
READ getFunction:获取属性值的函数。
WRITE setFunction(可选):设置属性值的函数。
RESET resetFunction(可选):重置属性值的函数。
NOTIFY notifySignal(可选):当属性值发生变化时发出的通知信号。
DESIGNABLE bool(可选):是否可以在设计模式下访问该属性(如 Qt Designer)。
SCRIPTABLE bool(可选):是否可以通过脚本访问该属性。
STORED bool(可选):属性值是否存储在对象的状态中(如果为 false,则表示该属性是派生的)。
USER bool(可选):是否是用户属性(主要用于 GUI 系统)。
CONSTANT(可选):表示该属性是一个常量,不能被修改。
FINAL(可选):表示该属性不能被子类覆盖。
Q_PROPERTY 是 Qt 框架中的一个宏,用于定义类的属性(property)。通过 Q_PROPERTY 宏,可以将类的成员变量暴露为属性,并提供对这些属性的访问、修改和通知机制。这是 Qt 的元对象系统(Meta-Object System)的一部分;实际上我们在做界面设计的时候经常会用到属性,比如修改Label的显示内容,需要用到Text属性,修改窗体长宽等等,在你做界面设计的时候,属性编辑框里面所显示的就是当前对象的所有属性,而这些属性的定义就是用上面的宏来定义的。实际上属性和变量是有点相似的,都是读值和写值的功能,那为什么不直接对变量操作就好了?虽然看起来相似,但是还是有不同点,第一属性可以定义为可读写的,也可以定义为只读的,比如有些数据我们只在类的内部做修改不允许在外部做修改,但是有时候又需要在外部查看这个值,就可以设置为只读属性,而变量是做不到这点的,你把变量放在public部分,那么这个变量就可以在任何地方被修改,这就破坏了类的封装性。第二属性可以定义信号,当属性变化的时候触发信号,这样我们可以在信号触发时做一些工作,比如当你设置LineEdit为readonly时,你会发现输入框的背景颜色被改变了,这就可以通过属性变化的信号来处理。
实例一:以下是一个简单的 Q_PROPERTY 使用示例:
#include <QObject>
#include <QDebug>class MyClass : public QObject {Q_OBJECT// 定义一个整数属性 "value"Q_PROPERTY(int value READ getValue WRITE setValue NOTIFY valueChanged)public:explicit MyClass(QObject *parent = nullptr) : QObject(parent), m_value(0) {}int getValue() const { return m_value; } // 获取属性值void setValue(int val) { // 设置属性值if (m_value != val) {m_value = val;emit valueChanged(m_value); // 发出通知信号}}signals:void valueChanged(int newValue); // 属性变化的通知信号private:int m_value;
};int main(int argc, char *argv[]) {MyClass obj;// 动态访问属性qDebug() << "Initial value:" << obj.property("value").toInt();// 修改属性obj.setValue(42);// 再次访问属性qDebug() << "Updated value:" << obj.property("value").toInt();return 0;
}#include "main.moc"
示例二: QML 中使用 Q_PROPERTY
// 注册类到 QML
qmlRegisterType<MyClass>("MyModule", 1, 0, "MyClass");// 在 QML 中使用
import MyModule 1.0MyClass {id: myClassvalue: 10onValueChanged: console.log("Value changed to", value)
}
注意事项
-
Q_OBJECT宏:使用Q_PROPERTY的类必须继承自QObject并包含Q_OBJECT宏。 -
类型限制:属性的类型必须是 Qt 支持的元类型(Meta-Type),或者需要通过
qRegisterMetaType()注册自定义类型。 -
性能:属性访问比直接访问成员变量稍慢,因为涉及到元对象系统的开销。
2.4.2.动态属性
继承QObject并使用Q_OBJECT宏后,你的类将能够使用动态属性。这是一种在运行时添加、修改或删除对象属性的机制。并通过 setProperty() 和 property() 方法访问这些属性。与通过 Q_PROPERTY 宏定义的静态属性不同,动态属性不需要在编译时定义,完全可以在运行时创建和修改。
动态属性的基本操作
1. 设置动态属性:使用 QObject::setProperty(const char *name, const QVariant &value) 方法可以为对象设置一个动态属性。
2. 获取动态属性:使用 QObject::property(const char *name) 方法可以获取指定名称的动态属性值。
3. 检查属性是否存在:使用 QObject::dynamicPropertyNames() 方法可以获取对象的所有动态属性名称列表,从而判断某个属性是否存在。
4. 删除动态属性:Qt 没有直接提供删除动态属性的方法,但可以通过重新设置为默认值或覆盖的方式来“清除”属性。
以下是一个简单的示例,展示如何使用动态属性:
#include <QObject>
#include <QDebug>int main(int argc, char *argv[]) {QObject obj;// 设置动态属性obj.setProperty("name", "MyObject");obj.setProperty("age", 42);// 获取动态属性qDebug() << "Name:" << obj.property("name").toString();qDebug() << "Age:" << obj.property("age").toInt();// 检查所有动态属性QList<QByteArray> propertyNames = obj.dynamicPropertyNames();qDebug() << "Dynamic Property Names:" << propertyNames;// 修改动态属性obj.setProperty("age", 43);qDebug() << "Updated Age:" << obj.property("age").toInt();return 0;
}
输出结果:
Name: "MyObject"
Age: 42
Dynamic Property Names: ("name", "age")
Updated Age: 43
动态属性的特点
1.灵活性:动态属性可以在运行时动态添加、修改和访问,适合需要灵活扩展的对象。
2.类型安全:动态属性的值以 QVariant 的形式存储,支持多种数据类型。
3.元对象系统支持:动态属性是 Qt 元对象系统的一部分,可以通过反射机制访问。
4.不支持通知机制:动态属性没有内置的通知机制。如果需要属性变化的通知功能,建议使用 Q_PROPERTY 宏定义静态属性。
动态属性的应用场景
1.UI 属性扩展:在 GUI 应用程序中,可以为控件动态添加自定义属性,用于存储额外的信息。
2.配置管理:使用动态属性存储对象的运行时配置信息。
3.插件系统:在插件系统中,动态属性可以用来存储和传递插件相关的元数据。
2.4.3.实现原理
//设置属性
bool setProperty(const char *name, const QVariant &value);
//获取属性
QVariant property(const char *name) const;
//获取所有动态属性
QList<QByteArray> dynamicPropertyNames() const;
查看一下setProperty的源码:
bool QObject::setProperty(const char *name, const QVariant &value)
{Q_D(QObject);const QMetaObject* meta = metaObject();if (!name || !meta)return false;int id = meta->indexOfProperty(name);if (id < 0) {if (!d->extraData)d->extraData = new QObjectPrivate::ExtraData;const int idx = d->extraData->propertyNames.indexOf(name);if (!value.isValid()) {if (idx == -1)return false;d->extraData->propertyNames.removeAt(idx);d->extraData->propertyValues.removeAt(idx);} else {if (idx == -1) {d->extraData->propertyNames.append(name);d->extraData->propertyValues.append(value);} else {if (value.userType() == d->extraData->propertyValues.at(idx).userType()&& value == d->extraData->propertyValues.at(idx))return false;d->extraData->propertyValues[idx] = value;}}QDynamicPropertyChangeEvent ev(name);QCoreApplication::sendEvent(this, &ev);return false;}QMetaProperty p = meta->property(id);
#ifndef QT_NO_DEBUGif (!p.isWritable())qWarning("%s::setProperty: Property \"%s\" invalid,"" read-only or does not exist", metaObject()->className(), name);
#endifreturn p.write(this, value);
}
可以看到setProperty还是依赖于QObject的元系统的,首先判断属性名name是否为Q_PROPERTY设置的属性,如果是则根据元系统的实现方式,获取元属性的索引id,再写入值; 接着动态属性是在存在QObject的数据区中的propertyNames和propertyValues中,其实对动态属性的存取就是对propertyNames和propertyValues的操作。
class Q_CORE_EXPORT QObjectPrivate : public QObjectData
{Q_DECLARE_PUBLIC(QObject)public:struct ExtraData{ExtraData() {}#ifndef QT_NO_USERDATAQVector<QObjectUserData *> userData;#endifQList<QByteArray> propertyNames; //动态属性名QVector<QVariant> propertyValues; //动态属性值QVector<int> runningTimers;QList<QPointer<QObject> > eventFilters;QString objectName;};//...
};
2.5.国际化支持
#if defined(QT_NO_TRANSLATION)static QString tr(const char *sourceText, const char * = nullptr, int = -1){ return QString::fromUtf8(sourceText); }
#if QT_DEPRECATED_SINCE(5, 0)QT_DEPRECATED static QString trUtf8(const char *sourceText, const char * = nullptr, int = -1){ return QString::fromUtf8(sourceText); }
#endif
QObject提供了对翻译的支持,可以通过tr()函数实现多语言文本的翻译。
2.6. 定时器支持
int startTimer(int interval, Qt::TimerType timerType = Qt::CoarseTimer);
#if QT_HAS_INCLUDE(<chrono>)Q_ALWAYS_INLINEint startTimer(std::chrono::milliseconds time, Qt::TimerType timerType = Qt::CoarseTimer){return startTimer(int(time.count()), timerType);}
#endifvoid killTimer(int id);
QObject可以通过startTimer()创建定时器,并在timerEvent()中处理定时器事件。
3.类设计(q和d指针)
C++惯用法之Pimpl
我们知道,在C++中,几乎每一个类(class)中都需要有一些类的成员变量(class member variable),在通常情况下的做法如下:
class Person
{
private:std::string mszName; // 姓名bool mbSex; // 性别int mnAge; // 年龄
};
在QT中,却几乎都不是这样做的,那么,QT是怎么做的呢?
几乎每一个C++的类中都会保存许多的数据,要想读懂别人写的C++代码,就一定需要知道每一个类的的数据是如何存储的,是什么含义,否则,我们不可能读懂别人的C++代码。在这里也就是说,要想读懂QT的代码,第一步就必须先搞清楚QT的类成员数据是如何保存的。
为了更容易理解QT是如何定义类成员变量的,我们先说一下QT 2.x 版本中的类成员变量定义方法,因为在 2.x 中的方法非常容易理解。然后在介绍 QT 4.4 中的类成员变量定义方法。
QT 2.x 中的方法
在定义class的时候(在.h文件中),只包含有一个类成员变量,只是定义一个成员数据指针,然后由这个指针指向一个数据成员对象,这个数据成员对象包含所有这个class的成员数据,然后在class的实现文件(.cpp文件)中,定义这个私有数据成员对象。示例代码如下:
// File name: person.h
struct PersonalDataPrivate; // 声明私有数据成员类型
class Person
{
public:Person(); // constructorvirtual ~Person(); // destructorvoid setAge(const int);int getAge();
private:PersonalDataPrivate* d;
};
//---------------------------------------------------------------------
// File name: person.cpp
struct PersonalDataPrivate // 定义私有数据成员类型
{string mszName; // 姓名bool mbSex; // 性别int mnAge; // 年龄
};// constructor
Person::Person()
{d = new PersonalDataPrivate;
};// destructor
Person::~Person()
{delete d;
};void Person::setAge(const int age)
{if (age != d->mnAge)d->mnAge = age;
}int Person::getAge()
{return d->mnAge;
}
在最初学习QT的时候,我也觉得这种方法很麻烦,但是随着使用的增多,我开始很喜欢这个方法了,而且,现在我写的代码,基本上都会用这种方法。具体说来,它有如下优点:
减少头文件的依赖性
把具体的数据成员都放到cpp文件中去,这样,在需要修改数据成员的时候,只需要改cpp文件而不需要头文件,这样就可以避免一次因为头文件的修改而导致所有包含了这个文件的文件全部重新编译一次,尤其是当这个头文件是非常底层的头文件和项目非常庞大的时候,优势明显。
同时,也减少了这个头文件对其它头文件的依赖性。可以把只在数据成员中需要用到的在cpp文件中include一次就可以,在头文件中就可以尽可能的减少include语句
增强类的封装性
这种方法增强了类的封装性,无法再直接存取类成员变量,而必须写相应的 get/set 成员函数来做这些事情。
关于这个问题,仁者见仁,智者见智,每个人都有不同的观点。有些人就是喜欢把类成员变量都定义成public的,在使用的时候方便。只是我个人不喜欢这种方法,当项目变得很大的时候,有非常多的人一起在做这个项目的时候,自己所写的代码处于底层有非常多的人需要使用(#include)的时候,这个方法的弊端就充分的体现出来了。
还有,我不喜欢 QT 2.x 中把数据成员的变量名都定义成只有一个字母d,看起来很不直观,尤其是在search的时候,很不方便。但是,QT kernel 中的确就是这么干的。
QT 4.4.x 中的方法
在 QT 4.4 中,类成员变量定义方法的出发点没有变化,只是在具体的实现手段上发生了非常大的变化,在 QT 4.4 中,使用了非常多的宏来做事,这凭空的增加了理解 QT source code 的难度,不知道他们是不是从MFC学来的。就连在定义类成员数据变量这件事情上,也大量的使用了宏。
在这个版本中,类成员变量不再是给每一个class都定义一个私有的成员,而是把这一项common的工作放到了最基础的基类 QObject 中,然后定义了一些相关的方法来存取,好了,让我们进入具体的代码吧。
// file name: qobject.h
class QObjectData
{
public:virtual ~QObjectData() = 0;// 省略
};class QObject
{Q_DECLARE_PRIVATE(QObject)public:QObject(QObject* parent = 0);protected:QObject(QObjectPrivate& dd, QObject* parent = 0);QObjectData* d_ptr;
};
这些代码就是在 qobject.h 这个头文件中的。在 QObject class 的定义中,我们看到,数据员的定义为:QObjectData*d_ptr; 定义成 protected 类型的就是要让所有的派生类都可以存取这个变量,而在外部却不可以直接存取这个变量。而 QObjectData 的定义却放在了这个头文件中,其目的就是为了要所有从QObject继承出来的类的成员变量也都相应的要在QObjectData这个class继承出 来。而纯虚的析构函数又决定了两件事:
这个class不能直接被实例化。换句话说就是,如果你写了这么一行代码,new QObjectData, 这行代码一定会出错,compile的时候是无法过关的。
当 delete 这个指针变量的时候,这个指针变量是指向的任意从QObjectData继承出来的对象的时候,这个对象都能被正确delete,而不会产生错误,诸如,内存泄漏之类的。
我们再来看看这个宏做了什么,Q_DECLARE_PRIVATE(QObject)
#define Q_DECLARE_PRIVATE(Class) \inline Class##Private* d_func() { return reinterpret_cast<Class##Private *>(qGetPtrHelper(d_ptr)); } \inline const Class##Private* d_func() const { return reinterpret_cast<const Class##Private *>(qGetPtrHelper(d_ptr)); } \friend class Class##Private;
这个宏主要是定义了两个重载的函数,d_func(),作用就是把在QObject这个class中定义的数据成员变量d_ptr安全的转换成为每一个具 体的class的数据成员类型指针。我们看一下在QObject这个class中,这个宏展开之后的情况,就一幕了然了。
Q_DECLARE_PRIVATE(QObject)展开后,就是下面的代码:
inline QObjectPrivate* d_func() { return reinterpret_cast<QObjectPrivate *>(d_ptr); }
inline const QObjectPrivate* d_func() const
{ return reinterpret_cast<const QObjectPrivate *>;(d_ptr); } \
friend class QObjectPrivate;
宏展开之后,新的问题又来了,这个QObjectPrivate是从哪里来的?在QObject这个class中,为什么不直接使用QObjectData来数据成员变量的类型?
还记得我们刚才说过吗,QObjectData这个class的析构函数的纯虚函数,这就说明这个class是不能实例化的,所以,QObject这个class的成员变量的实际类型,这是从QObjectData继承出来的,它就是QObjectPrivate !
这个 class 中保存了许多非常重要而且有趣的东西,其中包括 QT 最核心的 signal 和slot 的数据,属性数据,等等,我们将会在后面详细讲解,现在我们来看一下它的定义:
下面就是这个class的定义:
class QObjectPrivate : public QObjectData
{Q_DECLARE_PUBLIC(QObject)public:QObjectPrivate(int version = QObjectPrivateVersion);virtual ~QObjectPrivate();// 省略
}
那么,这个 QObjectPrivate 和 QObject 是什么关系呢?他们是如何关联在一起的呢?
接上节,让我们来看看这个 QObjectPrivate 和 QObject 是如何关联在一起的。
// file name: qobject.cpp
QObject::QObject(QObject* parent): d_ptr(new QObjectPrivate)
{// ………………………
}QObject::QObject(QObjectPrivate& dd, QObject* parent): d_ptr(&dd)
{// …………………
}
从第一个构造函数可以很清楚的看出来,QObject class 中的 d_ptr 指针将指向一个 QObjectPrivate 的对象,而QObjectPrivate这个class是从QObjectData继承出来的。
这第二个构造函数干什么用的呢?从 QObject class 的定义中,我们可以看到,这第二个构造函数是被定义为protected 类型的,这说明,这个构造函数只能被继承的class使用,而不能使用这个构造函数来直接构造一个QObject对象,也就是说,如果写一条下面的语句, 编译的时候是会失败的,
new QObject(*new QObjectPrivate, NULL);
为了看的更清楚,我们以QWidget这个class为例说明。
QWidget是QT中所有UI控件的基类,它直接从QObject继承而来,
class QWidget : public QObject, public QPaintDevice
{ Q_OBJECT Q_DECLARE_PRIVATE(QWidget) // .....................
}
我们看一个这个class的构造函数的代码:
QWidget::QWidget(QWidget *parent, Qt::WindowFlags f)
: QObject(*new QWidgetPrivate, 0), QPaintDevice()
{ d_func()->init(parent, f);
}
非常清楚,它调用了基类QObject的保护类型的构造函数,并且以 *new QWidgetPrivate 作为第一个参数传递进去。也就是说,基类(QObject)中的d_ptr指针将会指向一个QWidgetPrivate类型的对象。
再看QWidgetPrivate这个class的定义:
class QWidgetPrivate : public QObjectPrivate
{ Q_DECLARE_PUBLIC(QWidget) // .....................
};
好了,这就把所有的事情都串联起来了。
关于QWidget构造函数中的唯一的语句 d_func()->init(parent, f) 我们注意到在class的定义中有这么一句话: Q_DECLARE_PRIVATE(QWidget)
我们前面讲过这个宏,当把这个宏展开之后,就是这样的:
inline QWidgetPrivate* d_func() { return reinterpret_cast<QWidgetPrivate *>(d_ptr); }
inline const QWidgetPrivate* d_func() const
{ return reinterpret_cast<const QWidgetPrivate *>(d_ptr); } \
friend class QWidgetPrivate;
很清楚,它就是把QObject中定义的d_ptr指针转换为QWidgetPrivate类型的指针。
4.总结
QObject 是 Qt 框架的核心组件,提供了丰富的功能和强大的灵活性,适用于从简单到复杂的各种应用程序开发需求。它的主要优势包括:
- 对象间通信:通过信号与槽机制实现松耦合的设计。
- 内存管理:通过对象树自动管理资源。
- 事件驱动:支持事件处理和扩展。
- 国际化支持:简化多语言应用的开发。
- 动态扩展性:支持动态属性和定时器。
在实际开发中,QObject 的功能被广泛应用于 GUI 开发、网络编程、嵌入式开发等多个领域。掌握 QObject 的核心特性对于高效使用 Qt 框架至关重要。
相关文章:
QT 中的元对象系统(三):QObject深入理解
目录 1.简介 2.特性 2.1.对象树与内存管理 2.2.信号与槽机制 2.3.事件处理 2.4.属性系统 2.4.1.Q_PROPERTY配置的属性 2.4.2.动态属性 2.4.3.实现原理 2.5.国际化支持 2.6. 定时器支持 3.类设计(q和d指针) 4.总结 1.简介 QObject这个 class 是 QT 对象模型的核心&…...
二、QT和驱动模块实现智能家居-----问题汇总1
1、文件地址改变后必须在QT下更改地址 2、指定了QT内Kits下的Sysroot头文件地址,但是还是找不到头文件: 3、提示无法执行QT程序:先干掉之前的QT程序 ps //查看程序PIDkill -9 PID 4、无法执行QT程序 1)未设置环境变量 …...
Golang的数据库分库分表
# Golang的数据库分库分表 什么是数据库分库分表 数据库分库分表是指将单一的数据库拆分成多个库,每个库中包含多张表,以提高数据库的性能和可伸缩性。通常在大型应用中,单一的数据库往往无法满足高并发和海量数据的需求,因此需要…...
Docker + Vue2 热重载:为什么需要 CHOKIDAR_USEPOLLING=true?
在 Docker 中运行 Vue 2 项目时,许多开发者会遇到 代码修改后热重载(Hot Reload)失效的问题。虽然 Vue 2 默认支持热重载,但由于 Docker 文件监听机制的特殊性,Webpack 的 watch 机制可能无法正常工作。 本文将深入解析…...
NModbus 连接到Modbus服务器(Modbus TCP)
1、在项目中通过NuGet添加NModbus,在界面中添加一个Button。 using NModbus.Device; using NModbus; using System.Net.Sockets; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Docu…...
基于vue3和flask开发的前后端管理系统(一):项目启动准备
准备工作 我们需要准备以下工具 vue3:构建前端 tailwind css:样式库vite:快速构建vue项目pinia :vue3 的事件管理器 flask:后端代码Mysql:数据库 heidisql:数据库图形化界面 vscode࿱…...
单例模式(线程案例)
单例模式可以分为两种:1.饿汉模式 2.懒汉模式 一.饿汉模式 //饿汉模式👇 class MySingleTon{//因为这是一个静态成员变量,在类加载的时候,就创建了private static MySingleTon mySingleTon new MySingleTon();//创建一个静…...
通过多线程分别获取高分辨率和低分辨率的H264码流
目录 一.RV1126 VI采集摄像头数据并同时获取高分辨率码流和低分辨率码流流程 编辑 1.1初始化VI模块: 1.2初始化RGA模块: 1.3初始化高分辨率VENC编码器、 低分辨率VENC编码器: 1.4 VI绑定高分辨率VENC编码器,VI绑定RGA模块…...
智慧农业中光谱相机对土壤成分的无损检测应用
可浏览之前发布的一篇文章:光谱相机在农业中的具体应用案例 一、土壤成分定量分析 养分检测 光谱相机通过捕捉土壤反射的特定波长光线,可精准检测氮、磷、钾等主要养分含量,以及有机质和水分比例。例如,不同养分对近红外波段…...
Muduo + OpenSSL 网络交互完整流程
🔥 Muduo OpenSSL 网络交互完整流程 这套架构结合了 Muduo(网络库) OpenSSL(TLS/SSL 加密) BIO(缓存),整个数据流动过程如下: 🌍 1. 网络通信的基本流程 M…...
2025年能源工作指导意见重点内容
一、总体目标 能源供应保障 全国发电总装机容量达到36亿千瓦以上,新增新能源发电装机2亿千瓦以上,发电量目标10.6万亿千瓦时,跨省跨区输电能力持续提升。 煤炭稳产增产,原油产量保持2亿吨以上,天然气产量较快增长&am…...
DNS 详细过程 与 ICMP
🌈 个人主页:Zfox_ 🔥 系列专栏:Linux 目录 一:🔥 DNS (Domain Name System) 快速了解🦋 DNS 背景🦋 域名简介🦋 真实地址查询 —— DNS🎀 域名的层级关系&am…...
学到什么记什么(25.3.3)
Upload-labs 今日重新做了一下文件上传漏洞,这里第一题之前采用直接抓包改后缀名.jpg为.php,再写入一句话<?php phpinfo();?>然后放行,得到图片地址(可复制),本来直接访问图片地址即可得到敏感信息…...
阿里云服务器部署项目笔记 实操 centos7.9
阿里云服务器部署项目笔记 实操 centos7.9 springboot vue elementUImysqlredis 相关的redis,mysql,nginx镜像,jdk 通过网盘分享的文件:docker镜像 链接: https://pan.baidu.com/s/15VwcWBP4Jy07xADuvylgQw?pwdm2g9 提取码: m2g9 配置环境 连接云服务器 安装…...
完全背包变体-排列和组合的循环顺序问题
排列,区分顺序:内层循环物品{1,2},可以让3-2->1-1和3-1->2-2都计算一遍。 组合不区分顺序:外层循环物品{1,2},只会按照物品顺序填充 总结:排列问题中,每个容量的状态更新时,允…...
华为飞腾D2000芯片(基于ARM架构)的欧拉操作系统(openEuler)上部署MySQL
一、环境准备 确认系统架构 uname -m # 应输出 aarch64(即ARM64)更新系统 sudo dnf update -y安装基础依赖 sudo dnf install -y libaio numactl openssl-devel tar wget二、安装MySQL 方案1:通过openEuler官方仓库安装(推荐&am…...
C#开发——日期操作类DateTime
在C#中,日期和时间的操作主要通过 System.DateTime 类来实现。 DateTime 提供了丰富的属性和法,用于处理日期和时间的创建、格式化、比较和计算等操作。以下是一些常用的日期函数和特性: 一、创建日期和时间 1、直接指定日期和时间&…...
win32汇编环境,窗口程序中使控件子类化的示例一
;运行效果 ;win32汇编环境,窗口程序中使编辑框控件子类化的示例一 ;窗口子类化,就是把某种控件,自已再打造一遍,加入自已的功能。比如弄个特殊形状的按钮,或只能输入特殊字符的编辑框 ;当然,一般来说,这都是…...
多镜头视频生成、机器人抓取、扩散模型个性化 | Big Model weekly第58期
点击蓝字 关注我们 AI TIME欢迎每一位AI爱好者的加入! 01 GLM-4-Voice: Towards Intelligent and Human-Like End-to-End Spoken Chatbot 本文介绍了一种名为GLM-4-Voice的智能且类人化的端到端语音聊天机器人。它支持中文和英文,能够进行实时语音对话&a…...
iOS实现一个强大的本地状态记录容器
我们开发中经常会遇到这样的场景,就是我们客户端用户进行了某个操作,这个操作影响了数据的状态,但是我们又不方便重新请求一次数据, 这个时候,就需要我们记录一下本地状态在内存中,随着业务越来越复杂&…...
第十四届蓝桥杯:(二分算法)字串简写
这道题我们的做法是开两个vector,分别把a和b字符的下标存进去,然后遍历a字符,我们要求长度必须大于等于k,我们可以画个图,也就是说b的下标减a的下标必须大于等于k-1 也就是b的下标必须大于等于a的下标k-1 我们用二分找…...
制服小程序的“滑手”:禁用页面左右滑动全攻略
哈哈,看来你已经很聪明地发现了小程序中左右滑动的“顽皮”行为!😄 没错,我们可以通过设置 disableScroll 属性来“管教”它,同时结合 CSS 样式让页面既禁得住横向“乱跑”,又能顺畅地上下滚动。你的方案已…...
java 实现xxl-job定时任务自动注册到调度中心
xxl-job 自动注册(执行器和任务) 前言 xxl-job是一个功能强大、简单易用、高可用且可扩展性强的分布式定时任务框架/分布式任务调度平台。它适用于各种需要定时任务调度的场景,并可根据业务需求进行灵活配置和扩展。 xxl-job简介 xxl-job是一个开源的分布式定时任务框架,…...
ZK Rollup
ZK Rollup 通过生成零知识证明来确保所有提交的交易都是有效的。生成零知识证明的过程涉及复杂的密码学运算,通常使用的是 zk-SNARK(零知识简洁非互动知识论证)或 zk-STARK(零知识可扩展透明知识论证)。以下是 ZK Roll…...
webstorm的Live Edit插件配合chrome扩展程序JetBrains IDE Support实现实时预览html效果
前言 我们平时在前端网页修改好代码要点击刷新再去看修改的效果,这样比较麻烦,那么很多软件都提供了实时预览的功能,我们一边编辑代码一边可以看到效果。下面说的是webstorm。 1 Live Edit 首先我们需要在webstorm的settings里安装插件Live …...
02 HarmonyOS Next仪表盘案例详解(一):基础篇
温馨提示:本篇博客的详细代码已发布到 git : https://gitcode.com/nutpi/HarmonyosNext 可以下载运行哦! 文章目录 1. 项目概述2. 技术架构2.1 文件结构2.2 ArkTS 语言特性装饰器的使用 3. 数据结构设计3.1 接口定义3.2 数据初始化 4. 生命周期与页面路由…...
张岳教授:语言模型推理与泛化研究 | ICLR 2025 特邀报告与团队专场
点击蓝字 关注我们 AI TIME欢迎每一位AI爱好者的加入! AITIME 01 ICLR 2025预讲会特邀报告 AITIME 02 ICLR 2025预讲会西湖大学张岳老师实验室专场 01 AI生成文本的自动化检测 Glimpse: Enabling White-Box Methods to Use Proprietary Models for Zero-Shot LLM-Ge…...
离散傅里叶变换(Discrete Fourier Transform, DFT)及其在图像处理中的应用
离散傅里叶变换(DFT)及其在图像处理中的应用 什么是离散傅里叶变换? 离散傅里叶变换(Discrete Fourier Transform, DFT)是一种强大的数学工具,用于将离散信号从时域(或空间域)转换…...
记一次误禁用USB导致键盘鼠标失灵的修复过程
背景说明 在电脑上插入了一个USB hub,然后弹窗提示:“集线器端口上出现电涌”,点开让选择“重置”或者“关闭”,不小心点了关闭,结果这个usb口就被关了,再插任何东西都没反应,找了很多办法都恢…...
Apache nifi demo 实验
Apache nifi 是个数据流系统,可以通过配置 自定义的流程来实现数据的转换。 比如可以配置一个流程,读取数据库里的数据,再转换,最后保存到本地文件。 这样可以来实现一些数据转换的操作,而不用特地编写程序来导入导出。…...
