0. 引言
智能指针auto_ptr是在C++98中被提出,当时还没有右值引用和移动语义的概念,所以到了C++11开始被弃用,在C++17就被移除了。本篇文章之所以要介绍auto_ptr是为了通过它进一步引出unique_ptr、shared_ptr和weak_ptr。
1. 智能指针要解决的主要问题
1.1. 内存泄漏
在解释内存泄漏之前,首先要清楚栈内存和堆内存在使用上的区别。存储在栈上面的数据,生命周期由操作系统进行管理,无需人工介入;存储在堆上面的数据,需要程序员手动申请和释放,由程序员负责。内存泄漏就是程序员手动申请了堆内存,由于某种原因程序未释放或无法释放导致这块堆内存没能释放掉。内存泄漏又可以细分为一下两类:
- 堆内存泄露:在堆上申请空间,未释放,导致该块内存不会被再次使用;
- 资源泄露:通常是指系统资源,如套接字、文件描述符等,由于这些资源在系统中都是有限的,如果创建了不归还就会耗尽资源。
1.2. 裸指针带来的问题
- 难以区分指针是指向一个对象还是一组对象;
- 难以确定指针在何时应该被销毁;
- 销毁指针应该用delete还是delete[],或者是其他销毁机制如fclose(),这需要追溯到最初创建该指针的位置;
- 在复杂的工程中,很难保证裸指针有且仅有一次销毁操作。
2. 什么是智能指针
简单来说,智能指针是一个自定义的类型,智能指针对象中包含一个指针,此指针指向动态开辟的空间,在智能指针对象离开作用域后,析构函数会自动调用删除器(Deleter,auto_ptr不具备删除器),释放所申请的资源。这种设计思想来源于RAII(Resource acquisition is initialization),它充分利用了C++语言局部对象自动销毁的特性来控制资源的生命周期。RAII过程可以被总结为四个步骤如下:
- 设计一个类来封装资源;
- 在构造函数中对资源进行申请;
- 在析构函数中对资源进行释放;
- 使用时定义一个该类的局部对象。
这里有必要解释一下删除器是什么。其实,删除器本身也是一个类,不过在这个类中主要是实现了一个operator()(type*)函数,通过该函数来释放指针(type*)。比如,有一个删除器对象Deleter,可以通过Deleter(ptr)的方式对指针ptr进行堆空间或资源的释放。
3. auto_ptr分析
auto_ptr类型包含的主要成员函数有构造函数、析构函数、operator=、get、operator*、operator->、reset和release等,下面的代码对这些函数进行了简单仿写,展示了auto_ptr的使用特点。
template<class _Tp>
class auto_ptr
{
public:
using element_type = _Tp;
using pointer = _Tp*;
private:
pointer _M_ptr;
public:
explicit auto_ptr(pointer _P = 0) :_M_ptr(_P) {}
~auto_ptr() { delete _M_ptr; }
//返回指向被管理对象的指针
pointer get() const
{
return _M_ptr;
}
//替换被管理对象
void reset(pointer _P = nullptr)
{
delete _M_ptr;
_M_ptr = _P;
}
//释放被管理对象的所有权
pointer release()
{
pointer _tmp = _M_ptr;
_M_ptr = nullptr;
return _tmp;
}
//以解引用的方式访问被管理对象
_Tp& operator*() const
{
return *_M_ptr;
}
//以指针的方式访问被管理对象
pointer operator->() const
{
return _M_ptr;
}
auto_ptr(auto_ptr& _Y) :_M_ptr(_Y.release()) {}
//从另一个auto_ptr转移所有权
auto_ptr& operator=(auto_ptr& _Y)
{
if (this != &_Y)
{
reset(_Y.release());
}
return *this;
}
};
从上面实现的代码可以看出,auto_ptr在构造函数中获取资源,在析构函数中释放资源,这样就有效避免了内存泄露的问题。由于C++98还未引入右值引用以及移动语义,这就导致auto_ptr的拷贝构造和赋值陷入困境。
- 浅拷贝和浅赋值会带来重复释放的问题;
- 深拷贝和深赋值会带来语义上的问题。
最终auto_ptr做了语义上的牺牲,自己实现了移动拷贝和移动赋值,称为资源所有权的转移,这就导致了auto_ptr的拷贝构造和赋值与一般类型的不同,参数都是auto_ptr&而不是const auto_ptr&,这样直接就把资源移跑了。
auto_ptr主要存在三方面的问题:
- 拷贝和赋值会改变资源的所有权,这不符合一般性指针的用法;
- 无法与STL中的容器结合使用,容器中元素应该支持拷贝和赋值;
- 由于auto_ptr的析构函数中直接使用的delete来释放资源,所有auto_ptr无法处理对象数组。