智能指针
简介
什么是智能指针
由于C++中不存在垃圾回收机制,需要手动释放分配出去的内存,否则会造成内存泄漏。而智能指针(smart pointer)能够有效解决该问题。
智能指针是存储指向动态分配(堆)对象指针的类,用于生存期的控制,能够确保在离开指针所在作用域时,自动地销毁动态分配的对象,防止内存泄露。核心实现技术是引用计数,每使用它一次,内部引用计数加1,每析构一次内部的引用计数减1,减为0时,调用删除器(析构函数),并释放所指向的堆内存。
auto_ptr
复制auto_ptr对象时,把指针指传给复制出来的对象,原有对象的指针成员随后重置为nullptr。这说明auto_ptr是独占性的,不允许多个auto_ptr指向同一个资源。比如说:
void test(){
auto_ptr<int> a(new int(3));
auto_ptr<int> b = a;
if(a){
std::cout << "a = " << *a;
}
if(b){
std::cout << "b = " << *b;
}
}
上述输出为b = 3,此时a指针指向为nullptr,b指向值为3的地址块。
虽然它是c++11以前的最原始的智能指针,但是在c++11中已经被弃用(使用的话会被警告)了。unique_ptr,shared_ptr,weak_ptr是c++11新智能指针。
shared_ptr——共享智能指针
共享智能指针也被称为强引用指针,单个shared_ptr指向同一处资源,当所有的shared_ptr都释放后,该资源才会进行释放。其内部实现大概就是一个指针,并且还有一个引用计数器,当然还有其他方法,具体不一一分析。主要使用方法有以下几点要注意:
-
初始化
共享智能指针shared_ptr是一个模板类,如果要进行初始化,有三种方式:构造函数、std::make_shared辅助函数以及reset方法。
shared_ptr<T> ptr;//ptr 的意义就相当于一个 NULL 指针 shared_ptr<T> ptr(new T());//从new操作符的返回值构造 shared_ptr<T> ptr2(ptr1); // 使用拷贝构造函数的方法,会让引用计数加 //shared_ptr 可以当作函数的参数传递,或者当作函数的返回值返回,这个时候其实也相当于使用拷贝构造函数。 // make_shared 辅助函数创建 建议用该方式构造 std::shared_ptr<int> fo = std::make_shared<int> (10); // reset()函数,表示重置当前存储的指针。 shared_ptr<T> a(new T()); a.reset(); // 此后 a 原先所指的对象会被销毁,并且 a 会变成 NULL
-
获取原始指针
如果智能指针管理的是一个对象,可以通过取出原始内存的地址进行操作,调用共享智能指针类提供的get()方法得到原始地址即可。
shared_ptr<T> ptr(new T()); T *p = ptr.get(); // 获得传统 C 指针
-
指定删除器
当智能指针引用计数变为0时,这块内存会被智能指针所析构,释放所占的内存空间。析构操作其实是可以自定义的,在初始化智能指针时能够指定删除动作。这个删除操作对应的函数被称为删除器,函数的本质其实是一个回调函数,调用是由智能指针完成的。
注意:虽然 make_shared 是提倡的初始化 shared_ptr 的方法,但是 make_shared 没有办法让我们指定自己的删除器。
#include <iostream> using namespace std; // 当智能指针引用计数为0时,就会自动调用该删除器来删除对象 void myDelete(int* p) { delete p; // 既然自己提供了删除器来取代智能指针的默认删除器,那就有义务自己来删除所指向的对象 } int main() { shared_ptr<int> p1(new int(123456), myDelete); shared_ptr<int> p2(p1); p2.reset(); // reset后,p2被置nullptr,引用计数从2变为1 p1.reset(); // reset后,p1被置nullptr,引用计数从1变为0,此时调用我们的删除器myDelete释放所指向的对象 return 0; }
注意事项
-
不能使用原始指针初始化多个shared_ptr
int* p1 = new int; std::shared_ptr<int> p2(p1); std::shared_ptr<int> p3(p1); // 由于p2和p3是两个不同对象,但是管理的是同一个指针,这样容易造成空悬指针, // 比如p1已经delete了,这时候p2和P3里边的就是空悬指针了
-
不允许以暴露裸漏的指针进行赋值
//带有参数的 shared_ptr 构造函数是 explicit 类型的,所以不能像这样 std::shared_ptr<int> p1 = new int();//不能隐式转换,类型不匹配
隐式调用它构造函数
-
不要使用shared_ptr的get()初始化另一个shared_ptr
Base *a = new Base(); std::shared_ptr<Base> p1(a); std::shared_ptr<Base> p2(p1.get()); //p1、p2各自保留了对一段内存的引用计数,其中有一个引用计数耗尽,资源也就释放了,会出现同一块内存重复释放的问题
-
多线程中使用 shared_ptr
shared_ptr的引用计数本身是安全且无锁的,但对象的读写则不是,因为shared_ptr 有两个数据成员,读写操作不能原子化。shared_ptr 的线程安全级别和内建类型、标准库容器、std::string 一样,即:
- 一个 shared_ptr 对象实体可被多个线程同时读取
- 两个 shared_ptr 对象实体可以被两个线程同时写入,“析构”算写操作
如果要从多个线程读写同一个 shared_ptr 对象,那么需要加锁
-
不要用栈中的指针构造 shared_ptr 对象
int x = 12; std::shared_ptr<int> ptr(&x);
shared_ptr 默认的构造函数中使用的是delete来删除关联的指针,所以构造的时候也必须使用new出来的堆空间的指针。当 shared_ptr 对象超出作用域调用析构函数delete 指针&x时会出错。
unique_ptr——独占智能指针
独占智能指针其实算是auto_ptr的再版,都是独占资源的指针,内部实现相似;但是unique_ptr名字能更好的体现它的语义,而且在语法上比auto_ptr更为的安全,因为尝试赋值unique_ptr指针会在编译时指出错误,然而auto_ptr并不会(具体上面有提及过)。
如果需要转移独占权,那么可以使用std::move(std::unique_ptr)语法,但是,转移所有权后,仍可能存在出现原有指针调用的情况,造成运行时错误。
unique_ptr 在 memory
中的定义如下:
// non-specialized
template <class T, class D = default_delete<T>> class unique_ptr;
// array specialization
template <class T, class D> class unique_ptr<T[],D>;
-
初始化
std::unique_ptr<int>p1(new int(5)); std::unique_ptr<int>p2=p1;// 编译会出错 std::unique_ptr<int>p3=std::move(p1);// 转移所有权, 现在那块内存归p3所有, p1成为无效的针. p3.reset();//释放内存. p1.reset();//无效
-
删除器
删除器同shared_ptr类似
weak_ptr——弱引用智能指针
weak_ptr是为了辅助shared_ptr的存在,它只提供了对管理对象的一个访问手段,同时也可以实时动态地知道指向的对象是否存活。
只有某个对象的访问权,而没有它的生命控制权即是弱引用,所以weak_ptr是一种弱引用型指针。直白的说,就是只读类型,不可写!
weak_ptr解决循环引用
shared_ptr智能指针的循环引用导致的内存泄漏问题,可以通过weak_ptr解决。只需要将A或B的任意一个成员变量改为weak_ptr:
#include <iostream>
#include <memory>
using namespace std;
class A {
public:
std::weak_ptr<B> bptr;
~A() {
cout << "A is deleted" << endl;
}
};
class B {
public:
std::shared_ptr<A> aptr;
~B() {
cout << "B is deleted" << endl;
}
};
int main()
{
{
std::shared_ptr<A> ap(new A);
std::shared_ptr<B> bp(new B);
ap->bptr = bp;
bp->aptr = ap;
}
cout<< "main leave" << endl;
return 0;
}
上面代码中在对B的成员赋值时,即执行ap->bptr=bp时,由于bptr是weak_ptr,它并不会增加引用计数,所以bp的引用计数仍然会是1,在离开作用域之后,bp的引用计数为减为0,A指针会被析构,析构后其内部的aptr的引用计数会被减为0,然后在离开作用域后bp引用计数又从1减为0,B对象也被析构,不会发生内存泄漏。
总结
- 不要使用std::auto_ptr(C++11已经废弃该用法)
- 当你需要一个独占资源所有权(访问权+生命控制权)的指针,且不允许任何外界访问,请使用std::unique_ptr
- 当你需要一个共享资源所有权(访问权+生命控制权)的指针,请使用std::shared_ptr
- 当你需要一个能访问资源,但不控制其生命周期的指针,请使用std::weak_ptr
一个shared_ptr应该要和n个weak_ptr搭配使用吗,而不是n个shared_ptr。