前面已经学习过auto_ptr,这里补充另外一种智能指针,比auto_ptr要更强力更通用的shared_ptr。
shared_ptr
简介及使用选择
几乎所有的程序都需要某种形式的引用计数智能指针,这种指针让我们不再需要为两个对象或更多对象共享的对象的生命周期而编写复杂的逻辑(写起来有点绕口),当被共享的对象引用计数降为0时,被共享对象被自动析构。
引用计数指针分为插入式(instrusive)和非插入式(non-instrusive)两种。前者要求它所管理的类提供明确的函数或数据成员用于管理引用计数,这要求类在设计时即预料到将与一个插入式引用计数指针一起工作,或者重新设计它。非插入式引用计数指针对它管理的类没有任何要求,引用计数智能指针拥有与它所存指针有关的内存的所有权。
引用计数智能指针可以自动管理生存周期,避免共享对象拥有者之间有太强的耦合,增加了程序的重用性。
以下场景更适合应用引用计数智能指针:类的复制很昂贵,或者它代表的有些东西必须被多个实例共享;共享的资源没有一个明确的拥有者。
引用计数智能指针的优点:引用计数智能指针可以再需要访问共享对象的多个资源之间共享访问权;引用计数智能指针还能让你把对象存入标准库的容器中而不存在泄漏的风险;如果把指针放入容器可以获得多态的好处(模板即一种静态多态计数),可以提高性能(这个不敢苟同,多态的引入更多是为了代码的重用而不是为了效率,甚至可能会牺牲部分效率),可以把相同的对象放入多个容器进行特定的查找(这是一个很好的主意,提高查找的效率并且不管生命周期)。
使用插入式引用计数指针还是非插入式引用计数指针:通常选择非插入式引用计数指针,因为他更灵活、更通用,不需要修改已有的代码。
shared_ptr的关键函数
template <class U> explicit shared_ptr (U* p);
这个构造函数获得指定指针p的所有权,p必须是一个合法的指针,否则报错,构造完成后引用计数count=1
template <class U, class D> shared_ptr (U* p, D del);
这个构造函数包含两个参数,p是将要被管理的指针,del是被销毁时负责释放资源的对象,被保存的对象将以del(p)的形式传给del
template <class U> shared_ptr (const shared_ptr<U>& x) noexcept;
x中保存的资源被新构造的对象锁共享,引用计数count+1,貌似是唯一的通过构造共享对象的方式。
template <class U> explicit shared_ptr (const weak_ptr<U>& x);
从一个weak_ptr构造shared_ptr,这使得weak_ptr的使用具有线程安全性,因为指向weak_ptr参数的共享资源引用计数将会+1(weak_ptr不影响共享资源的引用计数)。
template <class U> shared_ptr (auto_ptr<U>&& x);
从auto_ptr中获取x保存的指针的所有权,方法是保存x保存的指针的一份拷贝并 对x调用release。
~shared_ptr();
析构函数对引用计数count-1,如果计数为0,保存的指针被删除,删除操作采用operator delete 或者用给定的删除器对象,将指针作为唯一的参数传递给删除器对象。
operator =的重载版本
template <class U> shared_ptr& operator= (const shared_ptr<U>& x) noexcept;
共享x中的资源
template <class U> shared_ptr& operator= (shared_ptr<U>&& x) noexcept;
将共享资源的所有权从x转移出来,x变成一个空的shared_ptr,共享资源的引用计数不变。
想要给shared_ptr指定共享资源不能直接对指针赋值,可以调用make_shared或者reset替代。
void reset() noexcept;
停止对保存指针所有权的共享,引用计数count-1
template <class U> void reset (U* p);
获得p的所有权并设置引用计数为1,获得p的所有权之前默认调用析构函数,即先另之前拥有的共享对象引用计数count-1.
element_type& operator*() const noexcept;
返回共享指针指向对象的引用。
element_type* operator->() const noexcept;
返回保存的指针
element_type* get() const noexcept;
返回保存的指针,当保存的指针可能为空时最好用get,而不是*或者->
bool unique() const noexcept;
当shared_ptr是共享对象的唯一拥有者才返回为true,否则返回false
long int use_count() const noexcept;
获取共享对象的引用计数,根据大神的建议,该函数最好只用于调试,因为引用计数的计算代价昂贵
explicit operator bool() const noexcept;
检测保存的共享对象指针是否是一个空指针,如果是空指针就返回false,其他情况返回true。
void swap (shared_ptr& x) noexcept;
交换两个shared_ptr保存的指针
template <class T, class... Args>
shared_ptr<T> make_shared (Args&&... args);
make_shared的使用略绕,构造一个T类型的共享对象,args就是T类构造函数的入参,T类型的共享对象通过new创建,创建以后引用计数count=1
allocate_shared的使用类比上面。
template <class T, class U>
shared_ptr<T> static_pointer_cast (const shared_ptr<U>& sp) noexcept;
当需要对保存的指针进行类型转换的时候调用,可以确保引用计数正确。
template <class T, class U>
shared_ptr<T> dynamic_pointer_cast (const shared_ptr<U>& sp) noexcept;
动态转换保存的指针且确保引用计数正确
template <class T, class U>
shared_ptr<T> const_pointer_cast (const shared_ptr<U>& sp) noexcept;
const转换保存的指针且保证引用计数正确
shared_ptr使用的时机主要是用来解决被多个对象共享的资源的正确释放机会。
大神的一个示例很能说明问题,有两个类A和B,它们共享一个int实例
a.value(28);
assert(b.value()==28);
}
类A和类B都保存了一个shared_ptr<int>,创建实例的时候,temp被传到它们的构造函数,这样同时有3个shared_ptr:a、b和temp,在例子中a、b、temp都离开main的作用域时,最后一个智能指针负责删除共享的int。
shared_ptr用标准库容器
把对象直接存入容器会有些麻烦,以值传递方式保存对象意味着调用者将获得值得一份拷贝,对于那些复制带价昂贵的类型来说可能会有性能问题,另外传值意味着没有多态的行为,如果想在容器中存放多态的对象而不想切割他们,那必须用指针,如果用裸指针,维护元素的完整性将十分复杂,使用shared_ptr可以不必担心多个使用者使用同一个元素,元素将在没有对象引用的时候被释放掉。
下面转一下大神的例子:将共享指针存入标准容器库,本来准备自己写一个例子的,可怎么样都超越不了这个经典的小例子。
这个例子的精彩之处在于同时实现了多态和共享指针的保护,多态很好理解,vector中存入的是类A的指针,通过指针实现了多态行为;共享指针的保护就堪称经典了,类A的析构函数设置为protect,这样就不能delete shared_ptr<A> get()来释放shared_ptr<A>指向的对象,手动delete释放shared_ptr<A>指向的对象将造成混乱,但类B的析构函数不是protect,shared_ptr在引用计数变为0的时候调用B的析构函数自动释放了对象。---其实这个例子里我不太明白shared_ptr是怎样正确调用B的析构函数的,内部实现肯定不是delete,也没见到传递析构器呀,问题保留,慢慢看源码分析。
shared_ptr与容器实现多态且保证安全性的一种方式:基类析构函数设置为protected。
shared_ptr与其他资源
有时候shared_ptr需要用于特殊的类型,需要其他的清理操作,而不是简单的delete,shared_ptr可以通过客户化删除器以支持这种需要,像处理FILE*这种操作系统句柄通常要用fclose来释放,这种时候我们可以定制一个客户化删除器或者传入一个单参函数来析构
上面的例子也可以用传递单参函数的方式实现
定制删除器在处理需要特殊释放程序的资源时非常有用,由于删除器不是shared_ptr的一部分,所以使用者不需要知道有关智能指针所拥有的资源的任何信息,例如使用对象池,只是简单的将对象返还到对象池中。或者单例模式singleton应该使用什么都不做的删除器。
使用定制删除器的安全性
对基类使用pretected的析构函数可以增加shared_ptr的安全性,另一个同样安全的方法是声明析构函数为pretected或者private并使用一个定制删除器来负责销毁对象,这个定制删除器必须是要删除类的友元,封装这个删除器的好方法是把删除器类实现为私有的嵌套类。
我们不能使用普通函数作为shared_ptr<A>的工厂函数,因为嵌套的删除器是A私有的,使用这个方法,用户不能在栈上创建A的对象,也不能对A的指针调用delete。
从this生成shared_ptr
有时候需要从this获得shared_ptr,也就是希望类被shared_ptr所管理,你需要把“this”变成shared_ptr的方法,我们可以用另一个智能指针weak_ptr来解决,weak_ptr是shared_ptr的一个观察者,它只是看着它们但不影响计数,通过存储一个指向this的weak_ptr作为类成员,就可以在需要的时候获得一个指向this的shared_ptr。
当以下情况时适合使用shared_ptr
多个使用者使用同一个对象,且没有明显的拥有者
要把指针存入标准库容器时
当要传送对象到库或者从库获取对象,且没有明显的拥有者
管理一些需要特殊清除方式的资源时。
补充
本文来自Beyond the c++ standard library中文版,有兴趣更进一步学习的朋友情参见原版。