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 <QCoreApplication>
#include <QTimer>
#include <QDebug>
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    QTimer *timer = new QTimer;
    // QPointer<QTimer> 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<MyClass> p(useSubClass ? new MyClass() : new MySubClass);
     QScopedPointer<QIODevice> 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<QWidget> 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<MyCustomClass, ScopedPointerCustomDeleter> customPointer(new MyCustomClass);

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

 class MyPrivateClass; // 前置声明 MyPrivateClass

class MyClass
{
private:
    QScopedPointer<MyPrivateClass> 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 <QSharedPointer>
#include <QWeakPointer>
#include <QDebug>

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<MyClass> shared(new MyClass(20));
    QWeakPointer<MyClass> 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 <QSharedData>
#include <QString>

// 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<EmployeeData> 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<T> 不会发生上述情况,此时 (*i) == 5

QExplicitlySharedDataPointer

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

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

posted @ 2024-03-22 15:04  永不停转  阅读(1293)  评论(0编辑  收藏  举报