十三.小心使用智能指针。
在前面几节已经很详细了介绍了智能指针适用方式。看起来,似乎智能指针很强大,能够很方便很安全的管理我们的资源。然而其实不然,如果不恰当的使用智能指针有时候会在很不起眼的地方造成内存泄漏。在这一节中主要介绍在使用智能指针过程中有哪些地方需要注意,以及 shared_ptr 在使用上的缺陷。 |
十四.使用智能指针的5个条款
- 条款1:不要把一个原生指针给多个shared_ptr或者unique_ptr管理
我们知道,在使用原生指针对智能指针初始化的时候,智能指针对象都视原生指针为自己管理的资源。换句话意思就说:初始化多个智能指针之后,这些智能指针都担负起释放内存的作用。那么就会导致该原生指针会被释放多次!!! |
int* ptr = new int;
shared_ptr<int> p1(ptr);
shared_ptr<int> p2(ptr);
//p1,p2析构的时候都会释放ptr,同一内存被释放多次!
- 条款2:不要把this指针交给智能指针管理
以下代码发生了什么事情呢?还是同样的错误。把原生指针 this 同时交付给了 m_sp 和 p 管理,这样会导致 this 指针被 delete 两次。 这里值得注意的是:以上所说的交付给m_sp 和 p 管理不对,并不是指不能多个shared_ptr同时占有同一类资源。shared_ptr之间的资源共享是通过shared_ptr智能指针拷贝、赋值实现的,因为这样可以引起计数器的更新;而如果直接通过原生指针来初始化,就会导致m_sp和p都根本不知道对方的存在,然而却两者都管理同一块地方。相当于”一间庙里请了两尊神”。 |
class Test{
public:
void Do(){ m_sp = shared_ptr<Test>(this); }
private:
shared_ptr<Test> m_sp;
};
int main()
{
Test* t = new Test;
shared_ptr<Test> p(t);
p->Do();
return 0;
}
- 条款3:如果不是通过new得到的动态资源内存请自定义删除器
以下代码试图将malloc产生的动态内存交给shared_ptr管理;显然是有问题的,delete 和 malloc 牛头不对马嘴!!! 所以我们需要自定义删除器[](int* p){ free(p); }传递给shared_ptr。 |
int main()
{
int* pi = (int*)malloc(4 * sizeof(int));
shared_ptr<int> sp(pi);
return 0;
}
- 条款4:尽量不要使用get()
智能指针设计者之处提供get()接口是为了使得智能指针也能够适配原生指针使用的相关函数。这个设计可以说是个好的设计,也可以说是个失败的设计。因为根据封装的封闭原则,我们将原生指针交付给智能指针管理,我们就不应该也不能得到原生指针了;因为原生指针唯一的管理者就应该是智能指针。而不是客户逻辑区的其他什么代码。 所以我们在使用get()的时候要额外小心,禁止使用get()返回的原生指针再去初始化其他智能指针或者释放。(只能够被使用,不能够被管理)。而下面这段代码就违反了这个规定: |
int main()
{
shared_ptr<int> sp(new int(4));
shared_ptr<int> pp(sp.get());
return 0;
}
- 条款5:尽量使用make_shared,不要把原生指针暴露出来
我们在定义shared_ptr智能指针的时候通常有3种方法: 1.先动态开辟内存,然后用局部变量接受指针。再把指针用于初始化。 2.直接在初始化参数中写new表达式。 3.使用make_shared函数。 实际应用中提倡使用第3中方法,第1种方法将原生指针暴露出来了,如果在外面的代码中不小心将该指针delete或者初始化其他的智能指针就会出现条款4的错误,所以这不是一个比较好的方法。第2种方法,直接在用new表达式作为实参,这样原生指针就匿名了。然而当你用new创建一个对象的同时创建一个shared_ptr时,这时会发生两次动态申请内存:一次是给使用new申请的对象本身的,而另一次则是由shared_ptr的构造函数引发的为资源管理对象分配的。与此相反,当你使用make_shared的时候,C++编译器只会一次性分配一个足够大的内存,用来保存这个资源管理者和这个新建对象。 下面是3种初始化shared_ptr的方法: |
int main()
{
{
//1.
int *p = new int(3);
shared_ptr<int> sp(p);
}
{
//2.
shared_ptr<int> sp(new int(3));
}
{
//3.
shared_ptr<int> sp = make_shared(3);
}
return 0;
}
十五.shared_ptr的陷阱(缺陷)
以上所述的一些需要注意的地方都是可以通过代码规范而避免的问题,而接下来要说的东西可能是shared_ptr天生的缺陷。 我们知道shared_ptr最引以为豪的就是其计数功能,实现了只有当无使用者才会释放掉内存。让我们使用起来管理内存十分方便,然而在使用过程中可能会不经意之间造成内存泄漏而且不容易查找。而这个问题就是 —— 循环引用。 一旦代码中出现了循环引用,那么基于计数的共享机制将会被彻底击败!先看下面的代码: |
class B;
class A
{
public:
shared_ptr<B> m_b;
};
class B
{
public:
shared_ptr<A> m_a;
};
int main()
{
{
shared_ptr<A> a(new A); //new出来的A的引用计数此时为1
shared_ptr<B> b(new B); //new出来的B的引用计数此时为1
a->m_b = b; //B的引用计数增加为2
b->m_a = a; //A的引用计数增加为2
}
//b先出作用域,B的引用计数减少为1,不为0;
//所以堆上的B空间没有被释放,且B持有的A也没有机会被析构,A的引用计数也完全没减少
//a后出作用域,同理A的引用计数减少为1,不为0,所以堆上A的空间也没有被释放
}
可以看出来以上的代码中A对象中指针引用B对象,B对象指针引用A对象,这样就导致了循环引用的出现。而代码运行到最后,由于两个指针计数都没有到0,所以资源无法释放导致了内存泄漏! 导致这样结果的原因就是:A和B都互相指着对方吼,“放开我的引用!“,“你先发我的我就放你的!”,于是悲剧发生了。 所以在使用基于引用计数的智能指针时,要特别小心循环引用带来的内存泄漏,循环引用不只是两方的情况,只要引用链成环都会出现问题。当然循环引用本身就说明设计上可能存在一些问题,如果特殊原因不得不使用循环引用,那可以让引用链上的一方持用普通指针(或弱智能指针weak_ptr)即可。 不过这个时候有人可能会说,真正业务逻辑中会出现这样的代码吗?答案是肯定的,举个简单的例子,链表!,一旦尾首相连形成循环链表的时候那么就出现了循环引用,所以使用shared_ptr的时候一定要先判断是否会出现循环引用。 |
前面几节基本上介绍了c++动态内存管理方案以及注意事项,后面我们会再花一节介绍一个例子来真正地将这些知识点用于实践! |