物件樹管理
個人經驗總結,如有錯誤或遺漏,歡迎各位大佬指正 ?
@
設定父物件的作用
眾所周知,Qt中,有為物件設定父物件的方法——setParent
。
而設定父物件的作用主要有,在父物件析構的時候,會自動去析構其子物件。如果是一個視窗物件,如果其父物件設定了樣式表(Style Sheet),子物件也會繼承父物件的樣式。
所以,這篇文章,我們們主要看一下setParent
的原始碼以及QObject
是怎麼進行物件管理的。
設定父物件(setParent)
我們可以看到,setParent
這個函式就是呼叫了QObjectPrivate
類的setParent_helper
這個函式。
void QObject::setParent(QObject *parent)
{
Q_D(QObject);
Q_ASSERT(!d->isWidget);
d->setParent_helper(parent);
}
所以,我們進一步分析setParent_helper
這個函式
完整原始碼
void QObjectPrivate::setParent_helper(QObject *o)
{
Q_Q(QObject);
// 不能把自己設為父物件
Q_ASSERT_X(q != o, Q_FUNC_INFO, "Cannot parent a QObject to itself");
#ifdef QT_DEBUG
// 檢查物件樹的迴圈
const auto checkForParentChildLoops = qScopeGuard([&](){
int depth = 0;
auto p = parent;
while (p) {
if (++depth == CheckForParentChildLoopsWarnDepth) {
qWarning("QObject %p (class: '%s', object name: '%s') may have a loop in its parent-child chain; "
"this is undefined behavior",
q, q->metaObject()->className(), qPrintable(q->objectName()));
}
p = p->parent();
}
});
#endif
// 如果要設定的父物件就是當前的父物件,直接返回
if (o == parent)
return;
if (parent) {
QObjectPrivate *parentD = parent->d_func();
if (parentD->isDeletingChildren && wasDeleted
&& parentD->currentChildBeingDeleted == q) {
// don't do anything since QObjectPrivate::deleteChildren() already
// cleared our entry in parentD->children.
} else {
const int index = parentD->children.indexOf(q);
if (index < 0) {
// we're probably recursing into setParent() from a ChildRemoved event, don't do anything
} else if (parentD->isDeletingChildren) {
parentD->children[index] = 0;
} else {
// 如果物件已經存在父物件的列表中,將原先存在的物件刪除,併傳送事件
parentD->children.removeAt(index);
if (sendChildEvents && parentD->receiveChildEvents) {
QChildEvent e(QEvent::ChildRemoved, q);
QCoreApplication::sendEvent(parent, &e);
}
}
}
}
// 設定父物件
parent = o;
if (parent) {
// object hierarchies are constrained to a single thread
if (threadData != parent->d_func()->threadData) {
qWarning("QObject::setParent: Cannot set parent, new parent is in a different thread");
parent = nullptr;
return;
}
// 父物件新增子物件,併傳送事件
parent->d_func()->children.append(q);
if(sendChildEvents && parent->d_func()->receiveChildEvents) {
if (!isWidget) {
QChildEvent e(QEvent::ChildAdded, q);
QCoreApplication::sendEvent(parent, &e);
}
}
}
if (!wasDeleted && !isDeletingChildren && declarativeData && QAbstractDeclarativeData::parentChanged)
QAbstractDeclarativeData::parentChanged(declarativeData, q, o);
}
片段分析
-
一些先決條件的判斷
-
判斷設定的父物件是否是自己
// 不能把自己設為父物件 Q_ASSERT_X(q != o, Q_FUNC_INFO, "Cannot parent a QObject to itself"); /*...*/ // 如果要設定的父物件就是當前的父物件,直接返回 if (o == parent) return;
-
判斷原來的父物件是否處於正在刪除子物件的過程中,並且當前物件已經被刪除了,如果是,則什麼都不做(有點迷惑)
if (parentD->isDeletingChildren && wasDeleted && parentD->currentChildBeingDeleted == q) { // don't do anything since QObjectPrivate::deleteChildren() //already cleared our entry in parentD->children. }
-
判斷是不是透過從
ChildRemoved
事件遞迴到setParent()
if (index < 0) { // we're probably recursing into setParent() from a ChildRemoved event, // don't do anything } else if (parentD->isDeletingChildren) { parentD->children[index] = 0; }
-
判斷物件是不是已存在父物件的列表中,如果存在,就將物件刪除,併傳送事件
else { // 如果物件已經存在父物件的列表中,將原先存在的物件刪除,併傳送事件 parentD->children.removeAt(index); if (sendChildEvents && parentD->receiveChildEvents) { QChildEvent e(QEvent::ChildRemoved, q); QCoreApplication::sendEvent(parent, &e); } }
-
-
設定父物件,這裡有一個限制,就是新設定的父物件,必須和當前物件在同一個執行緒,否則不能設定。
// 設定父物件 parent = o; if (parent) { // object hierarchies are constrained to a single thread if (threadData != parent->d_func()->threadData) { qWarning("QObject::setParent: Cannot set parent, \ new parent is in a different thread"); parent = nullptr; return; } // 父物件新增子物件,併傳送事件 parent->d_func()->children.append(q); if(sendChildEvents && parent->d_func()->receiveChildEvents) { if (!isWidget) { QChildEvent e(QEvent::ChildAdded, q); QCoreApplication::sendEvent(parent, &e); } } }
物件的刪除
然後就是物件的管理,也就是在父物件析構的時候,自動析構掉所有的子物件。這一個在我們使用視窗部件的時候很有用,因為一個介面可能有很多個子控制元件,比如按鈕、label等,這時候,如果一個小視窗被關閉,我們也不需要一個一個的去析構,由Qt的物件樹去進行析構就好了。
QObject::~QObject()
{
/*...*/
// 刪除子物件
if (!d->children.isEmpty())
d->deleteChildren();
#if QT_VERSION < 0x60000
qt_removeObject(this);
#endif
if (Q_UNLIKELY(qtHookData[QHooks::RemoveQObject]))
reinterpret_cast<QHooks::RemoveQObjectCallback>(qtHookData[QHooks::RemoveQObject])(this);
Q_TRACE(QObject_dtor, this);
if (d->parent) // remove it from parent object
d->setParent_helper(nullptr);
}
將所有的子物件進行刪除,遍歷容器,按照子物件所加入進來的順序進行析構。
void QObjectPrivate::deleteChildren()
{
// 清空子物件
Q_ASSERT_X(!isDeletingChildren, "QObjectPrivate::deleteChildren()", "isDeletingChildren already set, did this function recurse?");
isDeletingChildren = true;
// delete children objects
// don't use qDeleteAll as the destructor of the child might
// delete siblings
for (int i = 0; i < children.count(); ++i) {
currentChildBeingDeleted = children.at(i);
children[i] = 0;
delete currentChildBeingDeleted;
}
children.clear();
currentChildBeingDeleted = nullptr;
isDeletingChildren = false;
}
夾帶私貨時間
在使用Qt的物件樹這個功能的時候,可能會遇到一種問題,會導致程式崩潰:就是手動的管理(也就是直接delete)一個有父物件的QObject
,為什麼會出現這樣的情況呢,因為,你在delete子物件之後,並沒有把這個物件從父物件的物件樹裡移除。在父物件進行析構的時候,還是會去遍歷子物件容器,一個一個析構。這個時候,就會出現,一個物件指標被刪除了兩次,自然就會崩潰。
那麼,如果非要自己管理這個物件,有什麼辦法呢?我們從物件樹下手,有兩種辦法:
-
使用
deleteLater
就是呼叫
QObject
物件的deleteLater
函式,來實現刪除。關於deleteLater
的分析,可以看這個大佬的文章Qt 中 deleteLater() 函式的使用QObject *object = new QObject(); QObject *m_child = new QObject(object); // 需要手動刪除的時候 m_child->deleteLater();
-
先將父物件設定為空,再直接delete
QObject *object = new QObject(); QObject *m_child = new QObject(object); // 需要手動刪除的時候 m_child->setParent(nullptr); delete m_child; m_child = nullptr;
-
先將父物件設定為空,再直接delete
QObject *object = new QObject(); QObject *m_child = new QObject(object); // 需要手動刪除的時候 m_child->setParent(nullptr); delete m_child; m_child = nullptr;
個人建議使用第一種方法,也就是呼叫deleteLater