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 类的隐式共享,需要以下步骤:
- 定义 Employee 类,并声明一个 QSharedDataPointer 成员变量;
- 定义一个 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 时删除共享数据。