天天看点

[c++] c++中的智能指针

要点汇总:1)   #9 - 基类的析构函数必须是virtual,否则可能导致析构调用链断层。
智能指针的使用:1)使用 普通指针/其他已存在的智能指针/其他已存在的普通指针,对当前创建的智能指针进行初始化。            (创建 指针 指向当前已有内存)
                 2)使用 make_shared 创建全新的内存区,然后创建一个全新的智能指针指向它。                                (创建 内存空间 和 指针)
                 3)使用 智能指针的 reset 方法来进行 智能指针的 重定向 和 释放                                           (重定向 和 销毁) #1  ===========================================================================================================================
 智能指针智能指针有两种: shared_ptr  和  unique_ptr
这两种指针都是模板类,因此原型为   xxx_ptr<T>
(!)注:智能指针是用来管理堆内存的,不是作为指针使用的
(!)智能指针是使用delete来释放内存,所以释放内存时的特性和delete一样
#2  ===========================================================================================================================
(!)首先明确一点,智能指针“仅仅”用来“动态分配内存”,而不是用来当做指针使用!!!!
因此:
 int i = 100;    //或者其他类,比如 classA a;
 shared_ptr<int> sp  = make_shared<int>(i);上述语句实际上是使用i的值作为新分配内存的初始值。sp并没有指向i,而是指向了新分配的堆,这个堆存放int型的数值100
上述语句不能算错误,但是需要理解  make_shared 动作实际上是取堆里申请了内存的。
    (可以这样理解,make_shared相当于 new ,尖括号指定需要分配内存的类型名,小括号指定作为拷贝构造传递给
       类型名的值,如果不指定,那么使用类型的默认构造) 同理:
 classA *pa = new classA();
 shared_ptr<classA> sp = make_shred<classA>(*pa);    //使用classA的拷贝构造在堆里创建一个新的对象,而不是使用pa指向的对象 ====== EXAMPLE 1 =======
#include "stdafx.h"
 #include <iostream>
 #include <memory>
 using namespace std;class A{
 public:
     A() = default;
     ~A(){ ; }
 public:
     int myint=10;
 };int _tmain(int argc, _TCHAR* argv[])
 {
     A* pa = new A();
     shared_ptr<A> sp = make_shared<A>(*pa);
     sp->myint = 100;    cout << pa->myint << endl;        //pa = 10 ,值并没有改变,因为它是用 new 分配的堆
     cout << sp->myint << endl;        //sp = 100,值和pa不一样,因为它是用 make_shared 分配的堆    getchar();
    return 0;
 }    注:小括号里的内容必须与尖括号里类型的某个构造函数相匹配
 ====== EXAMPLE 2 ======
#include "stdafx.h"
 #include <iostream>
 #include <memory>
 using namespace std;class A{
 public:
     A() = default;
     ~A(){ ; }
 public:
     int myint=10;
 };int _tmain(int argc, _TCHAR* argv[])
 {
     shared_ptr<A> sp = make_shared<A>();        //使用 A 的默认构造函数    cout << sp.use_count() << endl;                //为1
    shared_ptr<A> sp1 = sp;                        //增加了sp指向内存区的计数
    cout << sp.use_count() << endl;                //为2 
     cout << sp1.use_count() << endl;            //为2    sp1 = NULL;
    cout << sp.use_count() << endl;                //为1
    getchar();
    //小结:可见智能共享指针的计数器是由系统自动管理的,具体内部实现可能是 静态数据类型,这样各个
     //        实例操作的就是同一个数据,当然,可能会涉及到线程安全的问题,这时候就要加锁,这也伴随着
     //        效率略低和死锁的风向    return 0;
 }#3  ===========================================================================================================================
(!)在使用共享指针时,我们只需要操作指针对象,不要试图去直接操作指针指向的内容。因为当所有指针都销毁的时候,动态分配的内存会被
 自动释放。 计数增加:
 1)初始化一个shared_ptr
 2)作为参数传递给一个函数
 3)作为函数返回值计数减少:
 1)给shared_ptr新指向对象,替换旧对象,那么就对象计数减少
 2)shared_ptr调用析构函数,比如局部变量离开作用域等等     小结:shared_ptr将对对象的内存管理转变为对对指向对象指针的管理。而这个指针的获得仅能通过shared_make来实现。因此,从
           某种意义上来说,更集中。
           #4  ===========================================================================================================================
共享指针的应用场景:
 1)程序不知道自己需要多少个对象???
 2)程序不知道所需要对象的准确类型???
 3)程序需要在多个对象之间共享数据,这有点像类的static成员变量,是所有类共享的。但是既然有static,为什么还要共享指针??? (!!!)使用动态内存的一个常见原因是:允许多个对象共享相同的状态。
(!!!)为什么不做成static的?
 主要因为static数据存放在.bss区,无法手动释放,在程序运行时就载入,而这部分的可用空间是有限而狭小的。如果我们想要使用堆栈这样
 宽敞的空间来模拟static成员,此时就可以使用共享指针管理的动态内存(堆)来实现,实现一个仅在大家都不在需要时才会释放的堆区域。 #5  ===========================================================================================================================
引用计数法的内部实现:
  1)这个引用计数器保存在某个内部类型中,而这个内部类型对象在shared_ptr第一次构造时以指针的形式保存在shared_ptr中
  2)shared_ptr重载了赋值运算符,在赋值和拷贝另一个shared_ptr时,这个指针被另一个shared_ptr共享
  3)在引用计数归0时,这个内部类型指针与shared_ptr管理的资源一起释放
* 4)此外,为了保证线程安全,引用计数器的加1和减1都是原子操作,它保证了shared_ptr由多个线程共享时不会爆掉
#6  ===========================================================================================================================
一个典型的场景:
class A{
public:
     vector<classB> m_B_list;        //非常庞大的一个列表(实际这里可以使vector指针,这里只是做一个描述)}
如果A有N多个实例,问题就出现了,每个A实例都要有一个m_B_list,首先很吃内存,其次各个A实例之间需要实施互相同步数据,从而保证这个
 vector对于所有A来说,都是一样的。这是很棘手和难处理的:
 1)可以让vector是static的,但是这样比较占用static区域,而且无法释放                       (不可选)
 2)可以简单粗暴,让所有A在更改vector之后,通知其他A跟新自己的vector,这样既 占内存,又 占CPU         (不可选)
 3)创建一个command管理类,让command管理类来存放vector,并提供增删改查接口给所有A使用,这样保证了效率,一定程度上节约了内存。
    但是,需要新增一个类,而这个类的存在感很低,因为它只是来做一个中转                    (不是最优解)
 4)不创建command管理类,但是在外部区域创建vector,然后让A都能访问,这样有风险,如果某个A出于“某种原因”把vector给删掉了。
    那么其他A在访问的时候将访问到空指针,或者直接崩溃。这种情况特别是在A都存放vector指针是明显,因为析构时会释放成员变量。(不是最优解)*5)使用shared_ptr,vector不使用new创建,而是使用shared_make,在A构造的时候创建。但是需要注意各个A运行在不同的线程下,同时
     操作vector可能产生竞争。 第一种实现为 =============
    ---A.h---
     class A{
     public:
         A();    
         A(shared_ptr<vector<classB>> tmplist);     public:
         shared_ptr<vector<classB>> m_p_B_list;    }
    ---A.cpp---
     A::A():m_p_B_list(make_shared<vector<classB>>()){        //默认构造,列表初始化    }
     A::A(shared_ptr<vector<classB>> tmplist):m_p_B_list(tmplist){    }
(???)上面的实现虽然使用了共享指针管理动态内存,但是多个A如果都使用默认构造的话,实际上还是创建了多个vector实例,然后各个A
       管理自己的vector #7  ===========================================================================================================================
动态分配const对象是合法的,new const string("xxxx"); 
 按照const对象的属性,const必须在定义是初始化,因此这种情况下不能使用默认构造函数。也可以通过delete来释放分配的内存(???)疑问:const类型变量一般存放在 .data段 。那么动态分配的常量是否 也在其中。如果不是,是否存放在堆内,如果是在堆内,那么这块
 区域是如何增加读写权限的,毕竟const类型只读不可写。 #8  ===========================================================================================================================
内存耗尽:
 int *p  = new (nothrow) int;    //如果内存耗尽,那么不抛异常bad_alloc,只是返回空指针注:默认情况下,c++中,内存耗尽会抛出异常bad_alloc
#9  ===========================================================================================================================
delete接收一个指针,指针要么指向一个对象,要么是空指针。
 delete动作会执行以下两步:1)调用指针指向对象的析构函数;
               2)释放指针指向的内存。 如果指针类型和指向的内容不一致,比如 void* p = new classA();
 ====  例子  ==== 
 class A{
 public:
     A(){}
     ~A(){ cout << "~A()"; }
 };class B :public A{
 public:
     B(){}
     ~B(){ cout << "~B()"; }
 }; int _tmain(int argc, _TCHAR* argv[])
 {
     //A *p_A = new A();
     void *p_B = new B();    //delete p_A;
     delete p_B;    getchar();
    return 0;
 } 小结:1)delete只认指针类型,如果指针是void类型,“那么不会调用任何析构函数”。
       2)同理,如果有父子关系,那么delete父类指针,“只会调用父类的析构函数”。
 (*) 3)为了避免1)和2)中的问题,基类的析构函数必须是 “virtual” 的。
       4)不论delete接收的是子类的指针还是基类的指针。类实例都会被完整地释放掉,delete是跟着实例走,只认地址,而一旦涉及
      到地址部分,malloc和free便不再理会指针类型,而是从操作系统层面完成内存的释放。     (!!!)注:上面说到了 “基类的析构函数必须是virtual,否则在delete指向子类的基类指针时,会出现子类析构不会
              被调用的问题”。如果子类中没有需要回收的内存(通过new和malloc分配的),那么内存是安全的,子类实例
              会被正确地释放。但是如果子类中进行了动态内存分配(new,malloc),那么这部分内存就无法再被回收,至此
              会导致内存泄露。

 #10  ===========================================================================================================================
野指针:野指针是指,指针指向的内存已经被释放掉了,但是指针没有置空。使用delete操作指针并不会触发指针的置空,因此在delete释放
     完内存以后,需要手动把指针置空,因为内存区域已经不存在了,此时使用指针去访问就会触发异常 ---> "非法内存区访问"。#11  ===========================================================================================================================
可以在delete后立即置空指针来避免野指针的出现。但是如果某块内存区域被多个指针指向,那么就很容易忽略某个指针的置空操作。
因此:但凡涉及到多个指针指向同一块内存区域的场景,“   务必使用智能指针   ”来管理内存,此时我们不需要调用delete,只需要管理指针
       即可,当所有的指针都置空,则内存被释放。#12  ===========================================================================================================================
上面提到了智能指针的使用方法,通过make_shared来分配内存,但是如果想使用new来分配内存,然后交由智能指针管理,该如何操作???
首先,智能指针的构造函数时explict的,即不接受隐式转换,即必须是指定类型,故先new在通过赋值构造是行不通的。
可以使用复制构造函数来实现:
shared_ptr<classA> p_A = new classA();    //错误
 shared_ptr<classA> p_A(new classA());    //正确同理,使用普通指针给智能指针赋值也是行不通的:
shared_ptr<classA> clone(){        //错误
     return new classA();        //返回值不接收这种隐式转换
 }shared_ptr<classA> clone(){                //正确
     return shared_ptr<classA>(new classA());    //返回值也是智能指针
 } (!!!)注:上面提到的使用普通指针来初始化智能指针的场景,要求普通指针指向的必须是动态内存(即new分配的),静态内存不行,
         比如:
                   int i = 10;
             const int j = 10;
             int *p = &i;
             const int *pj = &j;
             shared_ptr<int> sp(p);            //不能把栈给智能指针
             shared_ptr<int> spp(pj);        //不能把data区给智能指针#(!)13  ===========================================================================================================================
shared_ptr 和 unique_ptr 的构造和析构
 classA A;
 classA *p_A = &A;            //创建一个实例给普通指针unique_ptr<classA> up(new A());        //创建一个实例给unique智能指针
 shared_ptr<classA> sp(new A());    //创建一个实例给智能指针shared_ptr<classA> sp1(p_A);        //让智能指针指向p_A(普通指针)指向的对象,要求unique_ptr指向的类型和shared_ptr指向的类型能够互换
 shared_ptr<classA> sp2(up);        //让智能指针指向unique_ptr指向的对象,要求unique_ptr指向的类型和shared_ptr指向的类型能够互换shared_ptr<classA> sp3(p_A,mydelete)    //让智能指针指向p_A(普通指针)指向的对象,在所有智能指针都释放以后,不再使用delete释放内存,而是使用mydelete
 shared_ptr<classA> sp4(sp1,mydelete)    //让智能指针指向sp1(智能指针),并使用mydelete代替delete来释放内存sp.reset()                //把sp从当前指向关系中解放出来,如果sp是最后一个指针,那么调用delete释放目标对象
 sp.reset(sp1)                //把sp重定向到sp1
 sp.reset(sp1,mydelete)            //把sp中定向到sp1,如果原sp指向的对象已经没有指针再指向,那么使用mydelete来释放原对象     注:如果智能指针指向的不是new出来的动态内存,如果需要释放资源,那么一定要提供mydelete来替代delete,因为这种情况下不会调用delete
 #(!)14 ===========================================================================================================================
 (!!!)
 智能指针的使用:1)使用 普通指针/其他已存在的智能指针/其他已存在的普通指针,对当前创建的智能指针进行初始化。        (创建 指针 指向当前已有内存)
         2)使用 make_shared 创建全新的内存区,然后创建一个全新的智能指针指向它。                (创建 内存空间 和 指针)
         3)使用 智能指针的 reset 方法来进行 智能指针的 重定向 和 释放                        (重定向 和 销毁)#15   ===========================================================================================================================
weak_ptr
weak_ptr 是给 shared_ptr做补充的,把weak_ptr增加到shared_ptr指向的对象上,不会增加计数,单当shared_ptr全部释放完以后,
 weak_ptr指向的内容也将不存在。 weak_ptr的使用必须 经过自己的lock()方法,这个方法会peek对象是否存在,存在返回true,否则
 false,用完释放weak_ptr亦不会导致计数减少。weak_prt可以理解为shared_ptr的监视器。仅仅用来peek数据。