介绍智能指针前先看这么段代码,定义一个类:
class A
{
private:
int b;
public:
A(int c):b(c) { cout << "call constructor..." << endl;}
~A() { cout << "call destructor..." << endl;}
int getValue() { return b;}
};
主方法中某代码块代码如下:
{
A *pa = new A(3);
cout << "pa.b:" << pa->getValue() << endl;
}
程序执行到代码块结束时,并没有执行 delete 运算符。
(gdb) n
call constructor...
21 cout << "pa.b:" << pa->getValue() << endl;
(gdb) p pa
$1 = (A *) 0x100304100
(gdb) p &pa
$2 = (A **) 0x7ffeefbff9a0
(gdb) n
pa.b:3
26 unique_ptr<A> ua (new A(4));
(gdb) p pa
No symbol "pa" in current context.
(gdb) p *(A **)0x7ffeefbff9a0
$3 = (A *) 0x100304100
(gdb) p **(A **)0x7ffeefbff9a0
$4 = {b = 3}
由上可以看到即使程序已跳出代码块,尽管指针 pa 已不存在,但内存地址 0x7ffeefbff9a0 指向的仍是堆空间地址 0x100304100,即发生了内存泄漏。
这只是人为因素没执行 delete 语句,但实际开发中还有可能因中间程序异常而导致没执行后面的 delete 语句,这样也会引发内存泄漏。由此便提出了智能指针,使得智能指针可以像类对象一样,当指针本身过期时,就会调用析构函数释放其指向的堆空间。这里我们仅介绍 unique_ptr:
{
unique_ptr<A> ua (new A(4));
cout << "ua.b:" << ua->getValue() << endl;
}
gdb 调试如下:
(gdb) p ua
$9 = {__ptr_ = {<std::__1::__compressed_pair_elem<A*, 0, false>> = {
__value_ = 0x100304110}, <std::__1::__compressed_pair_elem<std::__1::default_delete<A>, 1, true>> = {<std::__1::default_delete<A>> = {<No data fields>}, <No data fields>}, <No data fields>}}
(gdb) p &ua
$10 = (std::__1::unique_ptr<A, std::__1::default_delete<A> > *) 0x7ffeefbff988
...
(gdb) p ua
No symbol "ua" in current context.
(gdb) p *(A **)0x7ffeefbff988
$11 = (A *) 0x0
可以看到当程序跳出代码块时,不仅智能指针 ua 已过期,其指向的内容也得已释放,也就避免了内存泄漏。那么它是怎么做到的呢?可以看下 memory 头文件中的定义:
template <class _Tp, class _Dp = default_delete<_Tp> >
class _LIBCPP_UNIQUE_PTR_TRIVIAL_ABI _LIBCPP_TEMPLATE_VIS unique_ptr {
public:
...
~unique_ptr(void);
void reset(pointer);
...
private:
__compressed_pair<pointer, deleter_type> __ptr_;
...
}
它是一个模板类,有两个参数分别为 _Tp 和 _Dp:
_Tp
表示原生指针的类型。
_Dp
则表示析构器,开发者可以自定义指针销毁的代码。其拥有一个默认值default_delete<_Tp>
,其实就是标准的delete
函数。
还有一个析构函数,实现如下:
~unique_ptr() { reset(); }
void reset(pointer __p = pointer()) _NOEXCEPT {
pointer __tmp = __ptr_.first();
__ptr_.first() = __p;
if (__tmp)
__ptr_.second()(__tmp);
}
这里的 reset() 方法没接收任何参数,也就释放了当前指向的堆内存,其中的 __ptr_ 就是 unique_ptr 的数据成员,实为原生指针和析构器的 pair。
template <class _T1, class _T2>
class __compressed_pair : private __compressed_pair_elem<_T1, 0>,
private __compressed_pair_elem<_T2, 1> {
public:
...
typedef _LIBCPP_NODEBUG_TYPE __compressed_pair_elem<_T1, 0> _Base1;
typedef _LIBCPP_NODEBUG_TYPE __compressed_pair_elem<_T2, 1> _Base2;
...
__compressed_pair(_U1&& __t1, _U2&& __t2)
: _Base1(_VSTD::forward<_U1>(__t1)), _Base2(_VSTD::forward<_U2>(__t2)) {}
...
typename _Base1::reference first() _NOEXCEPT {
return static_cast<_Base1&>(*this).__get();
}
...
typename _Base2::reference second() _NOEXCEPT {
return static_cast<_Base2&>(*this).__get();
}
...
}
类 __compressed_pair 私有继承结构体 __compressed_pair_elem:
template <class _Tp, int _Idx,
bool _CanBeEmptyBase =
is_empty<_Tp>::value && !__libcpp_is_final<_Tp>::value>
struct __compressed_pair_elem {
typedef _Tp _ParamT;
typedef _Tp& reference;
typedef const _Tp& const_reference;
...
__compressed_pair_elem(_Up&& __u)
: __value_(_VSTD::forward<_Up>(__u))
{
}
...
_LIBCPP_INLINE_VISIBILITY reference __get() _NOEXCEPT { return __value_; }
_LIBCPP_INLINE_VISIBILITY
const_reference __get() const _NOEXCEPT { return __value_; }
private:
_Tp __value_;
};
这样通过 first() 方法就能获取到原生指针,通过 second() 获取 delete 函数。这点也应用到智能指针 unique_ptr 的其它成员函数中,具体实现可在 memory 头文件中查看。
另外 unique_ptr 接收同类型的构造函数有以下两种:
// 移动拷贝函数
unique_ptr(std::__1::unique_ptr<_Tp, _Dp> &&);
// 移动赋值函数
std::__1::unique_ptr<_Tp, _Dp> & operator=(std::__1::unique_ptr<_Tp, _Dp> &&);
可以看到两个构造函数接收的参数都只能是右值,所以如果参数本身就是 unique_ptr,需要对其类型转换为右值后才可进行赋值或拷贝,譬如执行 std::move() 方法。这也是 unique_ptr 与其它智能指针的不同之处,保证了对于一个特定的对象,只能有一个智能指针指向它。
本文部分参考于🔗C++ 智能指针最佳实践&源码分析。