昨天把shared_ptr的源码分析了一遍,也看了一些有关shared_ptr分析的文章,今天自己来总结一下。
以独立语句将newed对象置入智能指针
对于下面这样一个使用智能指针的函数调用,可能会造成内存泄漏:
process(std::shared_ptr<int>(new int(10)), priority());
编译器没有规定传入参数的调用顺序,可能先执行new表达式,然后执行priority()函数,最后构造shared_ptr对象。如果priority()函数中抛出异常,那么new出来的内存将得不到释放。这会造成内存泄漏。
所以要使用下面两种方式:
方式一:
shared_ptr<int> p(new int(10));
process(p, priority());
方式二:
process(std::make_shared<int>(10), priority);
前者即是以独立语句将new分配过的对象置入只能指针,后者是利用工厂函数,直接生成只能指针。
小知识:make_shared()和普通shared_ptr(new xx)构造方法的区别
make_shared的效率更高。直接使用shared_ptr(new xx)底层实际上涉及两次内存分配,一次是为被管理的对象分配内存,一次是为ref_count控制块分配内存。而make_shared则是一次性分配好一大块内存来同时持有被管理的对象和ref_count控制块。增加了代码执行速度,且能避免一些控制块的薄记信息(内存cookie),潜在减少了程序占用的空间。不过make_shared有个缺点,使用make_shared会导致对象销毁和内存被回收之间可能会有延迟。因为使用make_shared相当于把被管理对象内存和ref_count控制块内存绑定在一起了,我们知道,ref_count控制块内存本来是由use_count和weak_count共同维护的,假设被管理对象引用计数为0了,但是仍然有weak_ptr存在,那么这一大块内存就不能销毁;而传统的shared_ptr(new xx)方式,当对象引用计数为0时,对象内存销毁。当weak_ptr引用计数为0时,ref_count控制块内存销毁,所有的内存都能够及时清理。
- 相比于使用new,make函数可以消除代码重复,提高异常安全。而且std::make_shared生成的代码更小更快。不适用make函数的场合包括自定义删除器和想要传递大括号初始值(这个暂时不太懂)。使用make函数不明智的场合包括:(1)自定义内存管理函数的类(2)内存紧张的系统,有非常大的对象,然后std::weak_ptr比std::shared_ptr长寿。
注意循环引用带来的内存泄漏
循环引用就是指,两个shared_ptr,你持有我,我持有你,双方都认为对方死了自己才能销毁,结果两方都无法销毁,造成内存泄漏。
示例代码:
class B;
class A {
public:
A(){
std::cout<<"A ctor"<<std::endl;
}
~A(){
std::cout<<"A dtor"<<std::endl;
}
void do_something() {
if(b_.lock()){
std::cout<<"lock success, still alive"<<std::endl;
std::cout<<"use_count="<<b_.use_count()<<std::endl; //输出1
}
}
public:
//std::shared_ptr<B> b_;
std::weak_ptr<B> b_;
};
class B {
public:
B(){
std::cout<<"B ctor"<<std::endl;
}
~B(){
std::cout<<"B dtor"<<std::endl;
}
public:
std::shared_ptr<A> a_;
};
int main()
{
std::shared_ptr<A> pa(new A);
std::shared_ptr<B> pb(new B);
std::cout<<"pb->use_count"<<pb.use_count()<<std::endl;
pa->b_ = pb;
pb->a_ = pa;
pa->do_something();
std::weak_ptr<B> pbb(pb);
std::cout<<(pbb.lock()).use_count()<<std::endl; //输出2
return 0;
我们可以使用weak_ptr来打破这个局面,而不是手动打破循环。一方持有另一方的shared_ptr(强引用),而另外一方则持有对方的weak_ptr(弱引用)。与之前的区别是,持有弱引用的一方在使用weak_ptr时,需要使用lock()方法,提升成为shared_ptr,提升失败说明对方已死,提升成功便可正常使用。这样就可以打破循环引用,避免内存泄漏。
代码中要注意的一点是,weak_ptr.lock()方法虽然会提升引用计数,但是如果我们仅仅把.lock()用作表达式,那么实际上是临时对象temp存在的时候引用计数为2,临时对象在表达式结束后立即死亡,引用计数又回到1。所以,我们如果要使用weak_ptr提升成为shared_ptr,必须用一个shared_ptr来接收,否则weak_ptr不会提升成为shared_ptr。
类向外传递this与shared_ptr
当我们在一个类的成员函数中,想知道自己this所在的shared_ptr时,不可以用shared_ptr(this)直接构造,因为这样会构造一个独立的局部shared_ptr,该shared_ptr销毁时会释放掉this。如果外部不知道该对象已经销毁,再调用就会爆炸。所以应该用shared_from_this,所在类需要继承std::enable_shared_from_this。
示例代码:
class A : public std::enable_shared_from_this<A> { //注意继承要加模板参数
public:
A() { std::cout<<"ctor"<<std::endl; }
~A() { std::cout<<"dtor"<<std::endl; }
public:
void func() {
//std::shared_ptr<A> local_sp(this); //错误用法
//std::cout<<local_sp.use_count()<<std::endl; //引用计数为1,因为这个局部的shared_ptr是用this单独生成的,和外部没有关系,所以会造成两次销毁doble delete
std::shared_ptr<A> local_sp = shared_from_this(); //正确用法
std::cout<<local_sp.use_count()<<std::endl; //引用计数为2
}
};
int main()
{
std::shared_ptr<A> sp(new A);
{
sp->func();
}
return 0;
}
上述错误情况输出是:
正确情况输出是:
可见,使用this在类内部再造一个shared_ptr是多么可怕的事情,会造成多次释放。
在多线程环境环境中,我们可能在成员函数中需要将this的智能指针回调给别的类对象,就可使用shared_from_this()。这样,别的对象就能够知道本对象是否还活着,避免在死亡之后的时候调用,产生可怕的后果。
shared_ptr的技术与陷阱
- 意外延长对象生命周期。可能容器持有shared_ptr,或者boost::function持有,这都会不经意间延长对象的生命期。我们可能会通过让他们持有weak_ptr来避免生命期被延长。
函数参数,一个线程只要在最外层持有一个实体,那么安全不成问题。比如:
void on_message(const string& msg){ shared_ptr<foo> f(new foo(msg)); //在最外层有一个实体,然后将shared_ptr传给谁都可以,因为始终有一个引用计数为1存在。常引用方式传递效率更高。 if(validate(f)){ //const reference save(f); //const reference } }
- 虚析构不再是必须。因为shared_ptr有T和Y两个参数。使用base类的shared_ptr接收derived的shared_ptr,ref_count中维护的指针类型仍是T,即base*类型。而shared_ptr类中维护的才是Y。删除器还是按老样子删除。所以不要虚析构也成。
有一张图可以看一下:
暂时完毕。