Bootstrap

C++智能指针

智能指针

简介

什么是智能指针

 由于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都释放后,该资源才会进行释放其内部实现大概就是一个指针,并且还有一个引用计数器,当然还有其他方法,具体不一一分析。主要使用方法有以下几点要注意:

  1. 初始化

     共享智能指针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
    
    
  2. 获取原始指针

     如果智能指针管理的是一个对象,可以通过取出原始内存的地址进行操作,调用共享智能指针类提供的get()方法得到原始地址即可

    shared_ptr<T> ptr(new T());
    T *p = ptr.get(); // 获得传统 C 指针
    
  3. 指定删除器

     当智能指针引用计数变为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;
    }
    
注意事项
  1. 不能使用原始指针初始化多个shared_ptr

    int* p1 = new int;
    std::shared_ptr<int> p2(p1);
    std::shared_ptr<int> p3(p1);
    // 由于p2和p3是两个不同对象,但是管理的是同一个指针,这样容易造成空悬指针,
    // 比如p1已经delete了,这时候p2和P3里边的就是空悬指针了
    
  2. 不允许以暴露裸漏的指针进行赋值

    //带有参数的 shared_ptr 构造函数是 explicit 类型的,所以不能像这样
    std::shared_ptr<int> p1 = new int();//不能隐式转换,类型不匹配
    

    隐式调用它构造函数

  3. 不要使用shared_ptr的get()初始化另一个shared_ptr

    Base *a = new Base();
    std::shared_ptr<Base> p1(a);
    std::shared_ptr<Base> p2(p1.get());
    //p1、p2各自保留了对一段内存的引用计数,其中有一个引用计数耗尽,资源也就释放了,会出现同一块内存重复释放的问题
    
  4. 多线程中使用 shared_ptr

    ​ shared_ptr的引用计数本身是安全且无锁的,但对象的读写则不是,因为shared_ptr 有两个数据成员,读写操作不能原子化。shared_ptr 的线程安全级别和内建类型、标准库容器、std::string 一样,即:

    • 一个 shared_ptr 对象实体可被多个线程同时读取
    • 两个 shared_ptr 对象实体可以被两个线程同时写入,“析构”算写操作
      如果要从多个线程读写同一个 shared_ptr 对象,那么需要加锁
  5. 不要用栈中的指针构造 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>;
  1. 初始化

    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();//无效
    
  2. 删除器
    删除器同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对象也被析构,不会发生内存泄漏。

总结

  1. 不要使用std::auto_ptr(C++11已经废弃该用法)
  2. 当你需要一个独占资源所有权(访问权+生命控制权)的指针,且不允许任何外界访问,请使用std::unique_ptr
  3. 当你需要一个共享资源所有权(访问权+生命控制权)的指针,请使用std::shared_ptr
  4. 当你需要一个能访问资源,但不控制其生命周期的指针,请使用std::weak_ptr

一个shared_ptr应该要和n个weak_ptr搭配使用吗,而不是n个shared_ptr

;