菜鸟笔记
提升您的技术认知

神兵利器shared_ptr unique_ptr weak_ptr

本系列的上一篇文章讲了手动方式的内存管理 new delete
这种方式往往存在很多问题  这一节将讲通过智能指针来自动管理动态内存

 智能指针的引入

智能指针的引入有以下几大原因

1.在动态内存管理中,如果new上一块空间,但是没有delete,就会产生内存泄露的问题。


2.但是有时候,我们new了,也delete了,但是还会出现问题。例如在new和delete之间调用了某个抛异常的函数,就有可能导致没有执行delete。

例如:fun2里面使用了new[],最后也使用了delete[],看着没有问题,但是在new和delete之间调用了fun1,而fun1里面抛了异常,但是在fun2的delete之前并没有捕获,就会导致delete没有执行,仍然会有内存泄露的问题。

void fun1()
{
      throw int(11);
}
void fun2()
{
      int* p = new int[10000];
      fun1();
      delete[] p;
}

int main()
{
      try
      {
           fun2();
      }
      catch (int& e)
      {
           cout << "捕获" << endl;
      }
      system("pause");
      return 0;
}

如果想要解决上面的问题,有一种方法:就是如果发现delete之前发现某个函数抛出了异常,就在delete之前捕获这个异常,并且在catch语句里面进行资源的释放,并且可以再将这个异常重新抛出。

void fun2()
{
      int* p = new int[10000];
      try
      {
           fun1();
      }
      catch(int& e)
      {
           delete[] p;
           cout << "重新抛出" << endl;
           throw;
      }
      delete[] p;
}

但是这种方法写着比较繁琐,而且如果代码很多,就有可能忘记哪个函数抛出了异常,会防不胜防。

3.还有一种场景就是内存空间是delete释放了以后  但是指针没有置NULL  (delete是不会把指针自动置为null的) 这样是野指针 

3.因此,只要有一种方法,能够在出了作用域之后能够自动释放掉申请的空间,就会让空间得到正确的释放。而一个对象出了作用域会自动调用自己的析构函数,只要在析构函数里能够释放自己开辟的空间,就能达到目的。

4.智能指针就有上面的作用,能够自动的管理指针所指向的动态资源的释放。它不仅有着RAII的思想还能够像指针一样。(RAII:分配资源即初始化,即构造函数分配资源和初始化资源,在析构函数清理资源。像指针一样:能够解引用)。

5.智能指针实质上就是一个模板类,成员变量包含一个任意类型的指针,构造函数获得资源并初始化,析构函数清理资源。
注意:智能指针只能管理动态开辟的空间。

 

智能指针的发展历史

C++98时代的智能指针

 

Boost库的智能指针

 

C++11时代的智能指针
 

unique_ptr
	
介绍


		unique_ptr是auto_ptr的继承者,对于同一块内存只能有一个持有者,而unique_ptr和auto_ptr唯一区别就是unique_ptr不允许赋值操作,也就是不能放在等号的右边(函数的参数和返回值例外),这一定程度避免了一些误操作导致指针所有权转移,然而,unique_str依然有提供所有权转移的方法move,调用move后,原unique_ptr就会失效,再用其访问裸指针也会发生和auto_ptr相似的crash,如下面示例代码,所以,即使使用了unique_ptr,也要慎重使用move方法,防止指针所有权被转移。

unique_ptr<int> up(new int(5));
//auto up2 = up; // 编译错误
auto up2 = move(up);
cout << *up << endl; //crash,up已经失效,无法访问其裸指针



除了上述用法,unique_ptr还支持创建动态数组。在C++中,创建数组有很多方法,如下所示:

// 静态数组,在编译时决定了数组大小
int arr[10];


// 通过指针创建在堆上的数组,可在运行时动态指定数组大小,但需要手动释放内存
int *arr = new int[10];


// 通过std::vector容器创建动态数组,无需手动释放数组内存
vector<int> arr(10);


// 通过unique_ptr创建动态数组,也无需手动释放数组内存,比vector更轻量化
unique_ptr<int[]> arr(new int[10]);



这里需要注意的是,不管vector还是unique_ptr,虽然可以帮我们自动释放数组内存,但如果数组的元素是复杂数据类型时,我们还需要在其析构函数中正确释放内存。


使用

 /***unique_ptr********/
    unique_ptr<int> uni_p1;//unique_ptr默认初始化
    unique_ptr<int> uni_p2(new int(42));//unique_ptr 普通初始化
        unique_ptr<int> uni_p4(new int(51));//unique_ptr 普通初始化
        unique_ptr<int> uni_p3(uni_p4.release());//unique_ptr 使用release初始化 
    //unique_ptr <int> uni_p2(uni_p1) 是错误的  不支持拷贝
    //unique_ptr<int> uni_p2; uni_p2=unoi_p1是错误的 不支持赋值
       *uni_p3=4;//unique_ptr解引用赋值  
       cout<<*uni_p3<<endl;//unique_ptr解引用取值
    cout<<"value of uni_p2="<<*uni_p2<<endl;
    swap(uni_p1,uni_p2);//unique_ptr交换两指针
    uni_p2.swap(uni_p1);//unique_ptr交换两指针
    uni_p2=nullptr;//unique_ptr 释放u指向的对象 将u置空
    int *temp_p3=uni_p1.release();//unique_ptr放弃对内存对象的控制权 返回普通指针 将uni_p1置空
    delete temp_p3;temp_p3=NULL;
    uni_p3.reset(new int(66));//unique_ptr释放原来所指向的内存对象 让其指向新给定的对象
    uni_p3.reset();//unique_ptr释放所指向的内存对象  将指针置空
   
shared_ptr


介绍

auto_ptr和unique_ptr都有或多或少的缺陷,因此C++11还推出了shared_ptr,这也是目前工程内使用最多最广泛的智能指针,他使用引用计数(感觉有参考Objective-C的嫌疑),实现对同一块内存可以有多个引用,在最后一个引用被释放时,指向的内存才释放,这也是和unique_ptr最大的区别。


另外,使用shared_ptr过程中有几点需要注意:

构造shared_ptr的方法,如下示例代码所示,我们尽量使用shared_ptr构造函数或者make_shared的方式创建shared_ptr,禁止使用裸指针赋值的方式,这样会shared_ptr难于管理指针的生命周期。
// 使用裸指针赋值构造,不推荐,裸指针被释放后,shared_ptr就野了,不能完全控制裸指针的生命周期,失去了智能指针价值
int *p = new int(10);
shared_ptr<int>sp = p;
delete p; // sp将成为野指针,使用sp将crash


// 将裸指针作为匿名指针传入构造函数,一般做法,让shared_ptr接管裸指针的生命周期,更安全
shared_ptr<int>sp1(new int(10));


// 使用make_shared,推荐做法,更符合工厂模式,可以连代码中的所有new,更高效;方法的参数是用来初始化模板类
shared_ptr<int>sp2 = make_shared<int>(10);


禁止使用指向shared_ptr的裸指针,也就是智能指针的指针,这听起来就很奇怪,但开发中我们还需要注意,使用shared_ptr的指针指向一个shared_ptr时,引用计数并不会加一,操作shared_ptr的指针很容易就发生野指针异常。
shared_ptr<int>sp = make_shared<int>(10);
cout << sp.use_count() << endl; //输出1
shared_ptr<int> *sp1 = &sp;
cout << (*sp1).use_count() << endl; //输出依然是1
(*sp1).reset(); //sp成为野指针
cout << *sp << endl; //crash


使用shared_ptr创建动态数组,在介绍unique_ptr时我们就讲过创建动态数组,而shared_ptr同样可以做到,不过稍微复杂一点,如下代码所示,除了要显示指定析构方法外(因为默认是T的析构函数,不是T[]),另外对外的数据类型依然是shared_ptr<T>,非常有迷惑性,看不出来是数组,最后不能直接使用下标读写数组,要先get()获取裸指针才可以使用下标。所以,不推荐使用shared_ptr来创建动态数组,尽量使用unique_ptr,这可是unique_ptr为数不多的优势了。
template <typename T>
shared_ptr<T> make_shared_array(size_t size) {
    return shared_ptr<T>(new T[size], default_delete<T[]>());
}


shared_ptr<int>sp = make_shared_array(10); //看上去是shared<int>类型,实际上是数组
sp.get()[0] = 100; //不能直接使用下标读写数组元素,需要通过get()方法获取裸指针后再操作


用shared_ptr实现多态,在我们使用裸指针时,实现多态就免不了定义虚函数,那么用shared_ptr时也不例外,不过有一处是可以省下的,就是析构函数我们不需要定义为虚函数了,如下面代码所示:
class A {
public:
    ~A() {
        cout << "dealloc A" << endl;
    }
};
class B : public A {
public:
    ~B() {
        cout << "dealloc B" << endl;
    }
};
int main(int argc, const char * argv[]) {
    A *a = new B();
    delete a; //只打印dealloc A
    shared_ptr<A>spa = make_shared<B>(); //析构spa是会先打印dealloc B,再打印dealloc A
    return 0;
}


循环引用,笔者最先接触引用计数的语言就是Objective-C,而OC中最常出现的内存问题就是循环引用,如下面代码所示,A中引用B,B中引用A,spa和spb的强引用计数永远大于等于1,所以直到程序退出前都不会被退出,这种情况有时候在正常的业务逻辑中是不可避免的,而解决循环引用的方法最有效就是改用weak_ptr,具体可见下一章。
class A {
public:
    shared_ptr<B> b;
};
class B {
public:
    shared_ptr<A> a;
};
int main(int argc, const char * argv[]) {
    shared_ptr<A> spa = make_shared<A>();
    shared_ptr<B> spb = make_shared<B>();
    spa->b = spb;
    spb->a = spa;
    return 0;
} //main函数退出后,spa和sp

在程序发生异常退出时,会销毁对象,如果使用new delete 程序在new delete之间发生异常退出 
只会销毁指针对象  斌不会释放指针对应的内存
而如果用智能指针在销毁对象后会自动把引用计数减一 ,  引用计数减到0的时候就销毁内存

使用
		    
/******shared_ptr*******/
    
    shared_ptr<int> p1;//shared_ptr默认初始化 保存为空指针
    shared_ptr<int> p2=make_shared<int>(5);//shared_ptr使用make_shared初始化,make_shared返回一个shared_ptr
   shared_ptr<int> p3(p2);//shared_ptr拷贝初始化
   shared_ptr<int> p4(new int(6));//shared_ptr使用new 来初始化
    int *p5=p4.get();//shared_ptr返回一个普通指针int * 注意后面不能delete p5 ,此处理解是p4和p5是独立创建的 各自引用技术为1,同时不能将两个shared_ptr绑定到一个get()返回的指针
    p1=p2;//shared_ptr赋值
    *p1=4;//shared_ptr解引用赋值  会同时影响别的共享在此对象上的指针
    cout<<"value of p2="<<*p2<<endl;
    swap(p1,p3);//shared_ptr交换两指针
    p3.swap(p1);//shared_ptr交换两指针
    p3.reset();//shared_ptr  释放一个shared_ptr 
    p1.reset(new int(6));//shared_ptr 令shared_ptr转移指向到内置指针
    cout<<p1.use_count()<<endl;//shared_ptr返回当前有几个指针指向共享对象
    cout<<p1.unique()<<endl;//shared_ptr当前若只有一个指针指向共享对象则返回true 否则false
    
weak_ptr

介绍

		weak_ptr来解决循环引用的问题,weak_ptr叫弱指针,它主要是为了配合shared_ptr使用,用来解决循环引用的问题;

将会出现循环引用问题的指针用weak_ptr保存着,weak_ptr并不拥有这块空间,所以weak_ptr里面不增加shared_ptr的引用计数,也不会释放这块空间。(注意weak_ptr里面也有自己的引用计数)

		正如上一章提到,使用shared_ptr过程中有可能会出现循环引用,关键原因是使用shared_ptr引用一个指针时会导致强引用计数+1,从此该指针的生命周期就会取决于该shared_ptr的生命周期,然而,有些情况我们一个类A里面只是想引用一下另外一个类B的对象,类B对象的创建不在类A,因此类A也无需管理类B对象的释放,这个时候weak_ptr就应运而生了,使用shared_ptr赋值给一个weak_ptr不会增加强引用计数(strong_count),取而代之的是增加一个弱引用计数(weak_count),而弱引用计数不会影响到指针的生命周期,这就解开了循环引用,上一章最后的代码使用weak_ptr可改造为如下代码。

class A {
public:
    shared_ptr<B> b;
};
class B {
public:
    weak_ptr<A> a;
};
int main(int argc, const char * argv[]) {
    shared_ptr<A> spa = make_shared<A>();
    shared_ptr<B> spb = make_shared<B>();
    spa->b = spb; //spb强引用计数为2,弱引用计数为1
    spb->a = spa; //spa强引用计数为1,弱引用计数为2
    return 0;
} //main函数退出后,spa先释放,spb再释放,循环解开了


使用weak_ptr也有需要注意的点,因为既然weak_ptr不负责裸指针的生命周期,那么weak_ptr也无法直接操作裸指针,我们需要先转化为shared_ptr,这就和OC的Strong-Weak Dance有点像了,具体操作如下:

shared_ptr<int> spa = make_shared<int>(10);
weak_ptr<int> spb = spa; //weak_ptr无法直接使用裸指针创建
if (!spb.expired()) { //weak_ptr最好判断是否过期,使用expired或use_count方法,前者更快
    *spb.lock() += 10; //调用weak_ptr转化为shared_ptr后再操作裸指针
}
cout << *spa << endl; //20

使用

		    /***unique_ptr********/
    unique_ptr<int> uni_p1;//unique_ptr默认初始化
    unique_ptr<int> uni_p2(new int(42));//unique_ptr 普通初始化
        unique_ptr<int> uni_p4(new int(51));//unique_ptr 普通初始化
        unique_ptr<int> uni_p3(uni_p4.release());//unique_ptr 使用release初始化 
    //unique_ptr <int> uni_p2(uni_p1) 是错误的  不支持拷贝
    //unique_ptr<int> uni_p2; uni_p2=unoi_p1是错误的 不支持赋值
       *uni_p3=4;//unique_ptr解引用赋值  
       cout<<*uni_p3<<endl;//unique_ptr解引用取值
    cout<<"value of uni_p2="<<*uni_p2<<endl;
    swap(uni_p1,uni_p2);//unique_ptr交换两指针
    uni_p2.swap(uni_p1);//unique_ptr交换两指针
    uni_p2=nullptr;//unique_ptr 释放u指向的对象 将u置空
    int *temp_p3=uni_p1.release();//unique_ptr放弃对内存对象的控制权 返回普通指针 将uni_p1置空
    delete temp_p3;temp_p3=NULL;
    uni_p3.reset(new int(66));//unique_ptr释放原来所指向的内存对象 让其指向新给定的对象
    uni_p3.reset();//unique_ptr释放所指向的内存对象  将指针置空
   

 

智能指针的底层原理