• QT 智能指针 QPointer QScopedPointer QSharedPointer QWeakPointer QSharedDataPointer 隐式共享 显示共享


    QPointer

    QPointer 是一种受保护的指针,当其引用的对象被销毁时,它会被自动清除(但是,销毁引用对象还是必须手动delete)。QPointer所指向的对象必须是QObject或其派生类对象。

    当多个指针指向同一个 Object 对象时,引用的对象可能被释放掉,这时使用 QPointer 就可以安全的测试引用对象是否有效,防止发生指针空悬。

    注意:Qt5 之前,QPointer 指向一个 QWidget 对象(或子类对象)时,QPointer 由 QWidget 的析构函数清除,Qt5 之后 由 QObject 的析构函数清除。在析构函数销毁被跟踪 QWidget 的子项之前,任何跟踪 QWidget 的 QPointers 都不会被清除。

    QPointer 提供的函数和运算符与普通指针的函数和运算符相同,但算术运算符+、-、 ++ 和 --除外(它们通常仅用于对象数组)。

    创建 QPointer 指针,可以使用构造函数、用 T * 赋值或相同类型的其他 QPointer 。QPointer 比较可以使用 == 和 !=,或使用 isNull() 进行测试。可以使用 nullptr 或 *xx->member 取消引用。

    QPointer 和普通指针可以混用,QPointer会自动转换为指针*。可以把 QPointer 对象传递给需要 QWidget * 参数的函数。因此,声明函数时没有必要用 QPointer 作为参数,只需使用普通指针即可。

    #include 
    #include 
    #include 
    int main(int argc, char *argv[])
    {
        QCoreApplication a(argc, argv);
        QTimer *timer = new QTimer;
        // QPointer timer = new QTimer;
        delete timer;
        // 不使用QPointer时需要设置timer为NULL
        // timer=NULL;
        if(timer)
        {
            qInfo()<<"timer is not null";
        }
        else
        {
            qInfo()<<"timer is null";
        }
        return a.exec();
    }
    

    不使用QPointer时,输出“Label is not null”,原因是,delete后未置空,易造成指针悬空。使用 QPointer 时,输出"timer is null"。
    在QPointer中 .是指针的属性,-> 是对象的属性。

    QScopedPointer

    QScopedPointer 类似于 C++ 11 中的 unique_ptr 用于管理动态分配的对象的独占所有权,即同一时间只能有一个QScopedPointer指向该对象。

    QScopedPointer使用了RAII(资源获取即初始化)技术,当QScopedPointer被销毁时,它将自动释放所管理的对象的内存。QScopedPointer不支持拷贝和赋值操作,这是为了避免在多个指针之间共享所有权的问题。如果需要在多个指针之间转移所有权,应该使用QSharedPointer或QWeakPointer。

    手动管理堆中分配的对象非常困难而且容易出错,容易导致内存泄露。QScopedPointer是一个简化内存管理的工具类,它通过将堆分配的空间赋值给基于栈的内存,通常称为RAII(resource acquisition is initialization)。
    QScopedPointer保证指向的对象在当前范围消失时自动删除。
    下面的函数分配了堆内存,使用后手动删除。

    void myFunction(bool useSubClass)
     {
         MyClass *p = useSubClass ? new MyClass() : new MySubClass;
         QIODevice *device = handsOverOwnership();
    
         if (m_value > 3) {
             delete p;
             delete device;
             return;
         }
    
         try {
             process(device);
         }
         catch (...) {
             delete p;
             delete device;
             throw;
         }
    
         delete p;
         delete device;
     }
    

    使用QScopedPointer可以简化上面的代码:

    void myFunction(bool useSubClass)
     {
         // assuming that MyClass has a virtual destructor
         QScopedPointer p(useSubClass ? new MyClass() : new MySubClass);
         QScopedPointer device(handsOverOwnership());
    
         if (m_value > 3)
             return;
    
         process(device);
     }
    

    编译器为QScopedPointer生成的代码与手动编写的代码相同。delete代码是QScopedPointer的选项之一。QScopedPointer没有赋值构造函数或赋值操作,以便清楚的表达所有权和生命周期。
    C++ 中const修饰的指针也可以使用QScopedPointer来表示。

        const QWidget *const p = new QWidget();
        // 等价于
        const QScopedPointer<const QWidget> p(new QWidget());
    
        QWidget *const p = new QWidget();
        // 等价于
        const QScopedPointer p(new QWidget());
    
        const QWidget *p = new QWidget();
        // 等价于
        QScopedPointer<const QWidget> p(new QWidget());
    

    自定义清理处理器:
    使用malloc分配的数组指针不能使用delete删除,QScopedPointer的第二个模板参数可以用于自定义清理处理器。QT提供了以下自定义清理处理器:

    • QScopedPointerDeleter 默认处理器,使用delete删除指针。
    • QScopedPointerArrayDeleter 使用delete []删除。用于处理使用new [] 创建的指针。
    • QScopedPointerPodDeleter 使用free()删除指针。用于处理使用malloc()创建的指针。
    • QScopedPointerDeleteLater 使用deleteLater()删除指针。用于处理QEventLoop中使用的QObject指针。
      自定义清理处理器中必须提供一个public的静态函数:public static function void cleanup(T *pointer)
     // QScopedPointer 使用 delete[] 删除数据
     QScopedPointer<int, QScopedPointerArrayDeleter<int> > arrayPointer(new int[42]);
    
     // QScopedPointer 使用 free()释放内存
     QScopedPointer<int, QScopedPointerPodDeleter> podPointer(reinterpret_cast<int *>(malloc(42)));
    
     // 该 struct 调用 "myCustomDeallocator" 删除指针
     struct ScopedPointerCustomDeleter
     {
        static inline void cleanup(MyCustomClass *pointer)
        {
            myCustomDeallocator(pointer);
        }
     };
    
     // QScopedPointer 使用自定义清理处理器:
     QScopedPointer customPointer(new MyCustomClass);
    

    QScopedPointer可以使用前置声明的类,但是要保证QScopedPointer需要清理的时候前置声明的类的析构函数可用。否则,编译器将输出析构函数不可用的警告。

     class MyPrivateClass; // 前置声明 MyPrivateClass
    
    class MyClass
    {
    private:
        QScopedPointer privatePtr; // QScopedPointer 使用前置声明类
    
    public:
        MyClass(); // OK
        inline ~MyClass() {} // VIOLATION - 析构函数不能使用inline修饰
    
    private:
        Q_DISABLE_COPY(MyClass) // OK - 设置拷贝构造函数和赋值操作不可用,编译器不会自动生成构造函数和赋值操作
    };
    

    成员函数

    QScopedPointer::QScopedPointer(T *p = nullptr)
    

    构造 QScopedPointer 实例,并指向p

    QScopedPointer::~QScopedPointer()
    

    销毁QScopedPointer对象。删除它指向的对象

    T *QScopedPointer::data() const
    

    返回QScopedPointer指向对象的值。QScopedPointer仍然指向该对象

    T *QScopedPointer::get() const
    

    与 data()功能相同

    bool QScopedPointer::isNull() const
    

    QScopedPointer指向的对象为nullptr时返回true

    void QScopedPointer::reset(T *other = nullptr)
    

    删除已指向的对象并指向新对象other

    QScopedArrayPointer

    QScopedArrayPointer 与 QScopedPointer 类似,但是删除指针时使用的时 delete[] 操作。QScopedArrayPointer 存储的指针指向动态分配的数组对象。如果我们指向的内存数据是一个数组,这时可以用 QScopedArrayPointer。例如:

     void foo()
     {
         QScopedArrayPointer<int> i(new int[10]);
         i[2] = 42;
         ...
         return; // 此时我们定义的 integer 数组会使用 delete[] 自动删除
     }
    

    QSharedPointer

    QSharedPointer 相当于C++11 标准中的 shared_ptr, 用于管理动态分配的对象的共享所有权,即多个 QSharedPointer 对象可以指向同一个对象,并共享该对象的内存管理。它使用引用计数来追踪对象的使用情况,当最后一个 QSharedPointer 对象被销毁时,它将自动删除它所持有的指针。由于使用了引用计数,QSharedPointer 能够自动处理指针的生命周期,避免内存泄漏和空悬指针等问题,因此是Qt中最常用的智能指针。

    需要注意的是,QSharedPointer只能管理动态分配的对象的内存。如果我们将其用于指向栈对象或全局对象,那么它就不会自动释放对象的内存,这可能会导致程序崩溃或内存泄漏。

    创建 QSharedPointer 对象可以用普通指针、另一个 QSharedPointer 对象,也可以通过将 QWeakPointer 对象提升为强引用来创建。

    线程安全

    QSharedPointer 和 QWeekPointer 是可重入类,如果不进行同步,多个线程无法同时访问指定的 QSharedPointer 对象或 QWeakPointer 对象。

    多个 QSharedPointer 和 QWeekPointer 指向同一个对象的情况下,多个线程可以安全的访问这些 QSharedPointer 和 QWeekPointer对象。QSharedPointer 采用的引用计数机制是原子操作,不需要手动同步。但是要注意,QSharedPointer 和 QWeekPointer 所指向的对象不一定是线程安全的,需要采用线程安全和重入规则来保证 QSharedPointer 和 QWeekPointer 所指向对象的安全。

    其他指针类

    Qt还提供了另外两个指针包装类: QPointer 和 QSharedDataPointer。它们彼此不兼容,因为每个都有其非常不同的用例。

    QSharedPointer 通过外部引用计数(即放置在对象外部的引用计数器)持有共享指针,指针值在 QSharedPointer 和 QWeekPointer 的所有实例之间共享。但是,指针指向的对象不应被视为共享的:都是同一个对象。QSharedPointer 不提供detach(隐式共享)或拷贝所引用对象的方法。

    QSharedDataPointer 通过基类 QSharedData 内的引用计数持有共享数据的指针(共享数据派生自 QSharedData 类)。QSharedDataPointer 可以根据访问类型对受保护数据执行detach(隐式共享):如果不是读访问,则以原子方式创建一个副本以完成操作。

    QExplicitlySharedDataPointer 是 QSharedDataPointer 的一个变量,QSharedDataPointer只有在 QExplicitlySharedDataPointer::detach() 时才会执行detach。

    QScopedPointer 专为堆内分配、删除对象设计,它持有指向堆分配对象的指针,并在其析构函数中删除对象。 QScopedPointer 是轻量级的,它不使用额外的结构或引用计数。

    QPointer 用于持有 QObject 派生对象的指针,但是一种弱引用。QWeakPointer 具有相同的功能,但不推荐使用该功能。

    可选指针跟踪
    编译调试时可以开启 QSharedPointer 的指针跟踪功能。启用后,QSharedPointer 会在全局集合中注册它跟踪的所有指针。这样就可以捕获错误(例如:将同一指针分配给两个 QSharedPointer 对象)。

    开启指针跟踪功能需要在 include QSharedPointer 前定义宏QT_SHAREDPOINTER_TRACK_POINTERS。

    即使没有启用指针跟踪功能进行代码编译,使用 QSharedPointer 跟踪指针也是安全的。如果编译的代码没有开启指针跟踪功能,QSharedPointer 会从跟踪器中删除指针。

    注意,指针跟踪功能对多重继承或虚拟继承有限制(此时两个不同的指针可以引用同一对象)。在这种情况下,如果指针被强制转换为不同的类型并且其值发生更改,则 QSharedPointer 的指针跟踪机制可能无法检测到被跟踪的对象是否为同一对象。

    QWeakPointer

    QWeakPointer 是对指针的弱引用,相当于C++11 标准中的 weak_ptr。QWeakPointer 不影响指针引用计数,可以用于验证指针是否在已另一个上下文中被删除。QWeakPointer对象的创建只能通过QSharedPointer赋值。QWeakPointer 用于追踪指针,但并不代表指针本身,它不保证指针对象的有效性,也不提供转换操作。如果要访问 QWeakPointer 追踪的指针,需要向将其提升为 QSharedPointer 并验证该对象是否为 null,若对象不为 null,则可以使用该指针。QWeakPointer::toStrongRef()用于将 QWeakPointer 转换为 QSharedPointer。
    QWeakPointer 指向 QSharedPointer 所管理的对象,但不会增加对象的引用计数,也不会影响对象的生命周期。当对象被释放时,QWeakPointer会自动被置为空指针,避免了空悬指针的问题。

    #include 
    #include 
    #include 
    
    class MyClass
    {
    public:
        MyClass(int value) : m_value(value) {
            qDebug() << "MyClass constructor called with value" << m_value;
        }
        ~MyClass() {
            qDebug() << "MyClass destructor called with value" << m_value;
        }
        int getValue() const {
            return m_value;
        }
    
    private:
        int m_value;
    };
    
    int main()
    {
        QSharedPointer shared(new MyClass(20));
        QWeakPointer weak(shared);
    
        qDebug() << "Shared pointer value:" << shared->getValue();
        qDebug() << "Weak pointer value:" << weak.data()->getValue();
    
        shared.clear();
        // 此时,MyClass对象的引用计数为0,将被自动删除,而此时 QWeakPointer 对象 weak 也为null。
    
        if (weak.isNull()) {
            qDebug() << "Weak pointer is null - object has been deleted"; //执行
        } else {
            qDebug() << "Weak pointer is not null - object still exists";
        }
    
        return 0;
    }
    

    QSharedDataPointer

    QSharedDataPointer 用于简化隐式共享类的实现。QSharedDataPointer 实现了线程安全的引用计数,保证了向可重入类添加 QSharedDataPointers 时不破坏的的可重入性。Qt 中很多类都采用了隐式共享来提升指针的访问速度和内存使用效率。Qt 的容器类都使用了隐式共享,如 QList 、QVarLengthArray 、QStack 、QQueue、 QSet、 QMap、 QMultiMap、 QHash、 QMultiHash 。
    例如,实现 Employee 类的隐式共享,需要以下步骤:

    1. 定义 Employee 类,并声明一个 QSharedDataPointer 成员变量;
    2. 定义一个 EmployeeData 并继承 QSharedData,将 Employee 需要的成员变量在 EmployeeData 中进行声明。
    #include 
    #include 
    
    // EmployeeData 继承自 QSharedData,QSharedData为其提供了一个引用计数器
    // EmployeeData 要实现共享必须提供默认的构造函数、拷贝构造函数和析构函数
    // 如果要对 Employee 类的使用者隐藏数据,应该把 EmployeeData 类声明到独立的 .h 文件中
    class EmployeeData : public QSharedData
    {
      public:
        EmployeeData() : id(-1) { }
        // 拷贝构造函数
        EmployeeData(const EmployeeData &other)
            : QSharedData(other), id(other.id), name(other.name) { }
        ~EmployeeData() { }
    
        // 共享数据
        int id;
        QString name;
    };
    
    class Employee
    {
      public:
        Employee() { d = new EmployeeData; }
        // 该构造函数调用了 setId(id) 和 setName(name), 2次修改共享数据,但是并不会触发 copy on write
        Employee(int id, const QString &name) {
            d = new EmployeeData;
            setId(id); // 此时 EmployeeData 的引用计数为 1
            setName(name); // 引用计数为 1 ,不会 copy 共享数据
        }
        // 这个拷贝构造函数可以不用定义,编译器会自动生成拷贝构造函数
        // 拷贝构造函数会对成员变量 d 进行赋值,赋值操作 =() 会增加共享数据的引用计数
        Employee(const Employee &other)
              : d (other.d)
        {
        }
        // 修改数据时,操作符 ->() 会自动调用 detach() ,如果共享数据的引用计数大于1,会自动copy一份数据用于修改。
        // 保证一个 Employee 对象对共享数据的修改不会影响其它Employee对象引用的共享数据
        void setId(int id) { d->id = id; }
        void setName(const QString &name) { d->name = name; }
    
        // 不修改 EmployeeData ,操作符 -> 不会触发detach()
        int id() const { return d->id; }
        QString name() const { return d->name; }
    
      private:
        // Employee 只用一个成员变量,其它数据在 EmployeeData 中定义
        QSharedDataPointer d;
    };
    

    在上面的场景中,Employee 对象拷贝、赋值或作为参数传递时,QSharedDataPointer 自动增加引用计数。Employee 对象删除或不在当前范围时, QSharedDataPointer 自动减少引用计数。当引用计数为 0 时,EmployeeData 对象自动被删除。当 Employee 的成员函数对成员变量 d 进行修改时,QSharedDataPointer 自动调用 detach() 操作,并为 Employee 对象拷贝出一份数据,保证 Employee 对象对数据的修改不会影响其它对象。如果 Employee 有多个成员函数都会修改成员变量 d,detach() 会被调用多次,但是只有首次调用 detach() 时才会拷贝共享数据,因为第一次调用 detach() 后成员变量 d 指向新拷贝的数据,它的引用计数为 1。

    隐式共享需要注意的问题:
    看下面的代码,如果 e1 和 e2 都代表 id 为 1001 的数据,那么将出现 1001 有 2 个不同 name 的问题,这在数据一致性上可能会有问题。这种问题需要使用显示共享来解决,即在 Employee 类中声明为显示共享 QExplicitlySharedDataPointer d; 显示共享不会自动执行 copy on write 操作。

    #include "employee.h"
    
    int main()
    {
        Employee e1(1001, "Albrecht Durer");
        // e1 和 e2 指向同一个共享数据(1001, "Albrecht Durer")
        Employee e2 = e1;
        // setName 后成员变量 e1 将指向另一个数据(1001, "Hans Holbein")
        e1.setName("Hans Holbein");
    }
    

    可以考虑使用宏Q_DECLARE_TYPEINFO()将隐式共享类声明为 movable 类型,例如上面的 Employee 类。

    隐式共享的 iterator 问题:当使用 iterator 遍历容器时,不能进行容器拷贝。例如:

    QList<int> a, b;
    // 构造一个用 0 初始化的 list
    a.resize(100000); 
    
    QList<int>::iterator i = a.begin();
    // 下面是 iterator i 的错误使用方式
    
    b = a;
    
    // 与 STL 容器不同的是,如果此时执行 *i = 4 会发生隐式拷贝
    
    a[0] = 5;
    // 此时 a 已经从共享数据中分离出来,虽然 i 是 容器 a 的遍历器,但实际上是 b 的遍历器,此时 (*i) == 0
    
    b.clear(); 
    // 此时遍历器 i 已经完全无效了
    
    int j = *i; 
    // 此时会出现 i 未定义的情况
    
    // 如果使用的是 STL 的容器类 std::list 不会发生上述情况,此时 (*i) == 5
    

    QExplicitlySharedDataPointer

    QExplicitlySharedDataPointer 用于简化显式共享类的实现。QExplicitlySharedDataPointer 实现了线程安全的引用计数,保证了向可重入类添加 QSharedDataPointers 时不破坏的的可重入性。
    QExplicitlySharedDataPointer 与 QSharedDataPointer 类似,惟一不同的是 QExplicitlySharedDataPointer 的成员函数在出现写操作时不会自动 copy 共享数据。QExplicitlySharedDataPointer 的 detach() 方法只能手动调用。QExplicitlySharedDataPointers 自动进行引用计数并在引用计数变为 0 时删除共享数据。

    参考文章:
    Qt 智能指针介绍: QSharedPointer、QWeakPointer 、QScopedPointer 、QPointer(附实例)
    Qt 中的智能指针
    【图解】Qt中的智能指针
    Qt 智能指针学习(7种QT的特有指针)

  • 相关阅读:
    云数融合与大数据技术在日常生活中的创新应用探索
    访问量突破1W,纪念一下~
    云计算实战应用案例精讲-【概念篇】多模态(附实战应用案例)
    使用KNN进行手写体识别和iris数据集分类
    Mysql binlog日志功能使用,简单易懂
    Win10笔记本开热点后电脑断网的解决方法
    教程图文详解 - 下一代互联网(第七章)
    onnx手术刀(ONNX-GraphSurgeon):对模型的输入端进行增,删,改操作(一)
    IPv6进阶:IPv6 过渡技术之 GRE 隧道
    微信小程序
  • 原文地址:https://www.cnblogs.com/ITnoteforlsy/p/18089527