QT
下类似于 std::unique_ptr
的 QScopedPointer
智能指针源码分析
因为手动管理分配到堆的对象非常困难且容易出错,常见结果就是代码的内存泄漏并且难以维护,QScopedPointer
是 Qt 中一个小型实用程序类,它将堆上申请的资源归属权交给了栈上内存管理,而栈上的资源会在生命周期结束后,主动调用析构函数,保证了资源的安全可靠。
用法总结
关于 QScopedPointer
如何使用,可以直接官方的说明文档 https://doc.qt.io/qt-5/qscopedpointer.html,我这里简单介绍一下用法和一些可能需要注意的细节
额外说明
- 它是非引用计数的 独享资源 的 强类型 指针,它是线程安全的。
QScopedPointer
的拷贝构造和赋值操作都是私有的, 所以该对象不能被拷贝或者被赋值,含有QScopedPointer
成员变量的类也就禁止拷贝和赋值了
QScopedPointer<int> sp1(new int(20));
QScopedPointer<int> sp2(sp1); // 错误,不支持拷贝
QScopedPointer<int> sp3;
sp3 = sp2 // 错误,不支持赋值
如果想重新赋值需要使用
void QScopedPointer::reset(T *other = Q_NULLPTR)
如果想要返回
QScopedPointer
里的指针
QWidget * newWidget()
{
QScopedPointer<QWidget> spWidget(new QWidget());
// return spWidget.data(); // 错误写法
return spWidget.take(); // 正确写法
}
T *QScopedPointer::data() const
返回的指针,QScopedPointer
仍对其有所有权,所以出了作用域还是会被析构,导致野指针
T *QScopedPointer::take()
, 不会在拥有对象的所有权,但是此时返回的指针需要开发者自己管理
析构
QScopedPointerDeleter
默认,调用delete
析构QScopedPointerArrayDeleter
调用delete[]
析构QScopedPointerPodDeleter
调用free()
析构QScopedPointerDeleteLater 使用
调用deleteLater
析构- 自定义 重新实现公共的静态函数
void cleanup(T *pointer)
// this QScopedPointer deletes its data using the delete[] operator:
QScopedPointer<int, QScopedPointerArrayDeleter<int> > arrayPointer(new int[42]);
// this QScopedPointer frees its data using free():
QScopedPointer<int, QScopedPointerPodDeleter> podPointer(reinterpret_cast<int *>(malloc(42)));
// this struct calls "myCustomDeallocator" to delete the pointer
struct ScopedPointerCustomDeleter
{
static inline void cleanup(MyCustomClass *pointer)
{
myCustomDeallocator(pointer);
}
};
// QScopedPointer using a custom deleter:
QScopedPointer<MyCustomClass, ScopedPointerCustomDeleter> customPointer(new MyCustomClass);
源码解析
从析构的方式看起
前面提到关于析构的时候有几个不同方式
QScopedPointerDeleter
默认QScopedPointerArrayDeleter
QScopedPointerPodDeleter
QScopedPointerDeleteLater 使用
- 自定义
这里先只以 QScopedPointerDeleter
为例
template <typename T>
struct QScopedPointerDeleter
{
static inline void cleanup(T *pointer)
{
typedef char IsIncompleteType[ sizeof(T) ? 1 : -1 ];
(void) sizeof(IsIncompleteType);
delete pointer;
}
};
这段代码的功能很好理解,定义了一个模板,用 struct
封装了一个静态函数 cleanup
, cleanup
函数删除指针
typedef char IsIncompleteType[ sizeof(T) ? 1 : -1 ];
(void) sizeof(IsIncompleteType);
这2行,主要就是在做 不完全类型检测
typedef char IsIncompleteType[ sizeof(T) ? 1 : -1 ];
这句话其实就是定义了一个数组名为 IsIncompleteType
, 数组大小为 sizeof(T)?1:-1
换句话就是如果 T
是不完全类型,这个定义就变成了 typedef char IsIncompleteType[-1]
(void)sizeof(IsIncompleteType)
时,因为数组的长度不能为负数,在编译期间就会报错
而(void)
强制类型转换,其实只是为了消除编译器对未使用sizeof返回值的警告
那么现在又有问题了,什么是不完全类型,那为什么 delete
前,需要添加这一步不完全类型检测
不完全类型的定义是
已经声明但是尚未定义的类型。不完全类型不能用于定义变量或者类的成员,但是用不完全类型定义指针或者引用是合法的。
简单点说,不完全类型 就是 尚未定义完全的类型。编译器知道这个类型(引用声明),但不知道给该类型对象分配多大内存
而在C++中,delete
一个类型不完整的类对象的指针,编译器会发出警告,但是不会报错
对于一个不完全类型,编译器看不到 T
的完整类型,没办法确定两件事情:
T
有没有自定义的析构函数(准确的说,有没有non-trivial的析构函数)。T
有没有自定义的operator delete函数。
在不确定这两件事情的情况下,编译器只能按最普通的方式去处理 delete T
:
- 不调用任何析构函数。
- 调用全局的operator delete
本来编译器应该在析构函数调用之前先调用基类的析构函数和成员的析构函数,但是现在基类和成员都无法确定,因此只有不调用,所以最后的结果就是:只释放指针所指的内存,不调用析构函数,也不调用基类和成员的析构函数。换句话说,delete T,实际上已经变成了delete (void*)T。后面一种写法的危险性是显而易见的。所以为了避免这种风险,增加了不完全类型检测
QScopedPointerDeleter
这种析构算是解释清楚了,其他三种基本一样的,不做详细解释了,下面开始正餐
QScopedPointer类
解构造函数和析构函数
template <typename T, typename Cleanup = QScopedPointerDeleter<T> >
class QScopedPointer
{
public:
explicit inline QScopedPointer(T *p = Q_NULLPTR) : d(p)
{
}
inline ~QScopedPointer()
{
T *oldD = this->d;
Cleanup::cleanup(oldD);
}
protected:
T *d;
private:
Q_DISABLE_COPY(QScopedPointer)
};
整体的结构可以看出用 T *d
保存需要代理的指针,创建时直接传入需要代理的指针,同时禁止拷贝和赋值 Q_DISABLE_COPY(QScopedPointer)
# define Q_DECL_EQ_DELETE = delete
# define Q_DISABLE_COPY(Class) \
Class(const Class &) Q_DECL_EQ_DELETE;\
Class &operator=(const Class &) Q_DECL_EQ_DELETE;
其他接口
接口 | 描述 |
---|---|
T *data() const | 返回此对象引用的指针的值。 QScopedPointer仍然拥有所指向的对象。 |
bool isNull() const | 如果此对象持有的指针为null,则返回true。 |
void reset(T *other = Q_NULLPTR) | 删除它指向的现有对象,并将其指针设置为other。 QScopedPointer现在拥有other,并将在其析构函数中将其删除。 |
void swap(QScopedPointer |
将该指针与其他交换。 |
T *take() | 返回此对象引用的指针的值。 此QScopedPointer对象的指针将重置为null。 该函数的调用者获得指针的所有权。 |
inline T *data() const
{
return d;
}
inline bool isNull() const
{
return !d;
}
inline void reset(T *other = Q_NULLPTR)
{
if (d == other)
return;
T *oldD = d;
d = other;
Cleanup::cleanup(oldD);
}
inline T *take()
{
T *oldD = d;
d = Q_NULLPTR;
return oldD;
}
void swap(QScopedPointer<T, Cleanup> &other) Q_DECL_NOTHROW
{
qSwap(d, other.d);
}
这一部分的实现结合功能描述很简单,难度不大, 这里需要注意一个地方 关于 isNull
, 指针值是可以隐式转换为 bool
,如果指针为空,转换为 false
,否则转换为 true
, 同时 bool
型也可以隐式转换为算术类型时,true
转换为 1
,false
转换为 0
。 这个先记一下,这个对后面有帮助
template <typename T>
inline void qSwap(T &value1, T &value2)
noexcept(noexcept(QtPrivate::SwapExceptionTester::checkSwap(value1)))
{
using std::swap;
swap(value1, value2);
}
重载操作符(2个类之间操作)
接口 | 描述 |
---|---|
== | 判断2个 QScopedPointer 指针的值是否相等 |
!= | 判断2个 QScopedPointer 指针的值是否不相等 |
swap | 交换2个 QScopedPointer 对象交换 |
template <class T, class Cleanup>
inline bool operator==(const QScopedPointer<T, Cleanup> &lhs, const QScopedPointer<T, Cleanup> &rhs)
{
return lhs.data() == rhs.data();
}
template <class T, class Cleanup>
inline bool operator!=(const QScopedPointer<T, Cleanup> &lhs, const QScopedPointer<T, Cleanup> &rhs)
{
return lhs.data() != rhs.data();
}
template <class T, class Cleanup>
inline void swap(QScopedPointer<T, Cleanup> &p1, QScopedPointer<T, Cleanup> &p2) Q_DECL_NOTHROW
{ p1.swap(p2); }
重载操作符(类本身)
接口 | 描述 |
---|---|
operator bool() | 如果此对象不为null,则返回true。 此函数适用于if,例如:if (scopedPointer) {},简单点说就是提供一个本类型对象到bool的隐式转换 |
bool operator!() const | 如果此对象引用的指针为null,则返回true,否则返回false。 |
T &operator*() const | 提供对作用域指针对象的访问。指针可能为null,用前建议判断 |
T *operator->() const | 提供对作用域指针对象的访问。指针可能为null,用前建议判断 |
inline T &operator*() const
{
Q_ASSERT(d);
return *d;
}
inline T *operator->() const
{
Q_ASSERT(d);
return d;
}
inline bool operator!() const
{
return !d;
}
#if defined(Q_QDOC)
inline operator bool() const
{
return isNull() ? Q_NULLPTR : &QScopedPointer::d;
}
#else
typedef T *QScopedPointer:: *RestrictedBool;
inline operator RestrictedBool() const
{
return isNull() ? Q_NULLPTR : &QScopedPointer::d;
}
#endif
为了先解释一下 Q_QDOC
Q_QDOC
宏是在生产文档时才会被设置。也就是说,operator bool() const
只有在提取文档时才使用了这个签名,而实际的Qt
库中并没有这个函数,实际上用的是就是else
里的部分
引入Q_QDOC
宏的目的可能只是为了让文档中比较复杂难看的函数签名变得更好看吧
!
, *
. ->
这3个的实现很好理解,但是 operator bool()
隐式转换这个不太好理解,简单点说就是
QScopedPointer<int> sp{new int(20)};
if (sp) {} // 可以
if (sp < 1) {} // 不可以
而这一部分设计的原因我会单独写在另一篇文章中,详细解释为什么这样设计,这篇文章就先写到这里了,除了隐式转换,其他部分算是理解透了。