Qt雜談8.淺談Qt智能指針那些事

1 引言

在 Qt 中,智能指針是一種能夠自動管理對象生命週期的指針類型。通過使用智能指針,可以避免手動釋放內存和處理懸掛指針等常見的內存管理問題。Qt中常用的智能指針主要有以下幾種:

  • QPointer:QPointer 是 Qt 提供的空安全的智能指針,用於解決對象懸掛指針的問題。QPointer 在對象被刪除後會被自動設置爲 nullptr,避免訪問已經無效的對象。它類似於普通指針,但提供了一些安全檢查。
  • QScopedPointer:QScopedPointer 是 Qt 提供的獨佔所有權的智能指針,用於管理動態分配的對象。QScopedPointer 在超出作用域時自動刪除對象,確保對象在不再需要時被正確釋放。它不能被複制,因此每次只有一個擁有對象的QScopedPointer。
  • QSharedPointer:QSharedPointer 是 Qt 提供的共享引用計數的智能指針,可用於管理動態分配的對象。它通過引用計數跟蹤對象的引用次數,當引用計數歸零時會自動刪除對象。可以通過多個 QSharedPointer 共享同一個對象,對象只會在最後一個引用者釋放它時纔會被刪除。
  • QWeakPointer:QWeakPointer 是 Qt 提供的弱引用智能指針,用於解決循環引用問題。QWeakPointer 可以引用由 QSharedPointer 管理的對象,但不會增加引用計數。QWeakPointer 需要轉換成 QSharedPointer 才能訪問對象,當引用計數爲零時,訪問會失敗。

2 案例分析

2.1 QPointer

QPointer 是 Qt 框架提供的一種智能指針,用於安全地處理對象的生命週期,並在對象銷燬後將指針置空,防止懸垂指針的問題。

QPointer 主要用於在持有對象的弱引用的同時能夠檢測對象是否已被銷燬。通過 QPointer,即使持有一個對象的指針,也可以確保在對象被刪除後,該指針會被自動置空。這樣,在使用 QPointer 指針時,可以檢查指針是否有效來判斷對象是否存在。

下面是一個示例,演示 QPointer 的用法:

#include <QCoreApplication>
#include <QPointer>
#include <QDebug>

class MyClass : public QObject
{
    Q_OBJECT
public:
    MyClass(const QString& name) : m_name(name)
    {
        qDebug() << "MyClass 構造函數,名稱爲" << m_name;
    }

    ~MyClass()
    {
        qDebug() << "MyClass 析構函數,名稱爲" << m_name;
    }

    QString getName() const { return m_name; }

private:
    QString m_name;
};

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    MyClass *obj = new MyClass("Object");

    QPointer<MyClass> myObject(obj);  // 創建一個 QPointer 對象來監視 MyClass 對象

    if (myObject) {
        qDebug() << "對象存在,名稱爲" << myObject->getName();
    } else {
        qDebug() << "對象不存在";
    }

    delete obj;  // 銷燬對象,導致myObject爲空

    if (myObject) {
        qDebug() << "對象存在,名稱爲" << myObject->getName();
    } else {
        qDebug() << "對象不存在";
    }

    return a.exec();
}

結果輸出:

MyClass 構造函數,名稱爲 Object
對象存在,名稱爲 Object
MyClass 析構函數,名稱爲 Object
對象不存在

在這個示例中,首先,使用 QPointer 創建了一個指向 MyClass 對象的智能指針 myObject。然後,通過判斷 myObject 是否爲空來驗證對象是否存在,並打印相應的輸出。接着,銷燬對象。最後,再次判斷 myObject 是否爲空,可以看到輸出爲"對象不存在"。

可以看出,通過使用 QPointer,即使持有對象的指針,也能夠安全地檢測對象是否有效。一旦對象被銷燬,QPointer 會自動將指針置空,避免了懸垂指針的問題,這樣可以在使用指向對象的指針時,更加安全和可靠。

2.2 QScopedPointer

使用 QScopedPointer 管理動態分配的內存時,它會確保在包含該 QScopedPointer 的作用域結束時,所管理的對象會被自動釋放,從而避免內存泄漏的問題。下面是一個示例:

#include <QScopedPointer>
#include <QDebug>

class Resource {
public:
    Resource() { qDebug() << "Resource 構造函數"; }
    ~Resource() { qDebug() << "Resource 析構函數"; }
};

void useResource()
{
    QScopedPointer<Resource> scopedResource(new Resource());

    // 執行一些操作,使用資源
    qDebug() << "使用資源...";
}

int main()
{
    useResource();
    qDebug() << "useResource 函數執行完畢";

    return 0;
}

如上,創建了一個名爲 Resource 的類,並使用 QScopedPointer 在 useResource 函數內創建動態分配的 Resource 對象。當 useResource 函數結束時,QScopedPointer 的析構函數會被調用,並自動釋放所管理的 Resource 對象。

輸出結果如下:

Resource 構造函數
使用資源...
Resource 析構函數
useResource 函數執行完畢

從結果可以看出,Resource 的構造函數在 QScopedPointer 創建對象時被調用,而析構函數在 useResource 函數結束後被調用。這表明 QScopedPointer 在適當的時候自動釋放了對象的內存,確保沒有內存泄漏的問題。

2.3 QSharedPointer和QWeakPointer

2.3.1 QSharedPointer

特點:

  • 用於管理動態分配的對象的所有權和生命週期。
  • 當存在至少一個 QSharedPointer 指向對象時,對象的內存不會被釋放。
  • 當最後一個指向對象的 QSharedPointer 超出作用域時,對象的內存會被釋放。
  • 可通過複製 QSharedPointer 來增加對象的引用計數,確保對象在合適的時候被釋放。

下面舉個例子:

#include <QSharedPointer>
#include <QDebug>

class MyClass
{
public:
    MyClass(int value) : m_value(value)
    {
        qDebug() << "MyClass 構造函數,數值爲" << m_value;
    }

    ~MyClass()
    {
        qDebug() << "MyClass 析構函數,數值爲" << m_value;
    }

    void setValue(int value)
    {
        m_value = value;
    }

    int getValue() const
    {
        return m_value;
    }

private:
    int m_value;
};

int main()
{
    QSharedPointer<MyClass> pointer1(new MyClass(10)); // 創建一個 QSharedPointer 智能指針,用於管理 MyClass 對象

    {
        QSharedPointer<MyClass> pointer2 = pointer1; // 複製構造函數,增加了 MyClass 對象的引用計數

        qDebug() << "pointer1 的值爲" << pointer1->getValue();
        qDebug() << "pointer2 的值爲" << pointer2->getValue();

        pointer2->setValue(20); // 通過 pointer2 修改對象的值

        qDebug() << "pointer1 的值爲" << pointer1->getValue();
        qDebug() << "pointer2 的值爲" << pointer2->getValue();
    } // pointer2 超出作用域,減少了 MyClass 對象的引用計數

    qDebug() << "pointer1 的值爲" << pointer1->getValue();

    return 0;
}

結果輸出:

MyClass 構造函數,數值爲 10
pointer1 的值爲 10
pointer2 的值爲 10
pointer1 的值爲 20
pointer2 的值爲 20
pointer1 的值爲 20
MyClass 析構函數,數值爲 20

如上,在 main 函數中,創建了一個 QSharedPointer 智能指針 pointer1,它指向一個值爲 10 的 MyClass 對象。然後,通過複製構造函數創建了另一個指針 pointer2,指向同一個 MyClass 對象。這個操作導致 MyClass 對象的引用計數加一。

在 pointer2 的作用域內,修改了 MyClass 對象的值,並觀察到修改同時影響了 pointer1 和 pointer2 指向的對象。當 pointer2 超出作用域時,MyClass 對象的引用計數減一,但不會立即銷燬,因爲 pointer1 仍然持有它的引用。最後,當 pointer1 超出作用域時,MyClass 對象被正確地銷燬。

2.3.2 QWeakPointer

特點:

  • 用於解決 QSharedPointer 可能導致的循環引用問題。
  • 不會增加對象的引用計數,不影響對象的生命週期。
  • 可以從 QSharedPointer 或者另一個 QWeakPointer 創建,用於在需要時保持對對象的非擁有者式引用。
  • 如果關聯的 QSharedPointer 被釋放,QWeakPointer 會自動置空,避免懸空指針問題。

這裏重點說下循環引用,當兩個或多個對象彼此持有對方的強引用時,就會形成循環引用。這種情況下,對象無法被正常釋放,會導致內存泄漏。Qt 的 QWeakPointer 類是爲了解決這個問題而引入的。

QWeakPointer 允許創建一個弱引用指向被QSharedPointer管理的對象,但不會增加該對象的引用計數。弱引用不會阻止對象的銷燬,即使所有強引用都失效,對象的析構函數也能被正確調用。

下面是一個循環引用的示例:

#include <QSharedPointer>
#include <QWeakPointer>
#include <QDebug>

class ObjectB;

class ObjectA
{
public:
    ObjectA(const QString& name) : m_name(name) {}

    ~ObjectA()
    {
        qDebug() << "ObjectA 析構函數,名稱爲" << m_name;
    }

    void setObjectB(const QSharedPointer<ObjectB>& objectB)
    {
        m_objectB = objectB;
    }

private:
    QString m_name;
    QSharedPointer<ObjectB> m_objectB;
};

class ObjectB
{
public:
    ObjectB(const QString& name) : m_name(name) {}

    ~ObjectB()
    {
        qDebug() << "ObjectB 析構函數,名稱爲" << m_name;
    }

    void setObjectA(const QSharedPointer<ObjectA>& objectA)
    {
        m_objectA = objectA;
    }

private:
    QString m_name;
    QSharedPointer<ObjectA> m_objectA;
};

int main()
{
    QSharedPointer<ObjectA> objectA(new ObjectA("ObjectA"));
    QSharedPointer<ObjectB> objectB(new ObjectB("ObjectB"));

    objectA->setObjectB(objectB);
    objectB->setObjectA(objectA);

    qDebug() << "程序結束";

    return 0;
}

結果輸出:

程序結束

如上,在 main 函數中,創建了兩個 QSharedPointer,用於管理 ObjectA 和 ObjectB 對象的生命週期。然後,通過 setObjectB 和 setObjectA 函數,相互設置對方的強引用,這樣就形成了循環引用,導致對象無法正常銷燬,從而出現內存泄漏。

爲了避免這個問題,將 m_objectB 和 m_objectA 至少一個聲明爲 QWeakPointer 類型,如下:

QSharedPointer<ObjectB> m_objectB -> QWeakPointer<ObjectB> m_objectB 
或
QSharedPointer<ObjectA> m_objectA -> QWeakPointer<ObjectA> m_objectA 

由於使用了 QWeakPointer,不會增加對象的引用計數,這樣也就打破了循環引用。當 objectA 和 objectB 超出作用域時,它們的引用計數會遞減,對象能夠被正常銷燬。修改後結果輸出如下:

程序結束
ObjectB 析構函數,名稱爲 "ObjectB"
ObjectA 析構函數,名稱爲 "ObjectA"

可以看到,對象的析構函數被正確調用,大家可以自行驗證下。

3 總結

Qt 框架提供了多種智能指針類,用於簡化對象生命週期管理和避免懸垂指針問題,使用這些智能指針類可以更方便、安全地管理對象的生命週期,減少內存泄漏和懸垂指針問題的發生,提高代碼的可靠性和性能。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章