深入理解Qt智能指针

目录

1.引言

2.共享数据

2.1.特点

2.2.QSharedData 

2.3.隐式共享

2.4.显示共享

3.共享指针

3.1.QSharedPointer

3.2.QWeakPointer

 4.范围指针

4.1.QScopedPointer

4.2.QScopedArrayPointer

5.追踪特定QObject对象生命

6.总结


1.引言

        在 Qt 中,智能指针是一种能够自动管理对象生命周期的指针类型。通过使用智能指针,可以避免手动释放内存和处理悬挂指针等常见的内存管理问题。根据不同的使用场景, 可分为以下几种:

1) 共享数据(QSharedData). 隐式或显式的共享数据(不共享指针), 也被称为 侵入式指针.

        QSharedDataPointer : 指向隐式共享对象的指针.

        QExplicitlySharedDataPointer : 指向显式共享对象的指针.

2) 共享指针. 线程安全.

        QSharedPointer: 有点像 std::shared_ptr, boost::shared_ptr. 维护引用计数, 使用上最像原生指针.

        QWeakPointer: 类似于boost::weak_ptr. 作为 QSharedPointer 的助手使用. 未重载*和->. 用于解决强引用形成的相互引用.

3) 范围指针. 为了RAII目的, 维护指针所有权, 并保证其在超出作用域后恰当的被销毁, 非共享.

        QScopedPointer: 相当于 std::unique_ptr,  所有权唯一, 其拷贝和赋值操作均为私有. 无法用于容器中.

        QScopedArrayPointer

4) 追踪给定 QObject 对象生命, 并在其析构时自动设置为 NULL.

        QPointer

2.共享数据

2.1.特点

1)共享数据是为了实现 “读时共享, 写时复制”. 其本质上是延迟了 执行深拷贝 的时机到了需要修改其值的时候.

C++之写时复制(CopyOnWrite)

2)C++实现为在拷贝构造和赋值运算符函数中不直接深度拷贝, 而是维护一个引用计数并获得一个引用或指针. 在需要改变值的方法中再执行深度拷贝.

3)隐式共享为, 我们无需管理深度拷贝的时机, 它会自动执行.

4)显式共享为, 我们需要人为判断什么时候需要深度拷贝, 并手动执行拷贝.

5)QSharedData 作为共享数据对象的基类. 其在内部提供 线程安全的引用计数.

6)其与 QSharedDataPointer 和 QExplicitlySharedDataPointer 一起使用.

7)以上三个类都是可重入的.

2.2.QSharedData 

        QSharedData是Qt框架中的一个基类,用于实现隐式共享(implicit sharing)机制。隐式共享是一种优化技术,允许多个对象共享同一份数据,直到数据被某个对象修改时,才会复制数据并分配给修改者,从而实现高效的内存使用和减少不必要的数据复制。下面是它的定义和实现:

(Qt5.12.12版本,源码路径:.\Qt\Qt5.12.12\5.12.12\Src\qtbase\src\corelib\tools\qshareddata.h)

class Q_CORE_EXPORT QSharedData
{
public:
    mutable QAtomicInt ref;

    inline QSharedData() : ref(0) { }
    inline QSharedData(const QSharedData &) : ref(0) { }

private:
    // using the assignment operator would lead to corruption in the ref-counting
    QSharedData &operator=(const QSharedData &);
};

QAtomicInt 是 Qt实现的原子变量,是线程安全的。

从代码可以总结出QSharedData的主要特点:

1)引用计数:QSharedData内部维护了一个引用计数(通常是一个QAtomicInt类型的成员变量),用于跟踪当前有多少对象正在共享这份数据。当对象被复制或赋值时,引用计数会增加;当对象被销毁或指向新的数据时,引用计数会减少。只有当引用计数为零时,共享的数据才会被销毁。

2)线程安全:QSharedData的引用计数是线程安全的,这意味着它可以在多线程环境中安全地使用,而无需额外的同步措施。

3)数据封装:通过继承QSharedData,开发者可以将需要共享的数据封装在派生类中,并通过QSharedDataPointer来管理这些数据的访问和共享。

2.3.隐式共享

QSharedDataPointer:表示指向隐式共享对象的指针;其在写操作时, 会自动调用detach(). 该函数在当共享数据对象引用计数大于1, 会执行深拷贝, 并将该指针指向新拷贝内容. (这是在该类的非 const 成员函数中自动调用的, 我们使用时不需要关心),这一点也可以从它的源码中体现出来,如下所示:

QSharedDataPointer 每次拷贝构造和赋值构造都是浅拷贝,只复制 QSharedData 指针的值,并增加引用计数:

(Qt5.12.12版本,源码路径:.\Qt\Qt5.12.12\5.12.12\Src\qtbase\src\corelib\tools\qshareddata.h)

 inline QSharedDataPointer(const QSharedDataPointer<T> &o) : d(o.d) { if (d) d->ref.ref(); }
    inline QSharedDataPointer<T> & operator=(const QSharedDataPointer<T> &o) {
        if (o.d != d) {
            if (o.d)
                o.d->ref.ref();
            T *old = d;
            d = o.d;
            if (old && !old->ref.deref())
                delete old;
        }
        return *this;
    }
    inline QSharedDataPointer &operator=(T *o) {
        if (o != d) {
            if (o)
                o->ref.ref();
            T *old = d;
            d = o;
            if (old && !old->ref.deref())
                delete old;
        }
        return *this;
    }

而写时深拷贝主要是借助接口的 const 声明来判断的,如果是非 const 接口,那么就会进行深拷贝,并修改引用计数:

template <class T> class QSharedDataPointer
{
public:
    //... ...
    inline void detach() { if (d && d->ref.loadRelaxed() != 1) detach_helper(); }
    inline T &operator*() { detach(); return *d; }
    inline const T &operator*() const { return *d; }
    inline T *operator->() { detach(); return d; }
    inline const T *operator->() const { return d; }
    inline operator T *() { detach(); return d; }
    inline operator const T *() const { return d; }
    inline T *data() { detach(); return d; }
    inline const T *data() const { return d; }
    inline const T *constData() const { return d; }
 
    void QSharedDataPointer<T>::detach_helper()
    {
        T *x = clone();
        x->ref.ref();
        if (!d->ref.deref())
            delete d;
        d = x;
    }
 
    template <class T>
    T *QSharedDataPointer<T>::clone()
    {
        return new T(*d);
    }
};

以下是一个简单的示例,展示了如何使用QSharedData和QSharedDataPointer来实现隐式共享:

#include <QSharedData>  
#include <QSharedDataPointer>  
#include <QDebug>  
  
// 定义共享数据类,继承自QSharedData  
class MySharedData : public QSharedData {  
public:  
    MySharedData() : value(0) {}  
    MySharedData(int val) : value(val) {}  
  
    int value;  
};  
  
// 定义使用隐式共享的类  
class MyClass {  
public:  
    MyClass() : d(new MySharedData) {}  
    MyClass(const MyClass &other) : d(other.d) {}  
  
    int getValue() const { return d->value; }  
    void setValue(int val) {  
        // 当修改数据时,可能需要分离(detach)以创建新的数据副本  
        // 这里为了简化,我们直接修改值,但在实际使用中可能需要更复杂的逻辑  
        d->value = val;  
    }  
  
private:  
    QSharedDataPointer<MySharedData> d;  
};  
  
int main() {  
    MyClass obj1(10);  
    MyClass obj2 = obj1; // obj1和obj2现在共享相同的数据  
    qDebug() << obj1.getValue() << obj2.getValue(); // 输出: 10 10  
  
    obj2.setValue(20); // 修改obj2的值,但这里我们假设没有分离,实际上应该处理分离逻辑  
    qDebug() << obj1.getValue() << obj2.getValue(); // 假设未分离,输出仍应为: 10 20(但实际上可能是未定义行为)  
  
    return 0;  
}  
  
// 注意:上面的示例中,我们没有处理分离逻辑,因为在实际使用中,当需要修改数据时,  
// QSharedDataPointer会提供detach()方法来创建一个新的数据副本,以避免影响其他共享者。  
// 但为了保持示例的简洁性,这里省略了这部分逻辑。

请注意,上面的示例为了简化而省略了分离逻辑。在实际使用中,当需要修改数据时,应该调用QSharedDataPointer的detach()方法来确保修改不会影响其他共享者。Qt的隐式共享机制正是通过这种方式来实现高效的数据共享和修改的。

2.4.显示共享

  QExplicitlySharedDataPointer :表示指向显式共享对象的指针,与QSharedDataPointer不同的地方在于, 它不会在非const成员函数执行写操作时, 自动调用 detach(),所以需要我们在写操作时, 手动调用 detach(),它的行为很像 C++ 常规指针, 不过比指针好的地方在于, 它也维护一套引用计数, 当引用计数为0时会自动设置为 NULL. 避免了悬空指针的危害.

        这些特点可以从它的源码体现出来:

(Qt5.12.12版本,源码路径:.\Qt\Qt5.12.12\5.12.12\Src\qtbase\src\corelib\tools\qshareddata.h)

template <class T> class QExplicitlySharedDataPointer
{
public:
    typedef T Type;
    typedef T *pointer;

    inline T &operator*() const { return *d; }
    inline T *operator->() { return d; }
    inline T *operator->() const { return d; }
    inline T *data() const { return d; }
    inline const T *constData() const { return d; }
    inline T *take() { T *x = d; d = nullptr; return x; }

    inline void detach() { if (d && d->ref.load() != 1) detach_helper(); }

    inline void reset()
    {
        if(d && !d->ref.deref())
            delete d;

        d = nullptr;
    }

    inline operator bool () const { return d != nullptr; }

    。。。

    template <class T>
    void detach_helper()
    {
        T *x = clone();
        x->ref.ref();
        if (!d->ref.deref())
            delete d;
        d = x;
    }

private:
    T *d;
};

示例代码:

#include <QSharedData>  
#include <QExplicitlySharedDataPointer>  
  
class MySharedData : public QSharedData {  
public:  
    int value;  
  
    MySharedData() : value(0) {}  
  
    // 允许对共享数据进行深度复制  
    MySharedData(const MySharedData &other)  
        : QSharedData(other), value(other.value) {}  
  
    // 如果需要,可以添加其他成员函数  
};  
  
class MyClass {  
public:  
    QExplicitlySharedDataPointer<MySharedData> data;  
  
    MyClass() {  
        data = QSharedDataPointer<MySharedData>(new MySharedData());  
    }  
  
    // 其他成员函数,可以访问和修改 data 指向的数据  
};

注意事项

1)当通过 QExplicitlySharedDataPointer 访问和修改数据时,需要注意线程安全。在多线程环境中,可能需要使用互斥锁(如 QMutex)来保护数据。

2)隐式共享模式可能会增加代码的复杂性,因为你需要确保在适当的时候进行数据的深拷贝。

3)并非所有情况下都需要使用隐式共享。在数据很小或者数据几乎不会被多个对象共享的情况下,使用传统的值语义可能更为简单和高效。

3.共享指针

3.1.QSharedPointer

        QSharedPointer 是 Qt 提供的共享引用计数的智能指针,可用于管理动态分配的对象。它通过引用计数跟踪对象的引用次数,当引用计数归零时会自动删除对象。可以通过多个 QSharedPointer 共享同一个对象,对象只会在最后一个引用者释放它时才会被删除。

        它可用于容器中;可提供自定义 Deleter, 所以可用于 delete [] 的场景;线程安全. 多线程同时修改其对象无需加锁. 但其指向的内存不一定线程安全, 所以多线程同时修改其指向的数据, 还需要加锁.

        它类似于C++11中的std::shared_ptr

示例如下:

#include <QSharedPointer>  
  
class MyClass {  
public:  
    MyClass() { /* 构造函数 */ }  
    ~MyClass() { /* 析构函数 */ }  
  
    // 其他成员函数  
};  
  
int main() {  
    // 创建一个 QSharedPointer 实例,指向一个新分配的 MyClass 对象  
    QSharedPointer<MyClass> ptr1(new MyClass());  
  
    // 复制 ptr1,现在 ptr2 也指向同一个 MyClass 对象  
    QSharedPointer<MyClass> ptr2 = ptr1;  
  
    // ptr1 和 ptr2 的引用计数都是 1  
  
    // 当 ptr1 超出作用域并被销毁时,引用计数减少到 1,对象不会被删除  
    {  
        QSharedPointer<MyClass> ptr3 = ptr1;  
        // 现在 ptr1、ptr2 和 ptr3 的引用计数都是 2  
    }  
    // ptr3 超出作用域并被销毁,引用计数减少到 1  
  
    // 当 ptr2 也超出作用域并被销毁时,引用计数变为 0,MyClass 对象被删除  
  
    return 0;  
}

注意事项

1)循环引用:当两个或多个 QSharedPointer 实例相互指向对方时,会导致引用计数永远无法减少到 0,从而发生内存泄漏。为了解决这个问题,可以使用 QWeakPointer 来打破循环引用。

2)线程安全:虽然 QSharedPointer 的引用计数操作是线程安全的,但指向的对象本身的访问和修改可能需要额外的同步措施,以避免数据竞争。

3)性能考虑:虽然 QSharedPointer 提供了方便的内存管理功能,但引用计数的维护会增加一定的性能开销。在性能敏感的应用中,需要权衡这种开销与便利性之间的关系。

3.2.QWeakPointer

        它提供了对 QSharedPointer 所管理对象的弱引用功能。与 QSharedPointer 的强引用不同,QWeakPointer 不会增加对象的引用计数,因此不会影响对象的生命周期。这使得 QWeakPointer 成为QSharedPointer解决循环引用问题的有力工具。

示例如下:

#include <QSharedPointer>  
#include <QWeakPointer>  
  
class MyClass {  
public:  
    MyClass() { /* 构造函数 */ }  
    ~MyClass() { /* 析构函数 */ }  
  
    // 其他成员函数  
};  
  
int main() {  
    // 创建一个 QSharedPointer 实例,指向一个新分配的 MyClass 对象  
    QSharedPointer<MyClass> sharedPtr(new MyClass());  
  
    // 创建一个 QWeakPointer 实例,通过赋值操作指向 sharedPtr 所管理的对象  
    QWeakPointer<MyClass> weakPtr = sharedPtr;  
  
    // 现在 weakPtr 指向 sharedPtr 所管理的对象,但不会增加对象的引用计数  
  
    // ...  
  
    // 在某个时刻,sharedPtr 被销毁或超出作用域  
    // 此时,如果 weakPtr 尝试访问对象,它将检测到对象已被删除,并可以避免悬垂指针的问题  
  
    // 通过 lock() 方法,可以将 QWeakPointer 转换为 QSharedPointer(如果对象仍然存在)  
    if (!weakPtr.isNull()) {  
        QSharedPointer<MyClass> lockedPtr = weakPtr.lock();  
        // 现在可以使用 lockedPtr 安全地访问对象  
    }  
  
    return 0;  
}

注意事项

1) QWeakPointer 不能用于直接访问对象,因为它不保证对象在访问时仍然有效。在访问对象之前,应该使用 isNull() 方法检查 QWeakPointer 是否为空。

2) QWeakPointer 可以通过 lock() 方法转换为 QSharedPointer,但只有在对象仍然存在时才会成功。如果对象已被删除,lock() 方法将返回一个空的 QSharedPointer

3) 在多线程环境中使用 QWeakPointer 时,需要注意线程安全问题。虽然 QWeakPointer 的操作本身是线程安全的,但指向的对象本身的访问和修改可能需要额外的同步措施。

 4.范围指针

4.1.QScopedPointer

      QScopedPointer的主要目的是管理动态分配(在堆上创建)的对象的生命周期。当 QScopedPointer 被销毁时,它会自动删除它所指向的对象,从而避免了内存泄漏的风险。

        QScopedPointer 不支持复制操作,这避免了因复制导致的对象重复删除问题。

        它的功能类似std::unique_ptr。

示例如下:

MyClass *foo() {
    QScopedPointer<MyClass> myItem(new MyClass);
    // 一些逻辑
    if (some condition) {
        return nullptr; // myItem在这里会被删除
    }
    return myItem.take(); // 释放scoped指针并返回
}
// 在异常情况下,item也会被删除

在类成员变量中使用QScopedPointer可以避免编写析构函数:

class MyClass {
public:
    MyClass() : myPtr(new int) {}
private:
    QScopedPointer<int> myPtr; // 在包含对象删除时自动删除
}

4.2.QScopedArrayPointer

  QScopedArrayPointer 是 Qt 框架中的一个智能指针模板类,它专门用于管理动态分配的数组。与 QScopedPointer 类似,QScopedArrayPointer 保证了当它的作用域结束时,所指向的数组会被自动使用 delete[] 运算符删除,从而避免了内存泄漏的风险。

        与 QScopedPointer 一样,QScopedArrayPointer 也不支持复制操作,这避免了因复制导致的数组重复删除问题。

示例如下:

#include <QScopedArrayPointer>  
  
void someFunction() {  
    QScopedArrayPointer<int> intArray(new int[10]);  
    // 初始化数组元素...  
    for (int i = 0; i < 10; ++i) {  
        intArray[i] = i * 2;  
    }  
    // ... 在这里使用 intArray 指向的数组 ...  
    // 当函数返回时,QScopedArrayPointer 会自动删除 intArray 指向的数组。  
}

5.追踪特定QObject对象生命

        QPointer专为 QObject 及其派生类对象设计。它的主要特点和用途如下:

主要特点

1)自动置空:当 QPointer 所指向的 QObject 对象被销毁时,QPointer 会自动被置为 nullptr,从而避免了悬挂指针(dangling pointer)的问题。这是通过 QObject 的析构机制实现的,当 QObject 对象被销毁时,它会通知所有指向它的 QPointer,使它们变为空指针。

2)空安全:由于 QPointer 会在对象销毁时自动置空,因此使用 QPointer 时无需担心指针悬挂的问题,增加了代码的安全性和稳定性。

3)对象限制:QPointer 只能指向 QObject 及其派生类的对象,这是因为它的工作原理依赖于 QObject 的析构机制。

使用场景

1)跨作用域对象引用:当需要在不同的作用域或类中引用同一个 QObject 对象时,使用 QPointer 可以确保即使原始对象被销毁,引用也不会导致程序崩溃。

2)信号与槽机制:在 Qt 的信号与槽机制中,如果槽函数可能引用已经销毁的对象,使用 QPointer 可以避免悬挂指针的问题。

3)对象生命周期管理:在某些复杂的场景中,需要手动管理对象的生命周期时,QPointer 可以提供一种相对安全的方式来引用这些对象。

示例代码:

#include <QPointer>  
#include <QLabel>  
  
// 假设有一个 QLabel 对象  
QLabel *label = new QLabel("Hello, QPointer!");  
  
// 创建一个 QPointer 来引用 QLabel 对象  
QPointer<QLabel> pLabel(label);  
  
// ... 在这里可以安全地使用 pLabel ...  
  
// 当 label 被 delete 后,pLabel 会自动变为 nullptr  
delete label;  
  
// 此时 pLabel.isNull() 将返回 true  
if (pLabel.isNull()) {  
    // pLabel 已经是 nullptr,可以安全地处理  
}

注意事项

1)手动销毁对象:虽然 QPointer 会在对象销毁时自动置空,但销毁对象本身(即调用 delete)仍然需要手动进行。

2)不支持非 QObject 对象:由于 QPointer 的工作原理依赖于 QObject,因此它不能用于指向非 QObject 类的对象。

3)性能考虑:虽然 QPointer 提供了额外的安全性,但它也可能带来一些性能开销,因为需要维护额外的机制来跟踪对象的销毁。在性能敏感的应用中,需要权衡这种开销与安全性之间的平衡。

6.总结

        Qt 智能指针是 Qt 框架中非常重要的特性之一,它们通过自动管理内存和资源,大大简化了 Qt 应用程序的开发和维护工作。开发者应根据具体需求选择合适的智能指针类型,并遵循相关的使用注意事项,以编写出更安全、更稳定的 Qt 应用程序。

  • 9
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值