龙空技术网

Qt QObject

QT教程 269

前言:

而今看官们对“无法解析外部符号qmetaobject”可能比较关注,兄弟们都需要知道一些“无法解析外部符号qmetaobject”的相关知识。那么小编同时在网上汇集了一些关于“无法解析外部符号qmetaobject””的相关内容,希望兄弟们能喜欢,大家一起来了解一下吧!

【1】Qt的QObject

1.测试代码如下:

 1 #include<QApplication> 2 #include<QPushButton> 3 #include<QDebug> 4 using namespace std; 5  6 int main(int argc, char *argv[]) 7 { 8     QApplication app(argc, argv); 9     int nSize = sizeof(QObject);10     qDebug() << nSize << endl; // 811     QPushButton* pQuit = new QPushButton("Quit");12     delete pQuit;13     return app.exec();14 }

QObject是Qt类体系的唯一基类,重要性就像MFC中的CObject或Delphi中的TObject,是Qt各种功能的活水源头。此句代码:

1     int nSize = sizeof(QObject);2     qDebug() << nSize << endl; // 8

QObject的大小是8,除了虚函数表(即所谓的虚表)指针需要4个字节以外,另外的4个字节是指d_ptr(指针成员变量:QObjectData *d_ptr;)

备注:在最新版本中,被替换为QScopedPointer<QObjectData> d_ptr;

那么,QObjectData是个什么鬼?且往下看:

经分析,QObject类的数据成员被封装在QObjectData类中了,为什么要如此封装数据呢?

原因简述:Qt中有一个很重要的设计模式,句柄(方法)—实体(数据)模式,也就是以QObject为基类的类一般都是句柄类,一般会有一个指针指向一个实体类(数据成员类),在实体类中保存全部的数据成员。而且,一般情况下这个指针还是受保护成员变量,方便其子类的使用。因此,也可以说,与句柄类继承关系平行的也有一套实体类派生体系。所以,准确的说,Qt的基类其实有两个:一个是QObject,这是句柄类的唯一基类;另一个是QObjectData,这是实体类的基类。

QObjectData类定义如下:

 1 class Q_CORE_EXPORT QObjectData 2 { 3 public: 4     virtual ~QObjectData() = 0; 5     QObject *q_ptr; 6     QObject *parent; 7     QObjectList children; 8  9     uint isWidget : 1;10     uint blockSig : 1;11     uint wasDeleted : 1;12     uint isDeletingChildren : 1;13     uint sendChildEvents : 1;14     uint receiveChildEvents : 1;15     uint isWindow : 1; //for QWindow16     uint unused : 25;17     int postedEvents;18     QDynamicMetaObjectData *metaObject;19     QMetaObject *dynamicMetaObject() const;20 };

QObject *q_ptr; 这个指针指向此实体类对应的句柄类,此指针与上面的指针QScopedPointer<QObjectData> d_ptr; 遥相呼应,使得句柄类和实体类可以双向的引用,为什么是这样的命名方式呢?可能q指的是Qt接口类,d指的是Data实体类。当然这是猜测,但是或许可以方便你记忆。在Qt中,这两个指针名字是非常重要的,必须记住(嗯哼?为什么重要呢?请看下面两个宏的定义)。

但是,仅仅如此还是不容易使用这两个指针,因为它们都是基类的类型,难道每次使用都要类型转换吗?为了简单起见,Qt在这里声明了两个宏:

 1 template <typename T> 2 static inline T *qGetPtrHelper(T *ptr) { return ptr; } 3  4 #define Q_DECLARE_PRIVATE(Class) \ 5     inline Class##Private* d_func() { return reinterpret_cast<Class##Private *>(qGetPtrHelper(d_ptr)); } \ 6     inline const Class##Private* d_func() const { return reinterpret_cast<const Class##Private *>(qGetPtrHelper(d_ptr)); } \ 7     friend class Class##Private; 8  9 #define Q_DECLARE_PUBLIC(Class)                                    \10     inline Class* q_func() { return static_cast<Class *>(q_ptr); } \11     inline const Class* q_func() const { return static_cast<const Class *>(q_ptr); } \12     friend class Class;

只要在类的头文件中使用这两个宏,就可以通过函数直接得到实体类和句柄类的实际类型了,而且这里还声明了友元,使得实体类和句柄类连访问权限也不用顾忌了。而且为了实体类与句柄类cpp文件中调用的方便,更是直接声明了以下两个宏:

1 #define Q_D(Class) Class##Private * const d = d_func()2 #define Q_Q(Class) Class * const q = q_func()

好了,使用起来倒是方便了,但是以后局部变量可尽量不能声明为d和q了哈。

这里的d_func和q_func函数是极其常用的函数,可以理解为一个是得到实体类,一个是得到Qt句柄类。

QObject *parent; 这里指向QObject的父类

QObjectList children; 这里指向QObject相关的子类列表,这确实是个大胆的设计,如果系统中产生了1000000个QObject实例(对于大的系统,这个数字很容易达到吧),每个QObject子类平均下来是 100(这个数字可能大了),光净这些指针的开销就有1000000 * 100 * 4 = 400M,是够恐怖的,如果我们必须在灵活性和运行开销之间做一个选择的话,无疑Qt选择了前者,对此我也很难评论其中的优劣,还是祈求越来越强的硬件水平和Qt这么多年来得到的赫赫威名保佑我们根本就没有这个问题吧,呵呵~ 总之,Qt确实在内存中保存了所有类实例的树型结构。

1     uint isWidget : 1;2     uint blockSig : 1;3     uint wasDeleted : 1;4     uint isDeletingChildren : 1;5     uint sendChildEvents : 1;6     uint receiveChildEvents : 1;7     uint isWindow : 1; //for QWindow8     uint unused : 25;

这些代码就简单了,主要是一些标记位,为了节省内存开销,这里采用了位域的语法,还保留了25位为unused,留做以后的扩充。

具体还是看一个例子吧!对这种句柄实体模式加深认识,这就是Qt中的按钮类QPushButton,QPushButton的句柄类派生关系以及QPushButtonPrivate的实体类派生关系是:

可以看出,这里确实是一个平行体系,只不过实体类派生关系中多了一个QObjectPrivate,这个类封装了线程处理,信号和槽机制等具体的实现,可以说它才是Qt实体类中真正起作用的基类,而QObjectData不过是一层浅浅的数据封装而已。先不忙了解QObjectPrivate类中的接口和实现,我们先看看在Qt中,句柄类和实体类这两条体系是如何构造的?

QPushButton* pQuit = new QPushButton("Quit"); 创建一个Qt的按钮,简简单单一行代码,其实背后大有玄机,请看如下代码:

 1 QObject::QObject(QObjectPrivate &dd, QObject *parent) 2     : d_ptr(&dd) 3 { 4     Q_D(QObject); 5     d_ptr->q_ptr = this; 6     d->threadData = (parent && !parent->thread()) ? parent->d_func()->threadData : QThreadData::current(); 7     d->threadData->ref(); 8     if (parent) 9     {10         QT_TRY11         {12             if (!check_parent_thread(parent, parent ? parent->d_func()->threadData : 0, d->threadData))13                 parent = 0;14             if (d->isWidget)15             {16                 if (parent)17                 {18                     d->parent = parent;19                     d->parent->d_func()->children.append(this);20                 }21                 // no events sent here, this is done at the end of the QWidget constructor22             }23             else24             {25                 setParent(parent);26             }27         }28         QT_CATCH(...)29         {30             d->threadData->deref();31             QT_RETHROW;32         }33     }34     qt_addObject(this);35 }36 37 QWidget::QWidget(QWidgetPrivate &dd, QWidget* parent, Qt::WindowFlags f)38     : QObject(dd, 0), QPaintDevice()39 {40     Q_D(QWidget);41     QT_TRY42     {43         d->init(parent, f);44     }45     QT_CATCH(...)46     {47         QWidgetExceptionCleaner::cleanup(this, d_func());48         QT_RETHROW;49     }50 }51 52 QAbstractButton::QAbstractButton(QAbstractButtonPrivate &dd, QWidget *parent)53     : QWidget(dd, parent, 0)54 {55     Q_D(QAbstractButton);56     d->init();57 }58 59 QPushButton::QPushButton(const QString &text, QWidget *parent)60     : QAbstractButton(*new QPushButtonPrivate, parent)61 {62     Q_D(QPushButton);63     setText(text);64     d->init();65 }

首先QPushButton的构造函数中调用了QAbstractButton的构造函数,同时马上new出来一个QPushButtonPrivate实体类,然后把对象传递给QAbstractButton

QAbstractButton的构造函数中再调用基类QWidget的构造函数,同时把QPushButtonPrivate实体类对象传给基类

QWidget继续做着同样的事情

终于到了基类QObject,直接把QPushButtonPrivate的指针赋值给了d_ptr(还记得这个变量名称吧)

最终在QPushButton构造时同时产生的new QPushButtonPrivate被写到了QObject中的d_ptr中。

 1 QObject::QObject(QObjectPrivate &dd, QObject *parent) 2     : d_ptr(&dd) 3 { 4     Q_D(QObject); 5     d_ptr->q_ptr = this; 6     d->threadData = (parent && !parent->thread()) ? parent->d_func()->threadData : QThreadData::current(); 7     d->threadData->ref(); 8     if (parent) 9     {10         QT_TRY11         {12             if (!check_parent_thread(parent, parent ? parent->d_func()->threadData : 0, d->threadData))13                 parent = 0;14             if (d->isWidget)15             {16                 if (parent)17                 {18                     d->parent = parent;19                     d->parent->d_func()->children.append(this);20                 }21                 // no events sent here, this is done at the end of the QWidget constructor22             }23             else24             {25                 setParent(parent);26             }27         }28         QT_CATCH(...)29         {30             d->threadData->deref();31             QT_RETHROW;32         }33     }34     qt_addObject(this);35 }

然后执行QObject的构造函数,这里主要是一些线程的处理,先不理它。

 1 QWidget::QWidget(QWidgetPrivate &dd, QWidget* parent, Qt::WindowFlags f) 2     : QObject(dd, 0), QPaintDevice() 3 { 4     Q_D(QWidget); 5     QT_TRY 6     { 7         d->init(parent, f); 8     } 9     QT_CATCH(...)10     {11         QWidgetExceptionCleaner::cleanup(this, d_func());12         QT_RETHROW;13     }14 }

然后是QWidget的构造函数,这里调用了数据类QWidgetPrivate的init函数,此函数不是虚函数,因此静态解析成 QWidgetPrivate的init函数调用。

1 QAbstractButton::QAbstractButton(QAbstractButtonPrivate &dd, QWidget *parent)2     : QWidget(dd, parent, 0)3 {4     Q_D(QAbstractButton);5     d->init();6 }

然后是QAbstractButton的构造函数,这里调用了数据类QAbstractButton的init函数,此函数不是虚函数,因此静态解析成 QAbstractButtonPrivate的init函数调用。

1 QPushButton::QPushButton(const QString &text, QWidget *parent)2     : QAbstractButton(*new QPushButtonPrivate, parent)3 {4     Q_D(QPushButton);5     setText(text);6     d->init();7 }

然后是QPushButton的构造函数,这里调用了实体类QPushButtonPrivate的init函数,此函数不是虚函数,因此静态解析成 QPushButtonPrivate的init函数调用。

现在的事情很清楚了。总结一下:

QPushButton在构造的时候同时生成了QPushButtonPrivate指针,QPushButtonPrivate创建时依次调用实体类基类的构造函数。QPushButton的构造函数中显式的调用基类的构造函数并把QPushButtonPrivate指针传递过去,QPushButton创建时依次调用句柄类基类的构造函数,在句柄类的构造函数中调用了平行实体类的init函数,因为这个函数不是虚函数,因此就是每次调用了对应实体类的init函数。

delete pQuit;

说完了构造,再说说析构

  1 /*!  2     Destroys the object, deleting all its child objects.  3   4     All signals to and from the object are automatically disconnected, and  5     any pending posted events for the object are removed from the event  6     queue. However, it is often safer to use deleteLater() rather than  7     deleting a QObject subclass directly.  8   9     \warning All child objects are deleted. If any of these objects 10     are on the stack or global, sooner or later your program will 11     crash. We do not recommend holding pointers to child objects from 12     outside the parent. If you still do, the destroyed() signal gives 13     you an opportunity to detect when an object is destroyed. 14  15     \warning Deleting a QObject while pending events are waiting to 16     be delivered can cause a crash. You must not delete the QObject 17     directly if it exists in a different thread than the one currently 18     executing. Use deleteLater() instead, which will cause the event 19     loop to delete the object after all pending events have been 20     delivered to it. 21  22     \sa deleteLater() 23 */ 24 QObject::~QObject() 25 { 26     Q_D(QObject); 27     d->wasDeleted = true; 28     d->blockSig = 0; // unblock signals so we always emit destroyed() 29  30     QtSharedPointer::ExternalRefCountData *sharedRefcount = d->sharedRefcount.load(); 31     if (sharedRefcount) 32     { 33         if (sharedRefcount->strongref.load() > 0) 34         { 35             qWarning("QObject: shared QObject was deleted directly. The program is malformed and may crash."); 36             // but continue deleting, it's too late to stop anyway 37         } 38  39         // indicate to all QWeakPointers that this QObject has now been deleted 40         sharedRefcount->strongref.store(0); 41         if (!sharedRefcount->weakref.deref()) 42             delete sharedRefcount; 43     } 44  45     if (!d->isWidget && d->isSignalConnected(0)) 46     { 47         QT_TRY 48         { 49             emit destroyed(this); 50         } 51         QT_CATCH(...) 52         { 53             // all the signal/slots connections are still in place - if we don't 54             // quit now, we will crash pretty soon. 55             qWarning("Detected an unexpected exception in ~QObject while emitting destroyed()."); 56             QT_RETHROW; 57         } 58     } 59  60     if (d->declarativeData) 61     { 62         if (static_cast<QAbstractDeclarativeDataImpl*>(d->declarativeData)->ownedByQml1) 63         { 64             if (QAbstractDeclarativeData::destroyed_qml1) 65                 QAbstractDeclarativeData::destroyed_qml1(d->declarativeData, this); 66         } 67         else 68         { 69             if (QAbstractDeclarativeData::destroyed) 70                 QAbstractDeclarativeData::destroyed(d->declarativeData, this); 71         } 72     } 73  74     // set ref to zero to indicate that this object has been deleted 75     if (d->currentSender != 0) 76         d->currentSender->ref = 0; 77     d->currentSender = 0; 78  79     if (d->connectionLists || d->senders) 80     { 81         QMutex *signalSlotMutex = signalSlotLock(this); 82         QMutexLocker locker(signalSlotMutex); 83  84         // disconnect all receivers 85         if (d->connectionLists) 86         { 87             ++d->connectionLists->inUse; 88             int connectionListsCount = d->connectionLists->count(); 89             for (int signal = -1; signal < connectionListsCount; ++signal) 90             { 91                 QObjectPrivate::ConnectionList &connectionList = 92                     (*d->connectionLists)[signal]; 93  94                 while (QObjectPrivate::Connection *c = connectionList.first) 95                 { 96                     if (!c->receiver) 97                     { 98                         connectionList.first = c->nextConnectionList; 99                         c->deref();100                         continue;101                     }102 103                     QMutex *m = signalSlotLock(c->receiver);104                     bool needToUnlock = QOrderedMutexLocker::relock(signalSlotMutex, m);105 106                     if (c->receiver)107                     {108                         *c->prev = c->next;109                         if (c->next) c->next->prev = c->prev;110                     }111                     c->receiver = 0;112                     if (needToUnlock)113                         m->unlock();114 115                     connectionList.first = c->nextConnectionList;116 117                     // The destroy operation must happen outside the lock118                     if (c->isSlotObject)119                     {120                         c->isSlotObject = false;121                         locker.unlock();122                         c->slotObj->destroyIfLastRef();123                         locker.relock();124                     }125                     c->deref();126                 }127             }128 129             if (!--d->connectionLists->inUse)130             {131                 delete d->connectionLists;132             }133             else134             {135                 d->connectionLists->orphaned = true;136             }137             d->connectionLists = 0;138         }139 140         /* Disconnect all senders:141          * This loop basically just does142          *     for (node = d->senders; node; node = node->next) { ... }143          *144          * We need to temporarily unlock the receiver mutex to destroy the functors or to lock the145          * sender's mutex. And when the mutex is released, node->next might be destroyed by another146          * thread. That's why we set node->prev to &node, that way, if node is destroyed, node will147          * be updated.148          */149         QObjectPrivate::Connection *node = d->senders;150         while (node)151         {152             QObject *sender = node->sender;153             // Send disconnectNotify before removing the connection from sender's connection list.154             // This ensures any eventual destructor of sender will block on getting receiver's lock155             // and not finish until we release it.156             sender->disconnectNotify(QMetaObjectPrivate::signal(sender->metaObject(), node->signal_index));157             QMutex *m = signalSlotLock(sender);158             node->prev = &node;159             bool needToUnlock = QOrderedMutexLocker::relock(signalSlotMutex, m);160             //the node has maybe been removed while the mutex was unlocked in relock?161             if (!node || node->sender != sender)162             {163                 // We hold the wrong mutex164                 Q_ASSERT(needToUnlock);165                 m->unlock();166                 continue;167             }168             node->receiver = 0;169             QObjectConnectionListVector *senderLists = sender->d_func()->connectionLists;170             if (senderLists)171                 senderLists->dirty = true;172 173             QtPrivate::QSlotObjectBase *slotObj = Q_NULLPTR;174             if (node->isSlotObject)175             {176                 slotObj = node->slotObj;177                 node->isSlotObject = false;178             }179 180             node = node->next;181             if (needToUnlock)182                 m->unlock();183 184             if (slotObj)185             {186                 if (node)187                     node->prev = &node;188                 locker.unlock();189                 slotObj->destroyIfLastRef();190                 locker.relock();191             }192         }193     }194 195     if (!d->children.isEmpty())196         d->deleteChildren();197 198     qt_removeObject(this);199 200     if (d->parent)        // remove it from parent object201         d->setParent_helper(0);202 }203 204 /*!205     Destroys the widget.206 207     All this widget's children are deleted first. The application208     exits if this widget is the main widget.209 */210 QWidget::~QWidget()211 {212     Q_D(QWidget);213     d->data.in_destructor = true;214 215 #if defined (QT_CHECK_STATE)216     if (paintingActive())217         qWarning("QWidget: %s (%s) deleted while being painted", className(), name());218 #endif219 220 #ifndef QT_NO_GESTURES221     foreach (Qt::GestureType type, d->gestureContext.keys())222         ungrabGesture(type);223 #endif224 225     // force acceptDrops false before winId is destroyed.226     d->registerDropSite(false);227 228 #ifndef QT_NO_ACTION229     // remove all actions from this widget230     for (int i = 0; i < d->actions.size(); ++i)231     {232         QActionPrivate *apriv = d->actions.at(i)->d_func();233         apriv->widgets.removeAll(this);234     }235     d->actions.clear();236 #endif237 238 #ifndef QT_NO_SHORTCUT239     // Remove all shortcuts grabbed by this240     // widget, unless application is closing241     if (!QApplicationPrivate::is_app_closing && testAttribute(Qt::WA_GrabbedShortcut))242         qApp->d_func()->shortcutMap.removeShortcut(0, this, QKeySequence());243 #endif244 245     // delete layout while we still are a valid widget246     delete d->layout;247     d->layout = 0;248     // Remove myself from focus list249 250     Q_ASSERT(d->focus_next->d_func()->focus_prev == this);251     Q_ASSERT(d->focus_prev->d_func()->focus_next == this);252 253     if (d->focus_next != this)254     {255         d->focus_next->d_func()->focus_prev = d->focus_prev;256         d->focus_prev->d_func()->focus_next = d->focus_next;257         d->focus_next = d->focus_prev = 0;258     }259 260     QT_TRY261     {262 #ifndef QT_NO_GRAPHICSVIEW263         const QWidget* w = this;264         while (w->d_func()->extra && w->d_func()->extra->focus_proxy)265             w = w->d_func()->extra->focus_proxy;266         QWidget *window = w->window();267         QWExtra *e = window ? window->d_func()->extra : 0;268         if (!e || !e->proxyWidget || (w->parentWidget() && w->parentWidget()->d_func()->focus_child == this))269 #endif270         clearFocus();271     }272     QT_CATCH(...)273     {274         // swallow this problem because we are in a destructor275     }276 277     d->setDirtyOpaqueRegion();278 279     if (isWindow() && isVisible() && internalWinId())280     {281         QT_TRY282         {283             d->close_helper(QWidgetPrivate::CloseNoEvent);284         }285         QT_CATCH(...)286         {287             // if we're out of memory, at least hide the window.288             QT_TRY289             {290                 hide();291             }292             QT_CATCH(...)293             {294                 // and if that also doesn't work, then give up295             }296         }297     }298 299 #if defined(Q_WS_WIN) || defined(Q_WS_X11)|| defined(Q_WS_MAC)300     else if (!internalWinId() && isVisible())301     {302         qApp->d_func()->sendSyntheticEnterLeave(this);303     }304 #endif305     else if (isVisible())306     {307         qApp->d_func()->sendSyntheticEnterLeave(this);308     }309 310     if (QWidgetBackingStore *bs = d->maybeBackingStore())311     {312         bs->removeDirtyWidget(this);313         if (testAttribute(Qt::WA_StaticContents))314             bs->removeStaticWidget(this);315     }316 317     delete d->needsFlush;318     d->needsFlush = 0;319 320     // The next 20 lines are duplicated from QObject, but required here321     // since QWidget deletes is children itself322     bool blocked = d->blockSig;323     d->blockSig = 0; // unblock signals so we always emit destroyed()324 325     if (d->isSignalConnected(0))326     {327         QT_TRY328         {329             emit destroyed(this);330         }331         QT_CATCH(...)332         {333             // all the signal/slots connections are still in place - if we don't334             // quit now, we will crash pretty soon.335             qWarning("Detected an unexpected exception in ~QWidget while emitting destroyed().");336             QT_RETHROW;337         }338     }339 340     if (d->declarativeData)341     {342         if (QAbstractDeclarativeData::destroyed)343             QAbstractDeclarativeData::destroyed(d->declarativeData, this);344         if (QAbstractDeclarativeData::destroyed_qml1)345             QAbstractDeclarativeData::destroyed_qml1(d->declarativeData, this);346         d->declarativeData = 0;                 // don't activate again in ~QObject347     }348 349     d->blockSig = blocked;350 351 #ifdef Q_WS_MAC352     // QCocoaView holds a pointer back to this widget. Clear it now353     // to make sure it's not followed later on. The lifetime of the354     // QCocoaView might exceed the lifetime of this widget in cases355     // where Cocoa itself holds references to it.356     extern void qt_mac_clearCocoaViewQWidgetPointers(QWidget *);357     qt_mac_clearCocoaViewQWidgetPointers(this);358 #endif359 360     if (!d->children.isEmpty())361         d->deleteChildren();362 363     QApplication::removePostedEvents(this);364 365     QT_TRY366     {367         destroy();                                        // platform-dependent cleanup368     }369     QT_CATCH(...)370     {371         // if this fails we can't do anything about it but at least we are not allowed to throw.372     }373     --QWidgetPrivate::instanceCounter;374 375     if (QWidgetPrivate::allWidgets) // might have been deleted by ~QApplication376         QWidgetPrivate::allWidgets->remove(this);377 378     QT_TRY379     {380         QEvent e(QEvent::Destroy);381         QCoreApplication::sendEvent(this, &e);382     }383     QT_CATCH(const std::exception&)384     {385         // if this fails we can't do anything about it but at least we are not allowed to throw.386     }387 }388 389 /*!390     Destroys the button.391  */392  QAbstractButton::~QAbstractButton()393 {394 #ifndef QT_NO_BUTTONGROUP395     Q_D(QAbstractButton);396     if (d->group)397         d->group->removeButton(this);398 #endif399 }400 401 /*!402     Destroys the push button.403 */404 QPushButton::~QPushButton()405 {406 }

这里当然会调用QPushButton的析构函数

然后,QAbstractButton的析构函数

然后,QWidget的析构函数,这里洋洋洒洒一大堆代码,先不管它

最后,QObject的析构函数,这里也是洋洋洒洒的一大堆,主要做了一下几件事:

(1)设一个wasDeleted的标志,防止再被引用,对于单线程情况下,马上就要被删除了,还搞什么标记啊,根本没用,但是对于多线程情况下,这个标记应该是有用的

(2)Qt的一个指针删除时要发送destroyed信号,一般情况下是没有槽来响应的

(3)清除了信号槽机制中的记录

(4)清除定时器

(5)清除事件过滤机制

(6)清除所有子类指针,当然每个子类指针清除时又会清除它的所有子类,因此Qt中new出来的指针很少有显式对应的delete,因为只要最上面的指针被框架删除了,它所连带的所有子类都被自动删除了

(7)删除相关的数据类指针

【2】对象数据存储

前言,为什么先说这个?

我们知道,在C++中,几乎每一个类(class)中都需要有类的一些成员变量(class member variable),在通常情况下的做法如下:

1 class Person2 {3 private:4     QString m_szName; // 姓名5     bool m_bSex;      // 性别6     int m_nAge;       // 年龄7 };

就是在类定义的时候,直接把类成员变量定义在内面,甚至于把这些成员变量的存取方法直接定义成是public的,您是不是也是这样做过呢?

在QT中,却几乎都不是这样做的!那么,QT是怎么做的呢?几乎每一个C++的类中都会保存许多的数据,要想读懂别人写的C++代码,就一定需要知道每一个类的的数据是如何存储的?是什么含义?否则,我们不可能读懂别人的C++代码。在这里也就是说,要想读懂QT的代码,第一步就必须先搞清楚QT的类成员数据是如何保存的。

为了更容易理解QT是如何定义类成员变量的,我们先说一下QT 2.x 版本中的类成员变量定义方法,因为在 2.x 中的方法非常容易理解。然后再介绍 QT 4.4 中的类成员变量定义方法。

2.1 QT 2.x 中的方法

在定义class的时候(在.h文件中),只包含有一个类成员变量,只是定义一个成员数据指针,然后由这个指针指向一个数据成员对象,这个数据成员对象包含所有这个class的成员数据,然后在class的实现文件(.cpp文件)中,定义这个私有数据成员对象。示例代码如下:

 1 // File name:  person.h 2 struct PersonalDataPrivate; // 声明私有数据成员类型 3 class Person 4 { 5 public: 6     Person ();   // constructor 7     virtual ~Person ();  // destructor 8     void setAge(const int); 9     int getAge();10 11 private:12     PersonalDataPrivate* d;13 };14 15 // File name:  person.cpp16 struct PersonalDataPrivate  // 定义私有数据成员类型17 {18     string m_szName; // 姓名19     bool m_bSex;     // 性别20     int m_nAge;      // 年龄21 };22 23 // constructor24 Person::Person ()25 {26     d = new PersonalDataPrivate;27 }28 29 // destructor30 Person::~Person ()31 {32     delete d;33 }34 35 void Person::setAge(const int age)36 {37     if (age != d->m_nAge)38     {39         d->m_nAge = age;40     }41 }42 43 int Person::getAge()44 {45     return d->m_nAge;46 }

在最初学习QT的时候,我也觉得这种方法很麻烦,但是随着使用的增多,我开始很喜欢这个方法了。而且,现在写的代码,基本上都会用这种方法。具体说来,它有如下优点:

减少头文件的依赖性。把具体的数据成员都放到cpp文件中去,这样,在需要修改数据成员的时候,只需要改cpp文件而不需要头文件,这样就可以避免一次因为头文件的修改而导致所有包含了这个文件的文件全部重新编译一次,尤其是当这个头文件是非常底层的头文件和项目非常庞大的时候,优势明显。同时,也减少了这个头文件对其它头文件的依赖性。可以把只在数据成员中需要用到的在cpp文件中include一次就可以,在头文件中就可以尽可能的减少include语句。增强类的封装性。这种方法增强了类的封装性,无法再直接存取类成员变量,而必须写相应的 get/set 成员函数来做这些事情。 关于这个问题,仁者见仁,智者见智,每个人都有不同的观点。有些人就是 喜欢把类成员变量都定义成public的,在使用的时候方便。只是我个人不喜欢这种方法,当项目变得很大的时候,有非常多的人一起在做这个项目的时候,自己所写的代码处于底层有非常多的人需要使用(#include)的时候,这个方法的弊端就充分的体现出来了。

还有,我不喜欢 QT 2.x 中把数据成员的变量名都定义成只有一个字母d,看起来很不直观,尤其是在search的时候,很不方便。但是,QT kernel 中的确就是这么干的。

那么,在最新的 QT4 里面是如何实现的呢?请关注下一节。

2.2 QT 4.4.x 中的方法

在QT4.4中,类成员变量定义方法的出发点没有变化,只是在具体的实现手段上发生了非常大的变化,下面具体来看。

在QT4.4中,使用了非常多的宏来做事,这无疑增加了理解 QT source code的难度,不知道他们是不是从MFC学来的。就连在定义类成员数据变量这件事情上,也大量的使用了宏。

在这个版本中,类成员变量不再是给每一个class都定义一个私有的成员,而是把这一项common的工作放到了最基础的基类 QObject 中,然后定义了一些相关的方法来存取。

 1 // file name: qobject.h 2 class QObjectData 3 { 4 public: 5     virtual ~QObjectData() = 0; 6     // 省略 7 }; 8  9 class QObject10 {11     Q_DECLARE_PRIVATE(QObject)12 public:13     QObject(QObject *parent = 0);14 15 protected:16     QObject(QObjectPrivate &dd,QObject *parent = 0);17 18 protected:19     QObjectData *d_ptr;20 }

这些代码就是在 qobject.h 这个头文件中的。

从句柄类QObject的定义中,可以看到,数据成员的定义为:QObjectData *d_ptr;

之所以定义成 protected 类型,就是要让所有的派生类都可以存取这个变量,而在外部却不可以直接存取这个变量。

而QObjectData的定义却放在了这个头文件中,其目的就是为了要所有从QObject继承出来的类的成员变量也都相应的要在QObjectData这个class继承出来。

而纯虚的析构函数又决定了两件事:

这个class不能直接被实例化。换句话说就是,如果你写了这么一行代码:new QObjectData,这行代码一定会出错,compile的时候是无法通过的。当delete这个指针变量的时候,这个指针变量是指向的任意从 QObjectData 继承出来的对象的时候,这个对象都能被正确delete,而不会产生错误(诸如,内存泄漏之类的)。

我们再来看看这个宏做了什么,Q_DECLARE_PRIVATE(QObject)

1 #define Q_DECLARE_PRIVATE(Class) \2     inline Class##Private* d_func() { return reinterpret_cast<Class##Private *>(d_ptr); } \3     inline const Class##Private* d_func() const { return reinterpret_cast<const Class##Private *>(d_ptr); } \4     friend class Class##Private;

这个宏主要是定义了两个重载的函数d_func()。作用就是把在QObject这个class中定义的数据成员变量d_ptr安全的转换成为每一个具体的class的数据成员类型(实体类)指针。

来,我们看一下在QObject这个class中,这个宏展开之后的情况,就一幕了然了。

将 Q_DECLARE_PRIVATE(QObject) 展开后,就是下面的代码:

 1 inline QObjectPrivate* d_func() 2 { 3     return reinterpret_cast<QObjectPrivate *>(d_ptr); 4 } 5  6 inline const QObjectPrivate* d_func() const 7 { 8     return reinterpret_cast<const QObjectPrivate *>(d_ptr); 9 }10 11 friend class QObjectPrivate;

宏展开之后,新的问题又来了,这个 QObjectPrivate 是从哪里来的?在QObject这个类中,为什么不直接使用 QObjectPrivate 来作为数据成员变量的类型呢?还记得我们刚才说过吗,QObjectData这个类的析构函数是纯虚函数,也就意味着这个类是不能实例化的。所以,QObject这个类的数据成员变量的实际类型是从QObjectData继承出来的,它就是QObjectPrivate。

这个类中保存了许多非常重要而且有趣的东西,其中包括 QT 最核心的 signal 和 slot 的数据、属性数据等等,我们将会在后面详细讲解,现在我们来看一下它的定义:

下面就是这个类的定义:

1 class QObjectPrivate : public QObjectData2 {3     Q_DECLARE_PUBLIC(QObject)4 public:5     QObjectPrivate(int version = QObjectPrivateVersion);6     virtual ~QObjectPrivate();7     // 省略8 };

那么,这个 QObjectPrivate 和 QObject 是什么关系呢?他们是如何关联在一起的呢?

接上节,让我们来看看这个 QObjectPrivate 和 QObject 是如何关联在一起的。

 1 // file name: qobject.cpp 2 QObject::QObject(QObject *parent) 3  4     : d_ptr(new QObjectPrivate) 5  6 { 7     // ……………………… 8 } 9 10 QObject::QObject(QObjectPrivate &dd, QObject *parent)11     : d_ptr(&dd)12 {13     // …………………14 }

怎么样,是不是 一目了然呀?

从第一个构造函数可以很清楚的看出来,QObject类中的 d_ptr 指针将指向一个 QObjectPrivate 的对象,而QObjectPrivate这个类是从QObjectData继承出来的。

这第二个构造函数干什么用的呢?从 QObject类的定义中,我们可以看到,第二个构造函数是被定义为 protected 类型的,即说明,这个构造函数只能被继承的类(子类)使用,(应用端)不能使用这个构造函数来直接构造一个QObject对象。也就是说,如果你写一条下面的语句,编译的时候是会失败的:

1 new QObject(*new QObjectPrivate, NULL)

为了看的更清楚,我们以QWidget类为例说明。

QWidget是QT中所有UI控件的基类,它直接从QObject继承而来:

 1 QObject::QObject(QObjectPrivate &dd, QObject *parent) 2     : d_ptr(&dd) 3 { 4     // ………………… 5 } 6  7 class QWidget : public QObject, public QPaintDevice  8 { 9     Q_OBJECT10     Q_DECLARE_PRIVATE(QWidget)11     // ..................... 12 }

我们看一个这个类的构造函数的代码:

1 QWidget::QWidget(QWidget *parent, Qt::WindowFlags f)2     : QObject(*new QWidgetPrivate, 0)3     , QPaintDevice()4 {5     d_func()->init(parent, f); 6 }

非常清楚,它调用了基类QObject的保护类型的构造函数,并且以 *new QWidgetPrivate 作为第一个参数传递进去。也就是说,基类QObject中的d_ptr指针将会指向一个QWidgetPrivate类型的对象。再看QWidgetPrivate这个类的定义:

1 class QWidgetPrivate : public QObjectPrivate2 {3     Q_DECLARE_PUBLIC(QWidget)4     // ..................... 5 };

好了,这就把所有的事情都串联起来了。

关于QWidget构造函数中的唯一的语句 d_func()->init(parent, f) 我们注意到在类的定义中有这么一句话: Q_DECLARE_PRIVATE(QWidget)

我们前面提到这个宏,当把这个宏展开之后,就是这样的:

1 #define Q_DECLARE_PRIVATE(Class) \2     inline Class##Private* d_func() { return reinterpret_cast<Class##Private *>(d_ptr); } \3     inline const Class##Private* d_func() const { return reinterpret_cast<const Class##Private *>(d_ptr); } \4     friend class Class##Private;

很清楚,它就是把QObject中定义的d_ptr指针转换为QWidgetPrivate类型的指针。

小 结:

要理解QT Kernel的code,就必须要知道QT中每一个对象内部的数据是如何保存的,而QT没有像我们平时写代码一样,把所有的变量直接定义在类中,所以,不搞清楚这个问题,我们就无法理解一个相应的类。其实,在QT4.4中的类成员数据的保存方法在本质是与QT2.x中的是一样的,就是在类中定义一个成员数据的指针,指向成员数据集合对象(这里是一个QObjectData或者是其派生类)。初始化这个成员变量的办法是定义一个保护类型的构造函数,然后在派生类的构造函数new一个派生类的数据成员,并将这个新对象赋值给QObject的数据指针。在使用的时候,通过预先定义个宏里面的一个inline函数来把数据指针再安全类型转换,就可以使用了。

【3】Qt对象之间的父子关系

很多C/C++初学者常犯的一个错误就是,使用malloc、new分配了一块内存却忘记释放,导致内存泄漏。

Qt的对象模型提供了一种Qt对象之间的父子关系,当很多个对象都按一定次序建立起来这种父子关系的时候,就组织成了一颗树。当delete一个父对象的时候,Qt的对象模型机制保证了会自动的把它的所有子对象,以及孙对象等等全部delete,从而保证不会有内存泄漏的情况发生。

任何事情都有正反两面作用,这种机制看上去挺好,但是却会对很多Qt的初学者造成困扰,我经常给别人回答的问题是:

1:new了一个Qt对象之后,在什么情况下应该delete它?

2:Qt的析构函数是不是有bug?

3:为什么正常delete一个Qt对象却会产生segment fault?

等等诸如此类的问题,这篇文章就是针对这个问题的详细解释。 在每一个Qt对象中,都有一个链表,这个链表保存有它所有子对象的指针。当创建一个新的Qt对象的时候,如果把另外一个Qt对象指定为这个对象的父对象, 那么父对象就会在它的子对象链表中加入这个子对象的指针。另外,对于任意一个Qt对象而言,在其生命周期的任何时候,都还可以通过setParent函数重新设置它的父对象。当一个父对象在被delete的时候,它会自动的把它所有的子对象全部delete。当一个子对象在delete的时候,会把它自己从它的父对象的子对象链表中删除。

QWidget是所有在屏幕上显示出来的界面对象的基类,它扩展了Qt对象的父子关系。一个Widget对象也就自然的成为其父Widget对象的子Widget,并且显示在它的父Widget的坐标系统中。例如,一个对话框(QDialog)上的按钮(QButton)应该是这个对话框的子Widget。 关于Qt对象的new和delete,下面我们举例说明:

例如,下面这一段代码是正确的:

1 int main()2 {3     QObject* pObjParent = new QObject(NULL);4     QObject* pObjChild1 = new QObject(pObjParent);5     QObject* pObjChild2 = new QObject(pObjParent);6     delete pObjParent;7 }

如果我们把上面这段代码改成这样,也是正确的:

1 int main()2 {3     QObject* pObjParent = new QObject(NULL);4     QObject* pObjChild1 = new QObject(pObjParent);5     QObject* pObjChild2 = new QObject(pObjParent);6     delete pObjChild1;7     delete pObjParent;8 }

在这段代码中,我们就对比一下和上一段代码不一样的地方,就是在delete pObjParent对象之前,先delete pObjChild1对象。在delete pObjChild1对象的时候,pObjChild1对象会自动的把自己从pObjParent对象的子对象链表中删除,也就是说,在pObjChild1对象被delete完成之后,pObjParent对象就只有一个子对象(pObjChild2)了。然后在delete pObjParent对象的时候,会自动把pObjChild2对象也delete。所以,这段代码也是安全的。(备注:Qt的这种设计对某些调试工具来说却是不友好的,比如valgrind。比如上面这段代码,valgrind工具在分析代码的时候,就会认为 objChild2对象没有被正确的delete,从而会报告说,这段代码存在内存泄漏。哈哈,我们知道,这个报告是不对的。)我们再看一看这一段代码:

1 int main()2 {3     QWidget window;4     QPushButton quit("Exit", &window);5 }

在这段代码中,我们创建了两个widget对象:第一个是window;第二个是quit。他们都是Qt对象,因为QPushButton是从 QWidget派生出来的,而QWidget是从QObject派生出来的。这两个对象之间的关系:window对象是quit对象的父对象,由于他们都会被分配在栈(stack)上面,那么quit对象是不是会被析构两次呢?我们知道,在一个函数体内部声明的变量,在这个函数退出的时候就会被析构,那么在这段代码中,window和quit两个对象在函数退出的时候析构函数都会被调用。那么假设,如果是window的析构函数先被调用的话,它就会去delete quit对象;然后quit的析构函数再次被调用,程序就出错了。事实情况不是这样的,C++标准规定,本地对象的析构函数的调用顺序与他们的构造顺序相反。那么在这段代码中,这就是quit对象的析构函数一定会比window对象的析构函数先被调用,所以,在window对象析构的时候,quit对象已经不存在了,不会被析构两次。 如果我们把代码改成这个样子,就会出错了,对照前面的解释,请你自己来分析一下吧。

1 int main() 2 { 3     QPushButton quit("Exit"); 4     QWidget window; 5     quit.setParent(&window); 6 }

构建无误,运行崩溃,提示如下:

我们自己在写程序的时候,也必须重点注意一项,千万不要delete子对象两次,就像前面这段代码那样,程序肯定就crash了。

【领QT开发教程学习资料,点击下方链接莬费领取↓↓,先码住不迷路~】

点击这里:「链接」

原文作者: kaizenly

原文链接:

标签: #无法解析外部符号qmetaobject