在effective C++经常会提到智能指针,这里对shared_ptr进行一个总结:
1 简要介绍用法
智能指针主要是用于资源管理,当申请一个资源的时候为了保证在离开控制流的时候对应资源应该得到相应的释放,这个时候如果资源对应一个类,在构造类的时候进行资源的分配(也就是书中经常提到的Resource Acquisition Is Initialization RAII),在对象离开作用域的时候调用对应的析构函数资源得到适当的释放
这里有几个智能指针得到应用:
auto_ptr: 被销毁的时候会自动删除它所指之物,所以不能让多个auto_ptr指向同一个对象,因此对于auto_ptr的对象,在调用复制构造函数和copy assignment的时候他们会变成null, 因此他并不是管理动态分配资源的利器
shared_ptr: 实现的是引用计数型的智能指针 ,可以被自由地拷贝和赋值,在任意的地方共享它,当没有代码使用(引用计数为0)它时才删除被包装的动态分配的对象。shared_ptr也可以安全地放到标准容器中,并弥补了auto_ptr因为转移语义而不能把指针作为STL容器元素的缺陷。
注意:auto+ptr和shared_ptr在析构函数中调用delete而不是delete[]操作,因此如果在定义的时候申请了一个对象数组是一个不好的情况
<span style="font-size:14px;">auto_ptr<string> aps(new string[10]) shared_ptr<int> spi(new int[10]) //<都很不好</span>
如果一定要定义数组,那么可以利用STL中vector,同时还有boost::scope_arry,boost::shared_arry calsses 可以使用
下面是shared_ptr的一些简单的使用方法:
example1:
<span style="font-size:14px;">#include <iostream> #include <assert.h> #include <memory> using namespace std; //<在RAII中提及,资源取得的时机便是初始化时机 //<将获得的资源放到一个智能指针对象中,在初始化时进行构造,没有对象引用时调用析构函数进行释放 int main() { shared_ptr<int> sp(new int(10)); //一个指向整数的shared_ptr assert(sp.unique()); //现在shared_ptr是指针的唯一持有者 shared_ptr<int> sp2 = sp; //第二个shared_ptr,拷贝构造函数 assert(sp == sp2 && sp.use_count() == 2); //两个shared_ptr相等,指向同一个对象,引用计数为2 *sp2 = 100; //使用解引用操作符修改被指对象 assert(*sp == 100); //另一个shared_ptr也同时被修改 sp.reset(); //停止shared_ptr的使用 assert(!sp); //sp不再持有任何指针(空指针) return 0; }</span>
example2:
<span style="font-size:14px;">class shared //一个拥有shared_ptr的类 { private: shared_ptr<int> p; //shared_ptr成员变量 public: shared(shared_ptr<int> p_):p(p_){} //构造函数初始化shared_ptr void print() //输出shared_ptr的引用计数和指向的值 { cout << "count:" << p.use_count() << "v =" <<*p << endl; } }; void print_func(shared_ptr<int> p) //使用shared_ptr作为函数参数 { //同样输出shared_ptr的引用计数和指向的值 cout << "count:" << p.use_count() << " v=" <<*p << endl; } int main() { shared_ptr<int> p(new int(100)); shared s1(p), s2(p); //构造两个自定义类 s1.print(); //<3,100 s2.print(); //3,100 *p = 20; print_func(p);//4,20 修改shared_ptr所指的值,因为作为是实参进行传递多了一次拷贝给临时变量 s1.print(); //3,20 } </span>
example3:
<span style="font-size:14px;">#include <vector> int main() { typedef vector<shared_ptr<int> > vs; //一个持有shared_ptr的标准容器类型 vs v(10); //声明一个拥有10个元素的容器,元素被初始化为空指针 int i = 0; for (vs::iterator pos = v.begin(); pos != v.end(); ++pos) { (*pos) = make_shared<int>(++i); //使用工厂函数赋值 cout << *(*pos) << ", "; //输出值 } cout << endl; shared_ptr<int> p = v[9]; *p = 100; cout << *v[9] << endl; } </span>
在上面的例子中shared指针中都是利用简单类型int类型,如果换成class类型等都是可以的,只是在销毁的时候还对应调用类的析构函数
example4
<span style="font-size:14px;">#include <memory> #include <vector> #include <iostream> using namespace std; class A { public: virtual void sing()=0; protected: virtual ~A() {}; }; class B : public A { public: virtual void sing() { std::cout << "Do re mi fa so la \n"; } }; shared_ptr<A> createA() { shared_ptr<A> p(new B()); return p; } int main() { typedef std::vector<shared_ptr<A> > container_type; typedef container_type::iterator iterator; container_type container; for (int i=0;i<10;++i) { container.push_back(createA()); } std::cout << "The choir is gathered: \n"; iterator end=container.end(); for (iterator it=container.begin();it!=end;++it) { (*it)->sing(); } }</span>
这里有两个类, A 和 B,
各有一个虚拟成员函数 sing. B 从 A公有继承而来,并且如你所见,工厂函数 createA 返回一个动态分配的B的实例,包装在shared_ptr<A>里。在 main里,
一个包含shared_ptr<A>的 std::vector 被放入10个元素,最后对每个元素调用sing。如果我们用裸指针作为元素,那些对象需要被手工删除。而在这个例子里,删除是自动的,因为在vector的生存期中,每个shared_ptr的引用计数都保持为1;当 vector 被销毁,所有引用计数器都将变为零,所有对象都被删除。有趣的是,即使 A 的析构函数没有声明为 virtual, shared_ptr 也会正确调用 B的析构函数!
注意:
条款1:不要把一个原生指针给多个shared_ptr管理
int* ptr = new int; shared_ptr<int> p1(ptr); shared_ptr<int> p2(ptr); //logic error
int* ptr = new int; shared_ptr<int> p1(ptr); shared_ptr<int> p2(p1); //logic OK
条款2:不要把this指针给shared_ptr
class Test{ public: void Do(){ m_member_sp = shared_ptr<Test>(this); } private: shared_ptr<Test> m_member_sp; }; int main() { Test* t = new Test; shared_ptr<Test> local_sp(t); local_sp->Do(); }
t对象给了local_sp管理,然后在shared_ptr<Test>(this)这句里又请m_member_sp来管理t,类似条款1中的错误,赋值了两个原始指针进行管理,很有可能导致删除两次
条款3:shared_ptr作为被保护的对象的成员时,小心因循环引用造成无法释放资源。
对象需要相互协作,对象A需要知道对象B的地址,这样才能给对象B发消息(或调用其方法)。设计模式中有大量例子,一个对象中有其他对象的指针。现在把原生指针替换为shared_ptr.
假设a对象中含有一个shared_ptr<B>指向b对象;假设b对象中含有一个shared_ptr<A>指向a对象并且a,b对象都是堆中分配的。很轻易就能与他们失去最后联系。考虑某个shared_ptr<A> local_a;是我们能最后一个看到a对象的共享智能指针,其use_count==2,因为对象b中持有a的指针。所以当local_a说再见时,local_a只是把a对象的use_count改成1。同理b对象。然后我们再也看不到a,b的影子了,他们就静静的躺在堆里,成为断线的风筝。
解决方案是:Use weak_ptr to "break cycles."(boost文档里写的)或者显示的清理
2 指定删除器
shared_ptr 是资源管理类,主要体现在heap-baesd资源上,但是不是所有的资源都是heap-baesd的,这个时候释放资源不一定是体现在删除资源上的
这里举了一个书中的例子:
互斥器Mutex 有两个函数lock,unlock
<span style="font-size:14px;">class Lock{ public: explicit Lock(Mutex* pm):mutexptr(pm) { lock(mutexPtr); } ~Lock() { unclock(mutexPtr) } private: Mutex *mutexPtr; };</span>
目前为止都还是很好的,在构造时初始化加锁,在析构的时候进行解锁操作,但是当出现copy情况的时候就变得复杂了
Lock m1(&m);
Lock m11(&m);
Lock m12(m11);
这个时候资源就混乱了,这个时候要么进制复制(将复制构造或者赋值操作符声明为private)或者引入资源管理类进行管理
同时在这个情况,引用次数为0的时候并不希望删除资源只是释放资源,所以在这里可以指定对应的删除器
<span style="font-size:14px;">class Lock{ public: explicit Lock(Mutex* pm):mutexptr(pm) { lock(mutexPtr.get());//<获得资源 } private: shared_ptr<Mutex> mutexPtr; };</span>
此时不再声明析构,class析构(无论是编译器生成或者用户自定义)会自动调用non-static成员变量的析构,mutexptr会在析构时自动调用删除器unlock
注意:复制底部资源也就是深度拷贝(无论是指针或者所指的heap内存被拷贝),在类中有copy操作的时候注意资源的管理
3 shared_ptr的资源获取:
可以利用get()函数,返回的是对象的指针
也可以利用operator-> 和 operator *操作
<span style="font-size:14px;">shared_ptr<Investment> pInv(new Investment); int days(const Investment *pi); iny day = days(pInv.get()); class Investment{ bool isTax() const; Investment * createInvestment(); shared_ptr<Investment> pInv(createInvestment); bool i = !(pInv->isTax()); bool j = (*pInv.isTax()); };</span>
4 shared_ptr的线程安全性
shared_ptr 本身不是 100% 线程安全的。它的引用计数本身是安全且无锁的,但对象的读写则不是,因为 shared_ptr 有两个数据成员,读写操作不能原子化。shared_ptr 的线程安全级别和内建类型、标准库容器、string 一样,即:
- 一个 shared_ptr 实体可被多个线程同时读取;
- 两个的 shared_ptr 实体可以被两个线程同时写入,“析构”算写操作;
- 如果要从多个线程读写同一个 shared_ptr 对象,那么需要加锁。
下面是从别的一些博客上看到的内容:
深入了解shared_ptr类结构:
template <class Y> explicit shared_ptr(Y* p);
这个构造函数获得给定指针p的所有权。参数 p 必须是指向 Y 的有效指针。构造后引用计数设为1。唯一从这个构造函数抛出的异常是std::bad_alloc (仅在一种很罕见的情况下发生,即不能获得引用计数器所需的自由空间)。
template <class Y,class D> shared_ptr(Y* p,D d);
这个构造函数带有两个参数。第一个是shared_ptr将要获得所有权的那个资源,第二个是shared_ptr被销毁时负责释放资源的一个对象,被保存的资源将以d(p)的形式传给那个对象。因此p的值是否有效取决于d。如果引用计数器不能分配成功,shared_ptr抛出一个类型为std::bad_alloc的异常。
shared_ptr(const shared_ptr& r);
r中保存的资源被新构造的shared_ptr所共享,引用计数加一。这个构造函数不会抛出异常。
template <class Y> explicit shared_ptr(const weak_ptr<Y>& r);
从一个weak_ptr (本章稍后会介绍)构造shared_ptr。这使得weak_ptr的使用具有线程安全性,因为指向weak_ptr参数的共享资源的引用计数将会自增(weak_ptr不影响共享资源的引用计数)。如果weak_ptr为空
(r.use_count()==0),shared_ptr 抛出一个类型为bad_weak_ptr的异常。
template <typename Y> shared_ptr(std::auto_ptr<Y>& r);
这个构造函数从一个auto_ptr获取r中保存的指针的所有权,方法是保存指针的一份拷贝并对auto_ptr调用release。构造后的引用计数为1。而r当然就变为空的。如果引用计数器不能分配成功,则抛出 std::bad_alloc。
~shared_ptr();
shared_ptr析构函数对引用计数减一。如果计数为零,则保存的指针被删除。删除指针的方法是调用operator delete 或者,如果给定了一个执行删除操作的客户化删除器对象,就把保存的指针作为唯一参数调用这个对象。析构函数不会抛出异常。
shared_ptr& operator=(const shared_ptr& r);
赋值操作共享r中的资源,并停止对原有资源的共享。赋值操作不会抛出异常。
void reset();
reset函数用于停止对保存指针的所有权的共享。共享资源的引用计数减一。
T& operator*() const;
这个操作符返回对已存指针所指向的对象的一个引用。如果指针为空,调用operator* 会导致未定义行为。这个操作符不会抛出异常。
T* operator->() const;
这个操作符返回保存的指针。这个操作符与operator*一起使得智能指针看起来象普通指针。这个操作符不会抛出异常。
T* get() const;
get函数是当保存的指针有可能为空时(这时 operator* 和 operator-> 都会导致未定义行为)获取它的最好办法。注意,你也可以使用隐式布尔类型转换来测试 shared_ptr 是否包含有效指针。这个函数不会抛出异常。
bool unique() const;
这个函数在shared_ptr是它所保存指针的唯一拥有者时返回 true ;否则返回 false。 unique 不会抛出异常。
long use_count() const;
use_count 函数返回指针的引用计数。它在调试的时候特别有用,因为它可以在程序执行的关键点获得引用计数的快照。小心地使用它,因为在某些可能的shared_ptr实现中,计算引用计数可能是昂贵的,甚至是不行的。这个函数不会抛出异常。
operator unspecified-bool-type() const;
这是个到unspecified-bool-type类型的隐式转换函数,它可以在Boolean上下文中测试一个智能指针。如果shared_ptr保存着一个有效的指针,返回值为True;否则为false。注意,转换函数返回的类型是不确定的。把返回类型当成bool用会导致一些荒谬的操作,所以典型的实现采用了safe bool
idiom
void swap(shared_ptr<T>& b);
这可以很方便地交换两个shared_ptr。swap 函数交换保存的指针(以及它们的引用计数)。这个函数不会抛出异常。
在以下情况时使用 shared_ptr :
- 当有多个使用者使用同一个对象,而没有一个明显的拥有者时
- 当要把指针存入标准库容器时
- 当要传送对象到库或从库获取对象,而没有明确的所有权时
- 当管理一些需要特殊清除方式的资源时
通过定制删除器的帮助。