[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  ===========================================================================================================================

https://www.cnblogs.com/yinbiao/p/11563520.html

引用计数法的内部实现:

  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),那么这部分内存就无法再被回收,至此

              会导致内存泄露。

    详细描述:https://blog.csdn.net/u013797029/article/details/41848407


#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数据。

猜你喜欢

转载自blog.csdn.net/ykun089/article/details/106984146