• 智能指针


    一、智能指针

    1.1为何使用智能指针

    首先我们先谈为何需要智能指针,C++11之前操作堆内存空间都是使用new,delete来维护,但是很容易造成new出来的内存忘记delete,开发人员需要大量时间维护修复。而new出来返回的指针也叫裸指针

    • 1)裸指针:直接用new返回的指针。这种指针强大,灵活,但是开发者需要全程负责维护,一不小心就容易出错,一旦用错将造成重大问题。
    • 2)智能指针:解决裸指针可能代码的各种问题。智能指针就理解成“裸指针”进行包装,给裸指针外边包了一层。它使用RAII技术,能自动根据对象的生命周期来自动释放,与unique_lock,guard_lock这些使用同样的技术

    1.2 智能指针的类型

      目前C++标准库有四种智能指针,分别是auto_ptr(C++98), unique_ptr(C++11), shared_ptr(C++11),weak_ptr(C++11)。
    其中:

    • 1)auto_ptr:目前auto_ptr已经完全被 unique_ptr取代,C++11中反对使用auto_ptr(弃用),绝对不能再使用。
    • 2)unique_ptr:独占式指针。同一时间只有一个指针能够指向该对象。当然,该对象的所有权还是可以移交出去,移交的方式有匿名对象(例如直接创建匿名对象赋值,或者间接使用返回值返回匿名对象),move函数
    • 3)shared_ptr:共享式指针。原理是利用引用计数的方法管理一片内存,每增加一个shared_ptr,count数加1,同理,每减少一个shared_ptr,count减1;这种引用也称为强引用。即多个指针指向同一个对象,最后一个指针被销毁时,这个对象会被释放。weak_ptr是辅助shared_ptr工作的。
    • 4)weak_ptr:看名字就知道,他是一个弱引用,他的存在是为了辅助shared_ptr的循环引用问题,它不占用引用数只能通过shared_ptr或者weak_ptr构造赋值没有重载 * 和 -> 运算符,因此不可以直接通过 weak_ptr类型访问对象,典型的用法是通过 lock() 成员函数来获得 shared_ptr,进而使用对象

    二、auto_ptr

    2.1 演示

    该智能指针已经被弃用,下面只是演示它所有权改变后不报错误的代码,实际上你编译的时候它也会提示你auto已经被弃用,但还能运行。

     1 #include <iostream>
     2 #include <string>
     3 #include <memory>
     4 using namespace std;
     5 
     6 /*
     7       auto_ptr<>的使用 已经被C++11抛弃 
     8       1 采用所有权模式 存在潜在的内存问题  任何情况别用
     9       这里只是演示它存在的问题
    10 */
    11 void test01(){
    12 
    13     auto_ptr<string> p1(new string("Hello World")); //开辟string对象并且用string的构造赋值 然后用智能指针p1指向 
    14     auto_ptr<string> p2;
    15 
    16     p2=p1;                          //将p1所有权给p2 p1被释放了 不能再使用
    17     for(int i=0;i<p2->size();i++){
    18         //cout<<p1->at(i); error
    19         cout<<p2->at(i);
    20     }
    21     cout<<endl;
    22 }
    23 /*
    24     总结:p2=p1后,p2夺走所有权,你看源码可以知道p1会在交换之后被释放掉了,
    25     所以在使用它就会相当于非法访问,但是他不会报错,这就是auto_ptr的问题。
    26 */
    27 
    28 
    29 int main(){
    30 
    31     test01();
    32     return 0;
    33 }

    结果

    1)p1所有权被代替后,仍使用p1就会报错。

    2)虽然更替后p2仍能使用,但存在的p1存在风险,开发人员可能不小心使用,所以绝对不能再使用auto_ptr。

    2.2 问题小结

    p2=p1后,p2夺走所有权,你看源码可以知道p1会在交换之后被释放掉了, 所以在使用它就会相当于非法访问,但是他不会报错,这就是auto_ptr的问题

    三、shared_ptr

    3.1 shared_ptr基础

    • 1)共享所有权,不是被一个shared_ptr拥有,而是被多个shared_ptr之间相互协作。shared有额外开销
    • 2)工作原理:利用引用计数的方法管理一片内存,每增加一个shared_ptr,count数加1,同理,每减少一个shared_ptr,count减1;这种引用也称为强引用
    • 3)最后一个指向该内存对象的shared_ptr在什么情况下,会释放该对象呢?
      • 这个shared_ptr被析构的时候。
      • 这个shared_ptr指向其他对象时。

    3.2 shared_ptr初始化

    shared_ptr的三种初始化方式:裸指针初始化、返回值初始化、make_shared函数初始化。

    3.2.1 通过裸指针与返回值初始化

     1 shared_ptr<int> make(int value){
     2     return shared_ptr<int>(new int(value));
     3 }
     4 
     5 void test02(){
     6     shared_ptr<int> pi1(new int(100)); //裸指针初始化,pi指向一个值为100的int型数据
     7     shared_ptr<int> pi2 = make(200);//返回值初始化
     8 
     9     cout<< (*pi1.get()) << endl;
    10     cout<< (*pi2.get()) << endl;
    11 }
    12 
    13 int main(){
    14 
    15     test02();
    16 
    17     return 0;
    18 }

    3.2.2 通过make_shared函数初始化

    3.2.2.1make_shared简介

    • 1)注意,裸指针可以初始化shared_ptr,但是不推荐,智能指针不要穿插用,否则会出问题。所以我们不建议使用返回值进行初始化,而是通过make_shared初始化
    • 2)make_shared是标准库里的函数模板,安全,高效的分配和使用shared_ptr,它能够在动态内存中,分配并初始化一个对象,然后返回指向此对象的shared_ptr。
     1 //1 性能更好(高效)
     2 /*
     3      ①性能更好(高效):用new来构造shared_ptr指针,那么new的过程是一次堆上面的内存分配,
     4      而在构造shared_ptr对象的时候,由于需要使用堆上面共享的引用计数(指针),又需要在堆上面
     5      分配一次内存,即需要分配两次内存,而如果用make_shared函数,则只需分配一次内存,
     6      所以性能会好很多。
     7      
     8      伪代码解释:下面可以看到,ptr传进来已经new一次,然后引用计数_pRefCount也会new一次
     9      造成两次分配内存。
    10 */
    11 template <class T>
    12 class SharedPtr
    13 {
    14 public:
    15     //
    16     SharedPtr(T* ptr = nullptr): _ptr(ptr), _pRefCount(new int(1)), _pMutex(new mutex){
    17         // 如果是一个空指针对象,则引用计数给0
    18         if (_ptr == nullptr)
    19             *_pRefCount = 0;
    20     }
    21 
    22 private:
    23     int*   _pRefCount; // 引用计数
    24     T*     _ptr;       // 指向管理资源的指针
    25     mutex* _pMutex;    // 互斥锁
    26 }
    27 
    28 //2 更安全
    29 /*
    30      ②更加安全:若我们使用裸指针对shared_ptr构造时,包含两步操作:
    31     (1)new一个堆内存。
    32     (2)分配一个引用计数区域管理该内存空间。
    33      但是并没有保证这两个步骤的原子性,当做了第(1)步,没有做第二步如果程序抛出了异常,
    34      将导致内存泄露,而make_shared内部有对这两个步骤合成一步进行处理,
    35      因此更推荐使用make_shared来分配内存。
    36 */
    37 
    38 //3 make_shared缺点
    39 /*
    40     虽然make_shared针对裸指针更好,但它也有缺点。
    41     ③缺点:make_shared一次性分配堆内存的做法,在释放的时候可能会导致内存延迟释放,
    42     因为如果有weak_ptr持有了指针,引用计数不会释放,而引用计数和实际的对象分配在同一块堆内存,
    43     因此无法将该对象释放,如果两块内存分开申请,则不存在这个延迟释放的问题。
    44 */
    45 
    46 //4 总结
    47 /*
    48     实际开发可能不会考虑这些,使用裸指针或者make_shared都行,但是大家一定要知道它们可能存在的问题。
    49 */

    3.2.2.2 make_shared优势

      1、性能更好(高效):用new来构造shared_ptr指针,那么new的过程是一次堆上面的内存分配,而在构造shared_ptr对象的时候,由于需要使用堆上面共享的引用计数(指针),又需要在堆上面
    分配一次内存,即需要分配两次内存,而如果用make_shared函数,则只需分配一次内存,所以性能会好很多

      2、更加安全:若我们使用裸指针对shared_ptr构造时,包含两步操作:
        (1)new一个堆内存。
        (2)分配一个引用计数区域管理该内存空间。
      但是并没有保证这两个步骤的原子性,当做了第(1)步,没有做第二步如果程序抛出了异常,将导致内存泄露,而make_shared内部有对这两个步骤合成一步进行处理,因此更推荐使用make_shared来分配内存

    3.2.2.3 make_shared缺点

      1、虽然make_shared针对裸指针更好,但它也有缺点。make_shared一次性分配堆内存的做法,在释放的时候可能会导致内存延迟释放,因为如果有weak_ptr持有了指针,引用计数不会释放,而引用计数和实际的对象分配在同一块堆内存,因此无法将该对象释放,如果两块内存分开申请,则不存在这个延迟释放的问题

      2、此外,需要注意,shared_ptr不支持隐式转换。

    1 //例1
    2 shared_ptr<int> pi2 = new int(200); //不可以,智能指针是explicit,不可以进行隐式类型转换,必须直接初始化形式
    3 
    4 //例2
    5 shared_ptr<int> make(int value){
    6     return new int(value);   //error,无法把new得到的int *转换成shared_ptr
    7     return shared_ptr<int>(new int(value))//正确写法
    8 }

    3.2.2.4 make_shared使用

    在上面介绍为何使用make_shared顶替裸指针初始化后,下面正式说明make_shared如何初始化shared_ptr。

     1 #include <iostream>
     2 #include <string>
     3 #include <memory>
     4 using namespace std;
     5 
     6 shared_ptr<int> make(int value){
     7     return shared_ptr<int>(new int(value));
     8 }
     9 
    10 void test02(){
    11     // shared_ptr<int> pi1(new int(100)); //pi指向一个值为100的int型数据
    12     // shared_ptr<int> pi2 = make(200);
    13     // cout<< (*pi1.get()) << endl;
    14     // cout<< (*pi2.get()) << endl;
    15 
    16     shared_ptr<int> p1 = make_shared<int>(100);
    17     shared_ptr<string> p2 = make_shared<string>(5, 'a'); //类似于string mystr(5, 'a')
    18     shared_ptr<int> p3 = make_shared<int>();//默认初值为0,cout<< (*p3.get()) << endl;
    19     p3 = make_shared<int>(300); //指向新int,p3首先释放指向值为0的内存,然后指向这个新的300的内存
    20     auto p4 = make_shared<string>("I love you");
    21 
    22     cout<< (*p1.get()) << endl;
    23     cout<< p2.get()->data() << endl;
    24     cout<< (*p3.get()) << endl;
    25     cout<< p4.get()->data() << endl;
    26 }
    27 
    28 int main(){
    29 
    30     //test01();
    31     test02();
    32 
    33     return 0;
    34 }

    结果

    3.3 shared_ptr的引用计数

      引用计数就是共用一片内存的shared_ptr个数。当一个shared_ptr开辟一个内存,引用计数为1,然后用该shared_ptr初始化其它shared_ptr那么就是共用一个内存,引用计数为2,以此类推。而当一个shared_ptr指向其它shared_ptr的内存,或者shared_ptr生命周期结束被析构时,引用计数都会减少

     1 #include <iostream>
     2 #include <string>
     3 #include <memory>
     4 using namespace std;
     5 
     6 void Func1(shared_ptr<int> a)
     7 {
     8     cout<<"函数1内:"<<endl;
     9     cout<<"值的引用数为: "<<a.use_count()<<endl;          // 2 调用完毕a被释放 引用计数减1
    10     cout<<"函数1结束。"<<endl;
    11 }
    12 
    13 shared_ptr<int> Func2(shared_ptr<int>& a)
    14 {
    15     cout<<"函数2内:"<<endl;
    16     cout<<"引用的引用数为: "<<a.use_count()<<endl;         // 1引用不会增加引用计数
    17     cout<<"函数2结束。"<<endl;
    18 
    19     return a;
    20 }
    21 
    22 //主要测试参数为值,引用和返回值对引用计数的影响
    23 void test03(){
    24     
    25     shared_ptr<int> sh1(new int(10));                  // 构造一个指向int类型对象的指针sh1,引用计数为1
    26     cout<<"Ref count: "<< sh1.use_count() << endl;
    27 
    28     {
    29         shared_ptr<int> sh2 = sh1;                     
    30         cout<<"Ref count: "<< sh1.use_count() << endl;
    31     }
    32     //sh2生命周期结束后
    33     cout<<"Ref count: "<< sh1.use_count() << endl;
    34     cout<<endl;
    35 
    36     //1 测试参数为值
    37     Func1(sh1);
    38     cout<<endl;
    39     
    40     //2 测试参数为引用
    41     Func2(sh1);
    42     cout<<endl;
    43 
    44     //3 测试接收匿名对象
    45     shared_ptr<int> sh3 = Func2(sh1);
    46     cout<<"sh3加入之后的引用数:"<<sh3.use_count()<<endl;
    47 
    48 }
    49 
    50 //测试引用计数增加减少
    51 void test04(){
    52     auto p1 = make_shared<int>(100);
    53     auto p2(p1);
    54     auto p3 = Func2(p2);
    55     cout<<"Ref count: "<< p1.use_count() << endl;
    56     p3 = make_shared<int>(200); //p3指向新对象,计数为1,p1、p2指向对象计数恢复为2;
    57     p2 = make_shared<int>(300); //p2指向新对象,计数为1,p1指向对象的计数恢复为1;
    58     cout<<"p3,p2改变指向后,Ref count: "<< p1.use_count() << endl;
    59 
    60     {
    61         shared_ptr<int> p4 = p1;                     
    62         cout<<"Ref count: "<< p1.use_count() << endl;
    63     }
    64     cout<<"p4生命周期结束后,Ref count: "<< p1.use_count() << endl;
    65 }
    66 
    67 int main(){
    68 
    69     //test01();
    70     //test02();
    71     test03();
    72 
    73     cout<<endl;
    74     cout<<"====test04 begin===="<<endl;
    75 
    76     test04();
    77 
    78     return 0;
    79 }

    结果

    从上面结果可以得出:

    • 1)传值会使引用计数加1,传引用则不会
    • 2)当返回值为shared_ptr类型,若我们使用变量接收,则引用计数会加1,不接收则不加。归根结底是因为返回值时,编译器会自动创建一个匿名对象返回。例如Func2(sh1);与shared_ptr sh3 = Func2(sh1)。
    • 3)使用同一片内存为其它shared_ptr赋值,会使引用计数加1。例如sh2 = sh1或者sh2(sh1);

    上面是针对于会使引用计数增加的总结。下面总结引用计数减少的。

    • 1)改变指向其它内存的shared_ptr会减1
    • 2)生命周期结束的shared_ptr会减1。当最终引用计数减为0时,就会释放该内存

    3.4 shared_ptr的其它成员函数

    3.4.1 use_count()函数

    use_count()是获取该片内存有多个shared_ptr个对象正在引用。

    3.4.2 unique()函数

    unique:该智能指针是否独占某个指向的对象,也就是若只有一个智能指针指向某个对象,则unique()返回true,多个返回fasle。(为空时也不属于独享)

    3.4.3 reset()与shared_ptr的比较

    reset()函数分为无参与有参的使用。

    • 1)无参时,调用该函数的shared_ptr对象的引用计数为0,而该片内存并不为0,只是减1
    • 2)有参时,调用该函数的shared_ptr对象指向新的内存,引用计数加1。而原本的内存减1
    • 3)由于reset被置空后,一般需要比较,所以将shared_ptr的比较放在reset一起,当然也可以使用use_count代替比较来判断是否被置空。
    • 4)下面例子可以看到,空指针也可以通过reset重新初始化。即sh1。

    看例子。

     1 //shared_ptr的比较 与 reset函数方法
     2 void test05(){
     3 
     4     shared_ptr<int> sh1=make_shared<int>(3);                 
     5     cout<<sh1.use_count()<<endl;                             // count=1
     6 
     7     shared_ptr<int> sh3=sh1;                                 
     8     cout<<sh1.use_count()<<endl;                             // count=2
     9 
    10     //1 比较 重载了==与!=运算符  实际上当成指针比较就好了
    11     if(sh1!=NULL && sh3!=NULL){
    12         cout<<"sh1和sh3不为空!"<<endl;
    13     }
    14 
    15     //2.1 无参reset函数  使调用的shared_ptr指向一个空资源 只是该shared_ptr的引用计数变为0 其他的因为sh1调用reset而减1 
    16     sh1.reset();                                             // 使sh1指向的count为0
    17     cout<<sh1.use_count()<<endl;                             // count=0
    18     if (sh1 == nullptr){
    19         cout << "sh1被置空" << endl;
    20     }
    21     cout<<sh3.use_count()<<endl;                             // count=1
    22 
    23     //2.2 有参reset 使shared_ptr指向参数new出的资源对象
    24     sh1.reset(new int(5));                                   // sh1指向该参数new出的资源
    25     cout<<sh1.use_count()<<endl;                             // count=1
    26 
    27 }

    结果

    3.4.4 解引用的意思

    解引用就是获取该裸指针的对象。

    1 shared_ptr<int> p(new int(123));
    2 cout << *p << endl;

    3.4.5 get()

    get():获取裸指针操作。考虑到有些函数的参数需要的是一个内置裸指针,而不是智能指针。例如上面的初始化使用过get函数。

    3.4.6 swap()

    swap():交换两个智能指针所指向的对象。

    1 shared_ptr<string> ps1(new string("1111111"));
    2 shared_ptr<string> ps2(new string("2222222"));
    3 std::swap(ps1, ps2); //交换ps1指向222222222
    4 ps1.swap(ps2);    //在交换ps1指向11111111

    3.5 自定义删除回调函数(删除器)

    分析:当传给shared_ptr构造函数不止一个对象时,例如是一个对象数组时,因为shared_ptr析构默认用的是delete删除器,只能delete掉一个对象,所以我们需要自定义回调函数,用于析构传进的对象数组

    3.5.1 使用函数模板作为自定义删除器

     1 //用来释放malloc出来的函数对象
     2 template<class T>
     3 class FreeFunc{
     4 public:
     5     void operator()(T* ptr)
     6     {
     7         cout << "free:" << ptr << endl;
     8         free(ptr);
     9     }
    10 };
    11 
    12 //用来释放new[]出来的函数对象
    13 template<class T>
    14 class DeleteArrayFunc {
    15 public:
    16     void operator()(T* ptr)
    17     {
    18         cout << "delete[]" << ptr << endl;
    19         delete[] ptr;
    20     }
    21 };
    22 
    23 //用来释放文件描述符的函数对象
    24 template<class T>
    25 class ClosefdFunc{
    26 public:
    27     void operator()(T* fd)
    28     {
    29         cout << "close fd" << fd << endl;
    30         fclose(fd);
    31     }
    32 };
    33 
    34 void test06(){
    35 
    36     FreeFunc<int>        Object1;
    37     shared_ptr<int>      sp1((int*)malloc(sizeof(int)*4), Object1);         // 回调函数是可调用对象,可以是普通的函数名或者函数对象或者lambda表达式
    38 
    39     DeleteArrayFunc<int> Object2;
    40     shared_ptr<int>      sp2(new int[4], Object2);
    41 
    42     ClosefdFunc<FILE>    Object3;
    43     shared_ptr<FILE>     sp3(fopen("myfile.txt","w"), Object3);
    44 
    45 }

    结果看到,三个自定义析构函数均被调用,并且可以发现文件对象会被编译器优先释放。

    3.5.2 使用其它新特性作为删除器

    • 1)可以用default_delete来做删除器,这个是标准库里的模板类。
      1 class A {
      2 public:
      3     A() {};
      4     ~A() {};
      5 };
      6  
      7 void test07(){
      8     shared_ptr<A> p(new A[10], std::default_delete<A[]>());
      9 }
    • 2)可以用C++新规则的简单写法。
      1 void test08(){
      2     shared_ptr<A[]> p1(new A[10]);//A类在上面
      3     shared_ptr<int[]> p2(new int[10]);
      4     p2[0] = 12;
      5     p2[1] = 15;
      6 
      7     cout<<p2[0]<<endl;//输出12
      8     cout<<p2[1]<<endl;//输出15
      9 }

    3.5.3 模板+lambda表达式实现开发时常用的删除器

    由于我们开发时很少使用到像5.1这种模板,因为比较长,且需要额外定义对象传入,所以我们开发时更倾向使用lambda表达式,代码更少。但是并不是不能使用5.1这种方法。

    • 1)不带删除数组的模板+lambda表达式删除器。
       1 template <class T>
       2 shared_ptr<T> my_make_shared(){
       3     return shared_ptr<T>(new T,[](T *ptr){delete ptr,ptr=nullptr;});
       4 }
       5 
       6 void test09(){
       7     //对于类中没有成员的,只有使用裸指针
       8     shared_ptr<int> sh1 = my_make_shared<int>();
       9     *sh1.get() = 2;
      10     cout<<*sh1.get()<<endl;
      11 
      12     //对于类中有成员的,可以直接使用智能指针
      13     shared_ptr<A> sh2 = my_make_shared<A>();
      14     sh2->SetI(100);
      15     cout<<sh2->GetI()<<endl;
      16 }
    • 2)带删除数组的模板+lambda表达式删除器。下面我将lambda表达式换成default_delete函数,因为上面已经有lambda,当然你也可以换一下。
       1 template<typename T>
       2 shared_ptr<T> my_make_shared_array(size_t size)
       3 {
       4     return shared_ptr<T>(new T[size], default_delete<T[]>());
       5 }
       6 void test10(){
       7     shared_ptr<A> parray = my_make_shared_array<A>(15);
       8     parray->SetI(200);
       9     //parray.get()[0].SetI(100);//也行,但直接使用智能指针更安全
      10     cout<<parray.get()[0].GetI()<<endl;//输出200
      11 }

    3.5.4 指定删除器额外说明

      就算是两个shared_ptr指定了不同的删除器,只要他们所指向的对象类型相同,那么这两个shared_ptr也是属于同一类型,所以它们可以改变指向,改变指向后,若引用计数变为0,则先用旧的删除器删除内存,然后它将使用新的删除器

     1 auto lambda1 = [](int *p)
     2 {
     3     delete p;
     4 };
     5  
     6 auto lambda2 = [](int *p)
     7 {
     8     delete p;
     9 };
    10  
    11  
    12 shared_ptr<int> p1(new int(100), lambda1);
    13 shared_ptr<int> p2(new int(100), lambda2);
    14 p2 = p1; //p2会先调用lambda2把自己所指向的对象释放,然后指向p1所指向的对象。p1所指向 
    15          //的对象引用计数为2,整个main执行完成后还会调用lambda1来释放p1,p2共同指向的 
    16          //对象

    3.5.5小结

    并且注意,我们5.3常用删除迭代器为何不使用make_shared,因为使用make_shared这种方法我们无法指定自己的删除器。所以只能使用new。

    3.6 enable_shared_from_this模板类详解

      什么时候该使用enable_shared_from_this模板类?当我们需要一个类对象返回本身并且该类使用了shared_ptr智能指针时,就需要使用enable_shared_from_this。并且需要注意,当我们使用智能指针管理资源时,必须统一使用智能指针,而不能再某些地方使用智能指针,某些地方使用原始指针,否则不能保持智能指针的语义,从而产生各种错误

    3.6.1 错误使用this返回对象本身

    错误代码如下:

     1 class Test
     2 {
     3 public:
     4     //析构函数
     5     ~Test() { std::cout << "Test Destructor." << std::endl; }
     6 
     7     //获取指向当前对象的指针
     8     shared_ptr<Test> GetObject(){
     9         return shared_ptr<Test>(this);
    10     }
    11 };
    12 
    13 void test11(){
    14     {
    15         shared_ptr<Test> p(new Test());
    16         shared_ptr<Test> q = p->GetObject();
    17         cout<<endl;
    18     }
    19 }

    结果,原因是析构了两次new出来的Test:

      为什么会这样呢?原因出在GetObject函数。它里面使用this给shared_ptr赋值,这就会导致原本shared_ptr p(new Test())时,该内存已经被p管理。当调用至shared_ptr(this),返回的匿名对象会被编译器再次用其创建新的匿名对象返回,然而,该匿名对象是由裸指针this赋值的这就导致p与q各自引用相同的内存,但是引用计数都是1,因为p与q这两个shared_ptr是没有任何关系的,所以当p与q生命周期结束后,必定使同一片内存被释放两次导致报段错误
    实际上上面的错误就是下面例子。

    1 int *p = new int(12);
    2 shared_ptr<int> p1(p);
    3 shared_ptr<int> p2(p);//必定报错,原因是两个没有任何关系的shared_ptr绑定同一片内存,导致释放两次。

    3.6.2 改正this,使用enable_shared_from_this模板类返回对象本身

      上面是错误的绑定this来获取shared_ptr即获取对象本身。那么有什么方法从一个类的成员函数中获取当前对象的shared_ptr呢,其实方法很简单:只需要该类继承至enable_shared_from_this模板类,然后调用该基类enable_shared_from_this中的shared_from_this即可。
    正确代码:

     1 class Test: public enable_shared_from_this<Test>//修改1
     2 {
     3 public:
     4     //析构函数
     5     ~Test() { std::cout << "Test Destructor." << std::endl; }
     6 
     7     //获取指向当前对象的指针
     8     shared_ptr<Test> GetObject(){
     9         //return shared_ptr<Test>(this);
    10         return shared_from_this();//修改2
    11     }
    12 };
    13 
    14 void test11(){
    15     {
    16         shared_ptr<Test> p(new Test());
    17         shared_ptr<Test> q = p->GetObject();
    18         cout<<endl;
    19     }
    20 }

    那为什么继承enable_shared_from_this后就可以正常使用呢?所以我们需要分析enable_shared_from_this这个类了,继续往下看。

    3.6.3 分析enable_shared_from_this源码

    为了方便理解,我们使用boost库的enable_shared_from_this类模板源码,实际上标准库的也差不多,标准库使用友元处理,而boost库使用public处理。
    enable_shared_from_this.hpp文件,enable_shared_from_this模板类的实现如下:

     1 template<class T> class enable_shared_from_this
     2 {
     3 protected:
     4     enable_shared_from_this() BOOST_NOEXCEPT
     5     {
     6     }
     7     enable_shared_from_this(enable_shared_from_this const &) BOOST_NOEXCEPT
     8     {
     9     }
    10     enable_shared_from_this & operator=(enable_shared_from_this const &) BOOST_NOEXCEPT
    11     {
    12         return *this;
    13     }
    14     ~enable_shared_from_this() BOOST_NOEXCEPT // ~weak_ptr<T> newer throws, so this call also must not throw
    15     {
    16     }
    17 public:
    18     shared_ptr<T> shared_from_this()
    19     {
    20         shared_ptr<T> p( weak_this_ );
    21         BOOST_ASSERT( p.get() == this );
    22         return p;
    23     }
    24     shared_ptr<T const> shared_from_this() const
    25     {
    26         shared_ptr<T const> p( weak_this_ );
    27         BOOST_ASSERT( p.get() == this );
    28         return p;
    29     }
    30 public: // actually private, but avoids compiler template friendship issues
    31     // Note: invoked automatically by shared_ptr; do not call
    32     template<class X, class Y> void _internal_accept_owner( shared_ptr<X> const * ppx, Y * py ) const
    33     {
    34         if( weak_this_.expired() )
    35         {
    36             weak_this_ = shared_ptr<T>( *ppx, py );
    37         }
    38     }
    39 private:
    40     mutable weak_ptr<T> weak_this_;
    41 };

      首先我们看到它有三个构造函数,而实际的public公有方法即能被调用的函数只有三个,其中_internal_accept_owner还被注明不能调用。所以该类非常简单,着重看不加const的shared_from_this(或者const的shared_from_this函数)外加私有成员weak_this_即可。
    然后我们分析:

    • 1)如果想调用shared_from_this返回shared_ptr,那么就必须要初始化weak_this_ ,因为shared_ptr是由weak_this_赋值创建的,这里解释一下为什么使用若引用weak_ptr类型作为成员:因为弱引用不占引用计数,若我们使用shared_ptr强引用,那么该成员就必定占用一个计数,就会导致该内存本该由1->0释放,但是由于该成员的存在导致无法释放,造成内存延时释放,与逻辑不匹配,所以必须使用弱引用作为成员
    • 2)接着上面讲,因为shared_ptr是由weak_this_赋值创建的,所以weak_this_必须被初始化,而整个类中只有_internal_accept_owner是初始化weak_this_的,该函数由shared_ptr构造时自动调用。所以目前逻辑就清楚了,只有shared_ptr构造被完整调用后,weak_this_才有值,然后shared_from_this才能被成功返回。但是注意,构造初始化的顺序刚好与这个顺序有点相反。例如:
      1 shared_ptr<Test> p(new Test());

      首先进入shared_ptr,但是会先去执行Test的构造,然后又因为enable_shared_from_this是Test的基类,所以最终先去执行完enable_shared_from_this的构造,再返回Test的构造执行完,最后返回shared_ptr的构造执行完。但是我们写代码时只需要记住必须只有shared_ptr被先执行,才能进入Test与enable_shared_from_this的构造。而不能越过shared_ptr的构造直接调用Test的构造和enable_shared_from_this的构造,这必然是错误的,因为没有shared_ptr的构造初始化weak_this_,shared_from_this返回的p肯定是非法的。

    3.6.4 继承于enable_shared_from_this后常见地错误

    所以我们可以根据上面所说,列出常见继承于enable_shared_from_this类后,使用错误的代码。

    3.6.4.1 错误1

    1 class Test : public enable_shared_from_this<Test>
    2 {
    3     Test() { shared_ptr<Test> pTest = shared_from_this(); }
    4 };

      这种写法必然是错误的,上面已经说过,不能在构造shared_ptr完成之前调用shared_from_this,而enable_shared_from_this的构造和Test的构造必定在shared_ptr构造函数完成之前,所以shared_ptr的构造函数必定未执行完毕,也就是说weak_this_未被初始化,所以shared_from_this返回的是一个未知状态值。

    有人就会说,那在pTest的构造函数之前创建一个shared_ptr不就行了吗,好,那我们就看错误2。

    3.6.4.2 错误2

    1 Test() {
    2     shared_ptr<Test> sh(new Test());
    3     shared_ptr<Test> pTest = sh->shared_from_this();
    4       //shared_ptr<Test> pTest = shared_from_this(); 
    5 }

      错误2这种情况实际上我真的不想列出来,当我们在Test构造中new一个Test本身,就会造成递归死循环调用该构造,根本就不会再往下执行,并且死循环过程中出现段错误,原因我猜可能是循环不断new,而没有释放内存,编译器认为出问题直接报错,因为我测试过不断new而不释放内存最终导致死机的情况。所以这种情况不再多说。

    3.6.4.3 错误3

     1 class Test : public enable_shared_from_this<Test>
     2 {
     3     void func() { shared_ptr<Test> pTest = shared_from_this(); }
     4 };
     5 int main()
     6 {
     7     Test test;
     8     test.func();    //错误
     9     // Test *pTest = new Test;
    10     // pTest->func(); //同理错误
    11 }

      上面错误3的代码实际上错误1也是一样,都是没有执行完整shared_ptr的构造函数,导致weak_this_未被初始化。gdb调试结果也报错weak_this_未被初始化。

      正确的写法就好像我们第2大点的调用例子,必须先调用shared_ptr执行完整的shared_ptr构造函数初始化weak_this_ 后,才能调用shared_from_this。
    正确写法:
      注意:func是返回void,我们只是在函数体内测试是否能获取shared_ptr pTest = shared_from_this(),实际上我们获取本身直接将shared_from_this返回即可,当然返回值也需要换换。

     1 class Test : public enable_shared_from_this<Test>
     2 {
     3     void func() { shared_ptr<Test> pTest = shared_from_this(); }
     4 };
     5 
     6 int main()
     7 {
     8     shared_ptr<Test> pTest( new Test() );
     9     pTest->func();
    10 }

    3.6.5 总结enable_shared_from_this的用法

    • 1)实际上enable_shared_from_this的用法非常简单,就是像第2大点那样继承,然后使用即可。只不过理解透需要点时间。
    • 2)获取本类对象本身,必须先调用shared_ptr p(new Test())执行完整个shared_ptr的构造函数,才能调用shared_from_this获取对象本身,而不能直接调用Test test这些自定义类对象的构造后,就调用shared_from_this,这是错误的

    3.7 智能指针之shared_ptr易错点

    3.7.1 慎用裸指针给shared_ptr赋值

    3.7.1.1 例1

     1 class A {
     2 public:
     3     A() {};
     4     A(int i) {
     5         m_i=i;
     6         cout<<"A"<<endl;
     7     };
     8     void SetI(int i) {m_i=i;};
     9     int GetI(){return m_i;}
    10     ~A() {
    11         cout<<"~A"<<endl;
    12     };
    13 private:
    14     int m_i;
    15 };
    16 
    17 void proc1(shared_ptr<int> ptr)
    18 {
    19     return;
    20 }
    21 
    22 void proc2(shared_ptr<A> ptr)
    23 {
    24     return;
    25 }
    26 
    27 void test13()
    28 {
    29     int *p1 = new int(100);
    30     //proc(p1); //语法错误,int *p1不能转换成shared_ptr<int>(不支持隐式转换)
    31     proc1(shared_ptr<int>(p1)); 
    32     *p1 = 45; //潜在的不可预料的问题,因为p已经被释放了
    33 }
    34 
    35 void test14(){
    36     A *p2 = new A(100);
    37     proc2(shared_ptr<A>(p2));//A构造只会被调用一次执行完proc2
    38     *p2 = 50;//错误1,系统会帮你重新开辟A对象,但是仍会报错,这就是不可预料的问题之一
    39     //p2->SetI(45); //错误2,系统未重新开辟对象
    40 }

      当我界面直接断点调试test13时,它执行到 *p1 = 45;没有报错,但是程序最终结束时却卡死,说明程序出现问题。然后我们又通过gdb调试一遍(命令行模式),它的错误就明显报错了。

      然后我又测试了test14,是shared_ptr存储自定义对象的。结果发现又出现不同的错误或称不可预料的错误,当p2在调完proc2函数后,p2就被释放了,我再次违法使用*p2=50时,系统会重新开辟A内存,但是最终也会报错。

    而测试p2->SetI(45);错误时与test13错误一样。

    3.7.1.2 例2

    同上面一样,均是用一个裸指针给多个shared_ptr赋值,导致释放多次。

    1 int *p = new int(12);
    2 shared_ptr<int> p1(p);
    3 shared_ptr<int> p2(p);   //不可以!!!!这种写法p1和p2无关联,会导致p1和p2所指向的 
    4                          //内存被释放两次,产生异常

    正确的用裸指针赋值应该是下面的方法:

    1 shared_ptr<int> p1(new int());
    2 shared_ptr<int> p2(p1);   //这种写法可以,p1与p2用的是同一个控制块,两者互通

    所以我们一定要慎用用裸指针赋值,或者说,使用裸指针赋值的方式只能用上面正确的写法。

    3.7.2 慎用get()返回的指针(智能指针返回对应的裸指针)

    1 shared_ptr<int> myp(new int(122));
    2 int *p = myp.get();
    3 delete p;   //不可以释放!!!!!!!!!!!!!!!
    4  
    5 shared_ptr<int> myp2(p); //不可以!!!myp和myp2的引用计数都为1,实际这种错误就是上面多次使用裸指针赋值

    也就是我们一旦使用智能指针管理对象,之后就最好不要对原来的裸指针进行操作,否则导致各种错误,如二次释放内存。

    3.7.3 禁用使用this给shared_ptr赋值,然后返回类对象本身

    这种错误本来属于第1大点的错误点,即使用一个裸指针给多个shared_ptr赋值,但是由于返回类对象本身比较重要,所以单独拿出来分析。
    错误写法:

     1 class CT
     2 {
     3 public:
     4     shared_ptr<CT> getself()
     5     {
     6         return shared_ptr<CT>(this); //该this已经被一个shared_ptr管理,但是现在却又被新的shared_ptr管理,必然报错
     7     }
     8 };
     9  
    10 shared_ptr<CT> pct1(new CT());
    11 shared_ptr<CT> pct2 = pct1->getself(); //不可以pct1与pct2虽然指向同一内存,但是pct1和pct2两个毫无关系,引用计数均为1

      pct1与pct2虽然指向同一块内存,但是二者毫无关系,各自的引用计数都是1,所以在离开他们的作用域时都会进行内存释放,这就导致了同一内存的二次释放。

      正确写法应该是使用enable_shared_from_this类模板返回。
      enable_shared_from_this的工作原理:enable_shared_from_this有一个弱指针,当我们创建了一个share_ptr,该弱指针就会被初始化赋值,但是引用数不会加1,然后当我们调用shared_from_this时,就能返回通过weak_ptr赋值的share_ptr。这样就可以获取到对象本身

     1 class CT: enable_shared_from_this<CT>//必须继承enable_shared_from_this
     2 {
     3 public:
     4     shared_ptr<CT> getself()
     5     {
     6         return shared_from_this(); //正确写法
     7    }
     8 };
     9  
    10 shared_ptr<CT> pct1(new CT());
    11 shared_ptr<CT> pct2 = pct1->getself(); 

    3.7.4 shared_ptr的循环引用问题

     1 //3.2 shared_ptr的循环引用
     2 class Children;    //声明
     3 
     4 class Parent
     5 {
     6 public:
     7     ~Parent()
     8     {
     9         cout << "Parent    destructor" << endl;
    10     }
    11 
    12     shared_ptr<Children> s_children1;
    13 };
    14 
    15 class Children
    16 {
    17 public:
    18     ~Children()
    19     {
    20         cout << "Children  destructor" << endl;
    21     }
    22     shared_ptr<Parent> s_parent1;
    23 };
    24 
    25 void test04()
    26 {
    27     shared_ptr<Parent>   s_parent(new Parent());      //1 调用拷贝对象构造 使s_parent指向Parent共享资源
    28     shared_ptr<Children> s_children(new Children());  //调用拷贝对象构造 使s_children指向Children共享资源
    29     if(s_parent && s_children)                        //两个对象成功创建的话
    30     {
    31         s_parent   -> s_children1 = s_children;       //2 使对象成员也指向Children共享资源
    32         s_children -> s_parent1   = s_parent;         //使对象成员也指向Parent共享资源
    33     }
    34 
    35     cout << "s_parent use_count: " << s_parent.use_count() << endl;     // 2
    36     cout << "s_children use_count: " << s_children.use_count() << endl; // 2
    37 }

      执行上面程序可以知道,析构并没有被调用,内存泄漏,解决方法是将成员变量其中一个改成weak_ptr,因为weak_ptr不占用引用数。以第一个变成weak_ptr为例,当两个shared对象释放时,成员对象s_children1不占用引用数,s_children(图下面那个)引用的count从1变成0,所以他可以调用析构函数析构掉它;而被析构时,他的成员变量s_parent1也会被析构,由于shared对象释放时减了1,所以count引用也变成了0,两个内存被释放,这样就解决了这个循环引用问题。

       构造执行了,且Children对象先被析构。因为s_children指向对象的引用先变为0先嘛。若改成第二个为weak,那么就s_parent指向的对象被析构,即Parent对象。

    3.7.5 补充说明和使用建议

    3.7.5.1 建议1

      移动语义是可以使用的,但是注意不要乱用,因为它所有权被夺走后相当于空。

     1 void test15(){
     2     shared_ptr<int> p1(new int(100));
     3     //移动语义,夺走p1内存所有权给一个新的智能指针p2管理,移动后p1就不能再管理该内存(变成空),引用计数依旧是1
     4     shared_ptr<int> p2(std::move(p1)); 
     5     //*p1 = 50;//报错
     6     if(p1 == nullptr){
     7         cout<<"p1被置空,count=" << p1.use_count() << endl;
     8     }
     9     if(p2 != nullptr){
    10         cout<<"p2通过move夺走p1所有权, count=" << p2.use_count() << endl;
    11     }
    12  
    13     shared_ptr<int> p3;
    14     //移动赋值,p2指向空,p3指向该对象,整个对象的引用计数仍然为1
    15     p3 = std::move(p2); 
    16     if(p2 == nullptr){
    17         cout<<"p2被置空,count=" << p2.use_count() << endl;
    18     }
    19     if(p3 != nullptr){
    20         cout<<"p3通过move夺走p2所有权, count=" << p3.use_count() << endl;
    21     }
    22 }

    结果

    而当我们移动后再使用空的对象,则会报错,所以也要慎用。

    3.7.5.2 建议2

      • 1)谨慎使用,奇怪用法不要轻易尝试。
      • 2)优先使用make_shared()。
        具体下面这两种的优势和劣势参考我这一篇-文章->智能指针之shared_ptr初始化,引用计数,常用操作和自定义删除器等等03。
    1 shared_ptr<string> ps1(new string("good luck"));  //需要分配两次内存
    2 auto ps2 = make_shared<string>("good luck");  //只分配一次

    四、weak_ptr

    4.1 简介

      看名字就知道,他是一个弱引用,他的存在是为了辅助shared_ptr解决循环引用问题,它不占用引用数。只能通过shared_ptr或者weak_ptr构造赋值。它没有重载 * 和 -> 运算符,因此不可以直接通过 weak_ptr 访问对象,典型的用法是通过 lock() 成员函数来获得 shared_ptr,进而使用对象。大家理解时,将weak_ptr看成用来监视shared_ptr(强引用)的生命周期用。是一种对shared_ptr的扩充。

    4.2 weak_ptr初始化赋值

     1     //1 weak_ptr初始化赋值
     2     std::shared_ptr<int> sh = std::make_shared<int>();  // 默认值为0
     3     cout<<sh.use_count()<<endl;                         // 1
     4     std::weak_ptr<int> w(sh);                           // 用shared_ptr构造
     5     std::weak_ptr<int> w1(w);                           // 用weak_ptr构造
     6     cout<<sh.use_count()<<endl;                         // 1
     7 
     8     std::weak_ptr<int> w2=sh;                           // 用shared_ptr赋值
     9     std::weak_ptr<int> w3=w2;                           // 用weak_ptr赋值
    10     cout<<sh.use_count()<<endl;                         // 1

    4.3 lock()函数

      lock():检查weak_ptr所指向的对象是否存在,如果存在那么lock将返回一个指向该对象的shared_ptr(指向对象的强引用就会+1),如果指向对象不存在,lock会返回一个空的shared_ptr。通过lock可以将weak_ptr转化成shared_ptr使用。

    4.4 expired()函数

      expired函数:判断weak_ptr的方法expired判断shared_ptr管理的资源是否已经释放。true已释放,0未释放

    4.5 上面1,2,3大点的案例

     1 //4 weak_ptr  --小弟
     2 void test16(){
     3 
     4     //1 weak_ptr初始化赋值
     5     std::shared_ptr<int> sh = std::make_shared<int>();  // 默认值为0
     6     cout<<sh.use_count()<<endl;                         // 1
     7     std::weak_ptr<int> w(sh);                           // 用shared_ptr构造
     8     std::weak_ptr<int> w1(w);                           // 用weak_ptr构造
     9     cout<<sh.use_count()<<endl;                         // 1
    10 
    11     std::weak_ptr<int> w2=sh;                           // 用shared_ptr赋值
    12     std::weak_ptr<int> w3=w2;                           // 用weak_ptr赋值
    13     cout<<sh.use_count()<<endl;                         // 1
    14 
    15     /*
    16         2. lock函数:检查weak_ptr所指向的对象是否存在,
    17         如果存在那么lock将返回一个指向该对象的shared_ptr(指向对象的强引用就会+1),
    18         如果指向对象不存在,lock会返回一个空的shared_ptr.
    19         通过lock可以将weak_ptr转化成shared_ptr使用
    20     */
    21     std::shared_ptr<int> another = w2.lock();  
    22     if (another != nullptr){
    23         *another = 12;
    24         cout<<"sh管理的内存未被释放"<<endl;
    25     }
    26     cout<<sh.use_count()<<endl;                         // 2
    27 
    28     //3 expired函数:判断weak_ptr的方法expired判断shared_ptr管理的资源是否已经释放。true已释放,0未释放
    29     bool isDeleted = w2.expired();
    30     cout<<sh.use_count()<<endl;                         // 2
    31     cout<<isDeleted<<endl;                              // 0 因为程序还没结束 sh与another在引用 
    32     
    33     //测试lock为空时
    34     another.reset();//无参reset置空,该内存引用数减1,本对象another的引用计数为0
    35     sh.reset();
    36     if (w2.lock() == nullptr){
    37         cout<<"sh管理的内存被释放"<<endl;
    38     }
    39 }

    结果

    4.6 reset()函数

    reset():将该引用的引用计数设置为空,不影响该片内存的引用计数,但是该片内存的引用计数会减少1。

    4.7 尺寸问题

    weak_ptr与shared_ptr的尺寸一样大,是裸指针的两倍。内部有两个裸指针

    • 1)第一个裸指针指向的是这个智能指针所指向的对象。
    • 2)第二个所指针指向一个数据结构(控制块),这个控制块里有:
      • 1、引用计数(强引用计数、弱引用计数)
      • 2、其他数据(弱引用删除器,互斥锁等等)

    五、unique_ptr

    5.1 unique_ptr概述

    独占式的概念(所有权);同一时刻只能有一个unique_ptr指向这个对象(这块内存),当这个unique_ptr被销毁时,它所指向的对象也被销毁。

    5.2 unique_ptr的初始化

    5.2.1 正常初始化

    1 unique_ptr<string> p1(new string("Hello World"));

    5.2.2 C++14新特性make_unique函数初始化

      C++11中没有,C++14中才有make_unique,它不支持指定删除器的语法,如果不用删除器,建议优先选择使用,更高的性能。由于我的编译器最高支持C++11,所以没有这个函数。

    1 unique_ptr<int> p1 = make_unique<int>(100);
    2 auto p2 = make_unique<int>(200);

    5.2.3 使用匿名对象初始化

     1 //方法1
     2 unique_ptr<string> p3=unique_ptr<string>(new string("Hello World"));
     3 //方法2(调用函数返回匿名对象)
     4 unique_ptr<string> return_unique_ptr(string str)
     5 {   
     6     //从函数返回一个局部的unique_ptr对象,这种局部对象,系统会给我们再次生 
     7     //成一个临时的unique_ptr对象,调用unique_ptr的移动构造函数
     8     unique_ptr<string> pr(new string(str));
     9     return pr;  
    10 }
    11 
    12 unique_ptr<string> p4=return_unique_ptr("func return");

    5.2.4 移动语义初始化

    实际上移动语义和匿名对象初始化最终都是调用右值拷贝构造函数

    1 unique_ptr<string> ps1(new string("good luck"));
    2 unique_ptr<string> ps2 = std::move(ps1);

    5.2.5 错误的使用拷贝构造,等号重载初始化

    由于unique_ptr是没有实现拷贝构造(或者具体说它只支持右值拷贝构造,所以上面才能使用move构造)和等号重载的,所以不能使用拷贝构造和等号(赋值)重载初始化

    1 unique_ptr<string> p1(new string("Hello World"));
    2 unique_ptr<string> p2;
    3 //unique_ptr<string> pp3(p1);     //报错,左值拷贝构造并未实现
    4 //p2=p1;                          //报错 =重载函数只写了声明没写函数体

    5.3 unique_ptr的成员函数和其它操作的使用

    5.3.1 release()函数

      释放,调用后智能指针和其所指向对象的联系再无联系,但是该内存仍然存在有效。它会返回裸指针,但是该智能指针被置空
      返回的裸指针我们可以手工delete来释放,也可以用来初始化另外一个智能指针,或者给另外一个智能指针赋值。

    1 unique_ptr<string> ps3(new string("good luck"));
    2 unique_ptr<string> ps4(ps3.release());//此时ps4夺走ps3的使用权,ps3被置空。类似于移动语义,但区别是release返回裸指针,move返回本类型unique_ptr。
    3 if(ps3==nullptr){
    4     cout<<"ps3被释放掉"<<endl;
    5 }

    结果显示

    5.3.2 reset()函数

    reset()不带参数情况:释放智能指针所指向的对象(释放因为它是独占,而不像shared_ptr还需要考虑引用计数),并将智能指针置空
    reset()带参数时:释放智能指针所指向的对象,并将该智能指针指向新对象

     1     //3 reset函数
     2     unique_ptr<string> ps5(new string("good luck"));
     3     unique_ptr<string> ps6(new string("good luck2"));
     4     /*
     5         首先ps6调用release后被置空,返回裸指针作为参数。然后
     6         由于ps5调用reset,ps5被置空,并指向新内存即ps6返回裸指针指向的那块内存
     7         所以我们打印ps5的数据应该是"good luck2"
     8     */
     9     ps5.reset(ps6.release());
    10     if(ps5==nullptr){
    11         cout<<"ps5被释放掉"<<endl;
    12     }
    13     if(ps6==nullptr){
    14         cout<<"ps6被释放掉,内存被ps5使用"<<endl;
    15     }
    16     cout << ps5->data() << endl;

    结果

    5.3.3 nullptr的使用

    实际上我们对于nullptr在上面已经使用过,用于比较。而这里也可以使用nullptr用于置空。所以nullptr目前在本篇可以作为

    • 1)比较。
    • 2)置空:释放智能指针所指向的对象,并将智能指针置空。
     1 class A {
     2 public:
     3     A() {};
     4     A(int i) {
     5         m_i=i;
     6         cout<<"A"<<endl;
     7     };
     8     void SetI(int i) {m_i=i;};
     9     int GetI(){return m_i;}
    10     ~A() {
    11         cout<<"~A"<<endl;
    12     };
    13 private:
    14     int m_i;
    15 };
    16 
    17 //4 nullptr的使用
    18 unique_ptr<A> ps7(new A(522));
    19 ps7 = nullptr; //释放ps7所指向的对象,并将ps7置空。
    20 if(ps7 == nullptr){
    21     cout<<"ps7被置空,并且所指内存也被释放"<<endl;
    22 }

    5.3.4 指向数组

    1 //unique_ptr的自定义删除器
    2 //unique_ptr<int,自定义删除器的类型> parr1(new int[5],自定义删除器);
    3 unique_ptr<int[]> parr(new int[10]);
    4 parr[0] = 12;
    5 parr[1] = 12;

    5.3.5 get()函数

    get():返回智能指针中保存的裸指针。考虑到有些函数的参数需要内置的裸指针,所以引入该函数。

    5.3.6 解引用(*)

    解引用:获取该智能指针指向的对象,可以直接操作。

    1 unique_ptr<string> ps8(new string("good"));
    2 *ps8 = "good luck";

    5.3.7 swap()函数

    swap():交换两个智能指针所指向的对象。

    1 unique_ptr<string> ps1(new string("good luck"));
    2 unique_ptr<string> ps2(new string("good luck2"));
    3 ps1.swap(ps2);
    4 std::swap(ps1, ps2);//也可使用标准库的swap

    5.4 如何将unique_ptr转换成shared_ptr类型

    转换条件:如果unique_ptr为右值时,他就可以赋值给shared_ptr。
    原因:因为shared_ptr包含一个显示构造函数可用于将右值unique_ptr转换为shared_ptr,shared_ptr将接管原来归unqiue_ptr所拥有的对象。

     1 auto myfunc()
     2 {
     3     return unique_ptr<string>(new string("good luck")); //这是一个右值(临时对象都是右值)
     4 }
     5  
     6 void test18()
     7 {
     8     shared_ptr<string> ps1 = myfunc(); //可以
     9     unique_ptr<int> pi1(new int(1));
    10     shared_ptr<int> pi2 = std::move(pi1); //可以,移动后pi1被置空
    11 
    12     // unique_ptr<int> pi3(new int(100));
    13     // shared_ptr<int> pi4(pi3);//不可以,原因pi4的参数不是右值
    14 }

    5.5 unique_ptr删除器

      unique_ptr和shared_ptr一样,默认删除器都是使用delete所以当我们创建的是一个数组或者文件这些时,显然delete是无法有效回收的删除器是一个可调用对象,其中可调用对象可以是普通函数,或者重载了()括号的类,或者lambda表达式等等unique_ptr删除器比shared_ptr多了一步,先要在类型模板参数中传递进去类型名,然后在参数中再给具体的其函数名

     1 void mydeleter(string *pdel)
     2 {
     3     delete pdel;
     4     pdel = nullptr;
     5     //可以打印日志
     6     cout<<"mydeleter"<<endl;
     7 }
     8  
     9 void test19(){
    10 
    11     // a.1
    12     typedef void(*fp)(string *);
    13     unique_ptr<string, fp> ps1(new string("good luck"), mydeleter);
    14  
    15     //a.2
    16     using fp2 = void(*)(string *);
    17     unique_ptr<string, fp2> ps2(new string("good luck"), mydeleter);
    18  
    19     //a.3
    20     typedef decltype(mydeleter)* fp3; //decltype()返回函数类型void(string *),所以要加*,现在fp3应该是void *(string *)
    21     unique_ptr<string, fp3> ps3(new string("good luck"), mydeleter);
    22  
    23     //a.4
    24     unique_ptr<string, decltype(mydeleter)*> ps4(new string("good luck"), mydeleter);
    25  
    26     //a.5,lambda表达式,可以理解成operator()类类型对象
    27     auto mydella = [](string *pdel)->void{
    28         delete pdel;
    29         pdel = nullptr;
    30         cout<<"mydella"<<endl;
    31     };
    32     //注意,lambda表达式mydella被编译器理解成对象(即ps5的参2),所以我们类型不能加*,否则地址与对象不匹配
    33     //而上面的函数名mydeleter代表是首地址,需要加*地址符号进行匹配。(对象不是对象,&对象才是地址)
    34     unique_ptr<string, decltype(mydella)> ps5(new string("good luck"), mydella);
    35  
    36 }

    结果

    指定删除器额外说明

    • 1)shared_ptr:就算两个shared_ptr指定的删除器不同,只要他们指向的对象类型相同,那么他们属于同一个类型。可以放到同一个容器中。
    • 2)unique_ptr:指定unique_ptr中的删除器会影响unique_ptr类型。所以,从灵活性上来讲,shared_ptr更灵活。unique_ptr如果删除器不同,那么就等于整个unique_ptr类型不同,这种类型不同的unique_ptr智能指针是没法放到同一个容器中的

    5.6 尺寸问题

    当我们在上面代码添加求智能指针的所占字节数,如下:

    //开头先求原指针所占字节数
    string *str = NULL;
    cout<<"未加删除器的指针所占字节数="<<sizeof(str)<<endl;
    
    //然后在每个ps下面各自求出对应智能指针的所占字节数。这里只列出一个。
    int ilen = sizeof(ps1);
    cout << "ps1的所占字节数=" << ilen << endl;

    结果:注意我的是32位下测试,所以原指针所占字节数为4。

    小结:
      如果你增加了自己的删除器,则unique_ptr的尺寸可能增加,指定删除器后,尺寸发生变化。增加字节的对效率有影响。unique_ptr不管你指向什么删除器,unique_ptr的尺寸大小都是裸指针的2倍。但是不指定删除器的unique_ptr的大小和管理的裸指针的大小一样,weak_ptr和shared_ptr的大小是管理的裸指针的大小的两倍,因为它们的内部还有一个管理引用计数的结构。

    六、智能指针的总结

    目前开发我们只需要用到shared_ptr, unique_ptr已经足够,weak_ptr基本不需要使用,它更多时候是给那些开发标准库的人使用。
    然后shared_ptr, unique_ptr的选择主要看下面的区别:

    1)如果程序要是用多个指向同一个对象的指针,应选择shared_ptr。
    2)如果程序不需要多个指向同一个对象的指针,应该首选unique_ptr。
    讲到本篇,智能指针的所有知识点结束,我以前也总结过智能指针,但是不够全面,目前这8篇已经非常全面了,智能指针各种使用场合,和语法都包含进去,总的来说写这八篇文章还是有点难度的,原因是C++的新语法越来越看不懂,有点吃力。完结。

    参考文章

    https://blog.csdn.net/weixin_44517656/article/details/114167353

    https://blog.csdn.net/weixin_44517656/article/details/114170328

    https://blog.csdn.net/weixin_44517656/article/details/114171334

    https://blog.csdn.net/weixin_44517656/article/details/114195265

    https://blog.csdn.net/weixin_45590473/article/details/113101912?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_title~default-1.pc_relevant_baidujshouduan&spm=1001.2101.3001.4242

    https://blog.csdn.net/weixin_44517656/article/details/114228284

    https://blog.csdn.net/weixin_44517656/article/details/114208041

    https://blog.csdn.net/weixin_44517656/article/details/114217244

    本文来自博客园,作者:Mr-xxx,转载请注明原文链接:https://www.cnblogs.com/MrLiuZF/p/15083620.html

  • 相关阅读:
    phpcms V9 MVC模式 与 URL访问解析
    PHPCMS V9 框架代码分析(入口程序)
    批处理命令——for
    批处理命令——set
    批处理命令——if
    AndroidStudio简单的apk混淆
    CKEditor与CKFinder学习--CKFinder源代码改动自己定义上传文件名称
    LeetCode_Path Sum
    Java 并发:内置锁 Synchronized
    做游戏长知识------基于行为树与状态机的游戏AI(一)
  • 原文地址:https://www.cnblogs.com/MrLiuZF/p/15083620.html
Copyright © 2020-2023  润新知