Qt杂谈8.浅谈Qt智能指针那些事

发布时间 2023-12-29 17:23:09作者: 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 框架提供了多种智能指针类,用于简化对象生命周期管理和避免悬垂指针问题,使用这些智能指针类可以更方便、安全地管理对象的生命周期,减少内存泄漏和悬垂指针问题的发生,提高代码的可靠性和性能。