目录
1.引言
2.共享数据
2.1.特点
2.3.隐式共享
2.4.显示共享
3.共享指针
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-countingQSharedData &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 应用程序。