浅析C++智能指针和enable_shared_from_this 机制
大家好,我是小牛,今天跟聊一下 BAT 面试 C++ 开发工程师必问的一个考点:智能指针。
小艾:你昨晚面 C++ 去了?
小牛:对啊,不是这个厂主要技术栈都是 C++ 嘛,我就面去了。
小艾:问了点啥啊?
小牛:BAT 这 C++ 问的都差不多,又问智能指针了。
小艾:那来讲讲呗。
小牛:来。
智能指针的引入
大家都知道,指针是 C++ 中非常重要的一部分,大家在初期学习 C++ 的时候一定学过类似这样的指针方式。
int *ptr;
这种指针也被称为裸指针。但是使用裸指针会存在一些不足:
如果使用裸指针分配内存后,忘记手动释放资源,会出现内存泄漏。
如果使用多个裸指针指向同一资源,其中一个指针对资源进行释放,其它指针成为空悬指针,如果再次释放会存在不可预测的错误。
上图中当 sp1 把资源释放后,sp2 成了空悬指针。空悬指针指的是指针所指向的对象已经释放的时候自身却没有被置为
nullptr
。sp1 通过free/delete
释放资源的内存时,内存不会立刻被系统回收,而是状态改变为可被其它地方申请的状态。这时当再次操作 sp2,这块内存可能被其它地方申请了,而具体被谁申请了是不确定的,因此可能导致的错误也是不可预测的。如果程序异常退出时,裸指针的释放资源的代码未能执行,也会造成内存泄漏。
为了改善裸指针的不足,确保资源的分配和释放是配对的,开发者提出了智能指针。智能指针主要是对裸指针进行了一次面向对象的封装,在构造函数中初始化资源地址,在析构函数中释放资源。 当资源应该被释放时,指向它的智能指针可以确保自动地释放它。
C++ 库中,为智能指针提供了不带引用计数和带引用计数的两种方案。
引用计数用于表示有多少智能指针引用同一资源。不带引用计数的智能指针采用独占资源的方式,而带引用计数的智能指针则可以同时多个指向同一资源。下面介绍一下它们的主要特点和区别。

不带引用计数的智能指针
不带引用计数的智能指针包括 auto_ptr
、scoped_ptr
和 unique_ptr
三种指针。

1. auto_ptr:
我们先来看个例子:
#include
int main()
{
auto_ptr<int> ptr(new int(6));//定义auto_ptr指针ptr
auto_ptr<int> ptr1(ptr); //拷贝构造ptr定义ptr1
*ptr=8;//对空指针ptr赋值会产生不可预料的错误
return 0;
}
开始时 ptr 指向资源,一个整型数字6,当用 ptr1 拷贝构造 ptr 时,ptr1 指向资源,而 ptr 则指向 nullptr
。下一行程序中如果对空指针 ptr 赋值 8,将会产生不可预料的错误。
下图表示 auto_ptr
指针对资源的指向过程。

使用拷贝构造时,如果只有最后一个 auto_ptr
持有资源,其余 auto_ptr
持有的资源会被置为 nullptr
。
因此需要注意,不能在容器中使用 auto_ptr
,当容器发生拷贝时,原容器中 auto_ptr
持有的资源会置 nullptr
。
下面我们再来看一下 auto_ptr
的部分源码和部分解析:
template<class _Ty>
class auto_ptr
{
public:
typedef _Ty element_type;
explicit auto_ptr(_Ty * _Ptr=nullptr) noexcept
: _Myptr(_Ptr)//初始化列表
{ //构造函数
}
auto_ptr(auto_ptr& _Right) noexcept
: _Myptr(_Right.release())
{ //拷贝构造函数,会调用release()函数
}
_Ty * release() noexcept
{
/*使用拷贝构造时,最后一个auto_ptr持有资源,
其余被置为nullptr*/
_Ty * _Tmp = _Myptr;
_Myptr = nullptr;
return (_Tmp);
}
private:
_Ty * _Myptr;//指向资源
};
当试图调用 auto_ptr
的拷贝构造函数时,在初始化列表中调用了 release()
函数,release()
函数用一个 _Tmp
指针保存资源并返回用于初始化当前的 auto_ptr
的类成员 _Myptr
,而 _Right
对应的 _Myptr
被置为 nullptr
。
2. scoped_ptr
scoped_ptr
和 auto_ptr
有些不同,它私有化了拷贝构造函数和赋值函数,资源的所有权无法进行转移,也无法在容器中使用。
下面使用一段代码表现 scoped_ptr
的特性,如果不规范使用会发生错误。
正确用法:
scoped_ptr<int> sp1(new int(6));//初始化sp1指针
错误用法:
scoped_ptr<int> sp2(sp1);//错误,无法拷贝构造
这种方法是错误的,因为scoped_ptr私有化了拷贝构造函数,无法显式调用。
scoped_ptr<int> sp3(new int(5))//初始化sp2指针
sp1=sp3;//错误,无法赋值
这种方法是错误的,因为scoped_ptr私有化了赋值构造函数,无法显式调用。
有时候面试官会问到,scoped_ptr
是如何保证资源的所有权的?

这时候就可以按照上面的讲解来回答:
scoped_ptr
私有化了拷贝构造函数和赋值函数,资源的所有权无法进行转移,所以保证了资源的所有权。
然后再来看一下 scoped_ptr
的部分源码和部分解析:
template<class T>
class scoped_ptr
{
private:
T * px;
scoped_ptr(scoped_ptr const &);//拷贝构造函数
scoped_ptr & operator=(scoped_ptr const &);//赋值构造函数
public:
typedef T element_type;
explicit scoped_ptr( T * p = nullptr ): px( p )
{
}
~scoped_ptr() //析构函数
};
scoped_ptr
通过私有化拷贝构造函数和赋值构造函数来拒绝浅拷贝的发生。
值得注意的是,auto_ptr
是通过将除最后一个以外的其它 auto_ptr
置 nullptr
来避免浅拷贝的发生,它的资源所有权是可以转移的。
而 scoped_ptr
是直接禁止了拷贝与赋值,资源所有权无法转移。
3. unique_ptr
unique_ptr
删除了拷贝构造函数和赋值函数,因此不支持普通的拷贝或赋值操作。如下所示:
unique_ptr<int> p1(new int(6));//正确写法
unique_ptr<int> p2(p1); //这么写是错误的:
// unique_ptr不支持拷贝
unique_ptr<int> p3;
p3=p2;//这么写是错误的:unique_ptr不支持赋值
再来看一下 unique_ptr
的部分源码和部分解析:
template<class _Ty,class _Dx>
class unique_ptr: public _Unique_ptr_base<_Ty, _Dx>
{
public:
typedef _Unique_ptr_base<_Ty, _Dx> _Mybase;
typedef typename _Mybase::pointer pointer;
typedef _Ty element_type;
typedef _Dx deleter_type;
unique_ptr(unique_ptr&& _Right) noexcept
: _Mybase(_Right.release(),
_STD forward<_Dx>(_Right.get_deleter()))
{ // 右值引用的拷贝构造函数
}
unique_ptr& operator=(unique_ptr&& _Right) noexcept
{ //提供了右值引用的operator=赋值构造函数
if (this != _STD addressof(_Right))
{
reset(_Right.release());
this->get_deleter() = _STD forward<_Dx>
(_Right.get_deleter());
}
return (*this);
}
/*
删除了unique_ptr的拷贝构造和赋值函数,拒绝浅拷贝
*/
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
};
unique_ptr
和scoped_ptr
一样禁止了拷贝构造和赋值构造,引入了带右值引用的拷贝构造和赋值。可以把 unique_ptr
作为函数的返回值。
不带引用计数的智能指针总结:
相同点:最终只有一个智能指针持有资源。
不同点:
auto_ptr
进行拷贝构造时,会对之前的auto_ptr的资源置nullptr操作;scoped_ptr
通过私有化了拷贝构造和赋值函数杜绝浅拷贝;unique_ptr
通过删除了拷贝构造和赋值函数函数杜绝浅拷贝,但引入了带右值引用的拷贝构造和赋值函数。
带引用计数的智能指针
当需要多个智能指针指向同一个资源时,使用带引用计数的智能指针。
每增加一个智能指针指向同一资源,资源引用计数加一,反之减一。当引用计数为零时,由最后一个指向资源的智能指针将资源进行释放。
下图表示带引用计数智能指针的工作过程。sp1 对象和 sp2 对象通过指针指向同一资源,引用计数器记录了引用资源的对象个数。

当 sp1 对象发生析构时,引用计数器的值减 1,由于引用计数不等于 0,资源并未释放,如下图所示:

当 sp2 对象也发生析构,引用计数减为 0,资源释放,如下图所示:

即引用计数可以保证多个智能指针指向资源时资源在所有智能对其取消引用再释放,避免过早释放产生空悬指针。带引用计数的智能指针包括 shared_ptr
和 weak_ptr
。

1. shared_ptr
shared_ptr
一般称为强智能指针,一个 shared_ptr
对资源进行引用时,资源的引用计数会增加一,通常用于管理对象的生命周期。只要有一个指向对象的 shared_ptr
存在,该对象就不会析构。
上图中引用计数的工作过程就使用了 shared_ptr
。
2. weak_ptr
weak_ptr
一般被称为弱智能指针,其对资源的引用不会引起资源的引用计数的变化,通常作为观察者,用于判断资源是否存在,并根据不同情况做出相应的操作。
比如使用 weak_ptr
对资源进行弱引用,当调用 weak_ptr
的 lock()
方法时,若返回 nullptr
,则说明资源已经不存在,放弃对资源继续操作。否则,将返回一个 shared_ptr
对象,可以继续操作资源。
另外,一旦最后一个指向对象的 shared_ptr
被销毁,对象就会被释放。即使有 weak_ptr
指向对象,对象也还是会被释放。
小艾问:既然它这引用都不算数,那它有什么用呢?
小牛答:别急,我们来慢慢讲。
enable_shared_from_this 机制
小牛:考虑下面这样一个场景:
在多线程环境中,假设有一个对象池类
ObjectPool
和一个对象类 Object。ObjectPool
类主要实现通过不同的 key 返回对应 Object 对象。要求同一程序中由 Object 类实例出的不同对象只有一个,即当多处用到同一个对象,Object 对象应该被共享。同时当对象不再需要时应该被析构,并删除对应的 key。

小艾说:这还不简单,看我的。代码刷的一下就写完了。
//场景代码
#include
class ObjectPool:boost::noncopyable
{
public:
shared_ptr