前言
大部分同学可能都可以熟练知道,智能指针是管理内存的一种有效手段,shared_ptr 是通过引用计数来管理内存,当引用计数为 0 的时候内存就会自动释放,weak_ptr 则是为了解决 shared_ptr 可能会出现的循环引用的问题出现,unique_ptr 则是有独占的概念的智能指针。
那概念上可能就是上面的概括,继续追问一句,那什么时候应该使用 unique_ptr,什么时候应该使用 shared_ptr,为什么? 没有在实战中真的去留意过这个问题,可能就会有点难以回答,除了对八股文有一些了解,更深入学习背后的内容,多少会对理解它们在实战中的使用场景有所帮助。
下面的内容主要是摘录整理自 《Effective Modern C++》,有需要的同学推荐直接去阅读原书的相关章节。
首先要了解一点,智能指针就是对普通指针的一个封装,相当于一个模板类。
std::unique_ptr
std::unique_ptr 实现独占的这么概念主要就是相关的类实现中,删除了类的赋值以及拷贝构造函数,只实现了移动语义。
// 示例代码
unique_ptr(unique_ptr const&) = delete;
unique_ptr& operator=(unique_ptr const&) = delete;
_LIBCPP_INLINE_VISIBILITY
~unique_ptr() { reset(); }
在效率方面,如果不自定义删除器,std::unique_ptr 的内存和速度基本与原始指针是一致的,因为删除器是 unique_ptr 模板类实例化的一部分,所以自定义删除器可能会多一个指向删除器的指针(使用 lambda 表达式则不会带来额外的内存负担)。
在使用方面,根据书中的描述,std::unique_ptr 体现了专有权语义,其常用作继承层次结构中对象的工厂函数返回类型,因为调用方会在对上分配一个对象然后返回指针,调用方在不需要的时候有责任销毁对象,当调用者需要对返回的资源负责(即对该资源的专有所有权),并且 std::unique_ptr 在自己被销毁时会自动销毁指向的内容。 除了上面说的工厂函数,还有一种 pimpl 的机制(point to implementation,一种隐藏实际实现而减弱编译依赖的设计思想),这个放在最后介绍。
下面是 std::unique_ptr 自定义删除器的用法示例,这里不过多展开。
auto delInvmt = [](Investment* pInvestment) //自定义删除器
{ //(lambda表达式)
makeLogEntry(pInvestment);
delete pInvestment;
};
template<typename... Ts>
std::unique_ptr<Investment, decltype(delInvmt)> //更改后的返回类型
makeInvestment(Ts&&... params)
{
std::unique_ptr<Investment, decltype(delInvmt)> //应返回的指针
pInv(nullptr, delInvmt);
if (/*一个Stock对象应被创建*/)
{
pInv.reset(new Stock(std::forward<Ts>(params)...));
}
else if ( /*一个Bond对象应被创建*/ )
{
pInv.reset(new Bond(std::forward<Ts>(params)...));
}
else if ( /*一个RealEstate对象应被创建*/ )
{
pInv.reset(new RealEstate(std::forward<Ts>(params)...));
}
return pInv;
}
std::unique_ptr 除了用于单个对象,同样可以用于数组(其他智能指针就没有相关支持数组的实现)。并且可以很方便的转化成 std::shared_ptr。
template<typename... Ts> //返回指向对象的std::unique_ptr,
std::unique_ptr<Investment> makeInvestment(Ts&&... params); //对象使用给定实参创建
std::shared_ptr<Investment> sp = makeInvestment(arguments); //将std::unique_ptr转为std::shared_ptr
- std::unique_ptr 是轻量级、快速、只可移动地管理专有权语义资源的智能指针
- 默认删除资源通过 delete 实现,支持自定义删除器(可能会影响 std::unique_ptr 对象大小)
- 转换成 shared_ptr 比较方便
std::shared_ptr
首先先抛出一个结论,std::shared_ptr 对象大小一般是原始对象的两倍。一份大小就是传入的原始指针的大小,还有一个指针大小是用来指向控制块的(类似于虚函数表指针的形式),这个控制块是另一块内存,里面存储了计数使用的相关变量,还可能有一些用户自定义的删除器,空间配置器的地址信息。
上面控制块的创建,有下面 3 种情况:
- std::make_shared 创建 std::shared_ptr 对象时总会创建一个控制块,它创建一个要指向的新对象,所以可以肯定 std::make_shared 调用时对象不存在其他控制块。
- 从独占指针(std::unique_ptr 或者 std::auto_ptr)上构造 std::shared_ptr 对象时会创建控制块,因为独占指针没有控制块,所以需要创建一个。
// 示例代码
template<class _Tp>
template <class _Yp, class _Dp>
shared_ptr<_Tp>::shared_ptr(unique_ptr<_Yp, _Dp>&& __r,
typename enable_if
<
!is_lvalue_reference<_Dp>::value &&
is_convertible<typename unique_ptr<_Yp, _Dp>::pointer, element_type*>::value,
__nat
>::type)
: __ptr_(__r.get()) // 初始化原始指针
{
#if _LIBCPP_STD_VER > 11
if (__ptr_ == nullptr)
__cntrl_ = nullptr;
else
#endif
{ // 额外创建需要的控制块
typedef typename __shared_ptr_default_allocator<_Yp>::type _AllocT;
typedef __shared_ptr_pointer<typename unique_ptr<_Yp, _Dp>::pointer, _Dp, _AllocT > _CntrlBlk;
__cntrl_ = new _CntrlBlk(__r.get(), __r.get_deleter(), _AllocT());
__enable_weak_this(__r.get(), __r.get());
}
__r.release();
}
- 从原始指针上创建std::shared_ptr 对象时会创建控制块。使用 std::shared_ptr 或者 std::weak_ptr 创建 std::shared_ptr 对象时不会创建控制块。
// 示例代码
template<class _Tp>
inline
shared_ptr<_Tp>::shared_ptr(const shared_ptr& __r) _NOEXCEPT
: __ptr_(__r.__ptr_),
__cntrl_(__r.__cntrl_) // 因为本身就有,直接赋值,不会创建新的
{
if (__cntrl_)
__cntrl_->__add_shared();
}
因为原始指针直接创建时会创建新的控制块,所以下面的用法就是一个错误的示范:
auto ptr = new Widget; // ptr 是原始指针
std::shared_ptr<Widget> spw1(ptr); // 为 ptr 创建第一个控制块
std::shared_ptr<Widget> spw2(ptr); // 为 ptr 创建第二个控制块
上面多个控制块意味着多个引用计数值,多个引用计数就意味着对象会被销毁多次。
建议的情况就是使用 std::make_shared 创建,或者由 std::shared_ptr 多次创建。
还有一种情况就是在类中需要往容器中添加 this 指针,因为一个类对象的 this 指针就只有一个,可能出现给 this 指针创建多个控制块的情况。这里需要使用 std::enable_shared_from_this 这个基类模板,其中定义了一个成员函数 shared_from_this(), 可以保证创建指向当前 this 的 shared_ptr 对象并不会创建多余的控制块,当想在成员函数中使用 std::shared_ptr 指向 this 所指对象时都可以使用它。
错误使用:
class Widget {
public:
…
void process();
…
std::vector<std::shared_ptr<Widget>> processedWidgets;
};
void Widget::process()
{
… //处理Widget
processedWidgets.emplace_back(this); //然后将它加到已处理过的Widget
} //的列表中,这是错的!
正确用法:
class Widget: public std::enable_shared_from_this<Widget> {
public:
…
void process();
…
};
void Widget::process()
{
…
//把指向当前对象的std::shared_ptr加入processedWidgets
processedWidgets.emplace_back(shared_from_this());
}
以上主要是说明在使用 std::shared_ptr 的时候要注意不要创建多个控制块。有这么多注意事项,并且大小还是原指针的两倍,听着好像 std::shared_ptr 的使用稍高,作为这些轻微开销的交换,你可以得到动态分配的资源的生命周期自动管理的好处,大多数时候,比起手动管理,其管理共享型资源还是比较合适的。如果独占型资源可行或者可能可行,还是优先推荐使用 unique_ptr, 并且 unique_ptr 转 shared_ptr 也很方便。
- std::shared_ptr 为共享所有权的任意资源提供一种自动垃圾回收的机制
- 相较于 std::unique_ptr, std::shared_ptr 对象通常大两倍,控制块会产生开销,需要原子性的引用计数修改操作
- 默认删除资源是 delete, 支持自定义删除器,删除器的类型不会影响大小
- 避免直接原始指针变量上创建 std::shared_ptr
std::weak_ptr
weak_ptr 不是一个独立的智能指针,通常都从 share_ptr 上创建,创建时 share_ptr 与 weak_ptr 指向相同的对象,但是 std::weak_ptr 不会影响所指对象的引用计数。
auto spw = //spw创建之后,指向的Widget的
std::make_shared<Widget>(); //引用计数(ref count,RC)为1。
…
std::weak_ptr<Widget> wpw(spw); //wpw指向与spw所指相同的Widget。RC仍为1
…
spw = nullptr; //RC变为0,Widget被销毁。wpw现在悬空
如果想判断一个 weak_ptr 是否已经过期有一下三种方式:
// 方式1:直接调用 expired 函数,线程不安全
if(wpw.expired())
// 方式2:使用 lock 函数,创建一个 shared_ptr 然后判空,有原子性,线程安全
std::shared_ptr<Widget> spw1 = wpw.lock(); //如果wpw过期,spw1就为空
// 方式3:直接初始化一个 shared_ptr
std::shared_ptr<Widget> spw3(wpw); //如果wpw过期,抛出std::bad_weak_ptr异常
以上是对 weak_ptr 功能及用法的一些介绍。可能的适用场景主要包括:
- 打破 std::shared_ptr 的循环引用,因为weak_ptr 不会引起计数变化。
- 观察者模式中的观察者列表,因为当一个观察者销毁时,消息产生者要不再使用,所以可以让消息产生者持有一个 std::weak_ptr 的容器指向观察者,这样可以在使用前检查是否悬空。
- 缓存:主要是在可缓存对象中,调用者应当接受缓存对象的智能指针,并且需要知道缓存对象是否悬空,悬空则销毁。
|