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的特有指针)

发表评论

评论已关闭。

相关文章