美文网首页
基类指针指向派生类数组的一些问题

基类指针指向派生类数组的一些问题

作者: fooboo | 来源:发表于2016-05-12 22:41 被阅读571次

    先讲一下C/C++的循环控制语句会被编译器转化成统一的形式:

    如do-while,while,for分别转化为:

    loop:                           |   t = test-expr                       |   init-expr

      body-statement       |   if (!t)                                   |   t = test-expr

      t = test-expr             |      goto done                      |   if (!t)

      if (t)                           |   loop:                                  |      goto done

         goto loop              |      body-statement             |  loop:

                                              t = test-expr                    |      body-statement

                                              if (t)                                  |      update-expr

                                                 goto loop                     |      t = test-expr

                                          done:                                   |      if (t)

                                                                                                  goto loop

                                                                                          done:

    以上结构用汇编代码来表示就比较容易了。

    如这样int * pInt = new int[0],在底层也会被分配1字节的空间,因为new返回的地址得独一无二,实现大概是这样:

    extern void * operator new(size_t size) { if (size == 0) size = 1 //more}

    对一个空指针delete操作也没什么问题[但不建议这么做],大概实现是这样:

    extern void * delete(void * ptr) {if (ptr) free((char*)ptr)}

    为什么不能delete指向派生类数组的基类指针?--出现未定义行为。

    由于继承基类,那么基类肯定有个virtual析构函数,不然会造成对象半销毁导致可能的资源泄露。如果类定义的析构函数,那么当delete时,内部实则调用类似:

    void * vec_delete(void * array, size_t elem_size, int elem_count, void (*destructor) (void * , char))这样的函数,参数分别表示对象的起始地址,每个对象的大小,总个数,析构函数,vec_delete大概实现如下:

    //////more code....

    if (destructor != NULL)

    {

        char * elem = (char *)array;

        char * limit = elem + elem_size * elem_count;

        int exec_time = 0;

        while (elem < limit)

        {

            exec_time ++;

            limit -= elem_size;

            (*destructor)((void *)limit);

       }

    }

    举个栗子:

    4 class CBase

    5 {

    6 public:

    7    CBase(int i = 10) : m_iBase(i)

    8    {

    9        cout<<"CBase ctor"<<endl;

    10    }

    11    virtual ~CBase()

    12    {

    13        cout<<"CBase dtor"<<endl;

    14    }

    15 private:

    16    int m_iBase;

    17 };

    19 class CDerived : public CBase

    20 {

    21 public:

    22    CDerived() : m_iDerived(100)

    23    {

    24        cout<<"CDerived ctor"<<endl;

    25    }

    26    virtual ~CDerived()

    27    {

    28        cout<<"CDerived dtor"<<endl;

    29    }

    30 private:

    31    int m_iDerived;

    32 };

    35 int main()

    36 {

    37    CBase * p = new CDerived[10];

    38    delete[] p;

    39    p = NULL;

    40    return 0;

    41 }

    执行程序最后core了,碰巧打印出一次而不是直接core:

    /////more....

    CDerived dtor

    CBase dtor

    段错误 (核心已转储)

    由于数组中存的是对象,按照道理说不大可能引发虚机制,但这里调用到了派生类的析构,需要结合汇编代码查原因,有些细节就省略,可以在其他文章中理解。

    先看37行对应的汇编解释:

    212  80488a6:  sub    $0x20,%esp

    213    CBase * p = new CDerived[10];

    214  80488a9:  movl  $0x7c,(%esp)

    215  80488b0:  call  8048740 <_Znaj@plt>

    216  80488b5:  mov    %eax,%ebx

    217  80488b7:  movl  $0xa,(%ebx)

    218  80488bd:  lea    0x4(%ebx),%edi

    219  80488c0:  mov    $0x9,%esi

    220  80488c5:  mov    %edi,0xc(%esp)

    221  80488c9:  jmp    80488df

    222  80488cb:  mov    0xc(%esp),%eax

    223  80488cf:  mov    %eax,(%esp)

    224  80488d2:  call  8048a8a <_ZN8CDerivedC1Ev>

    225  80488d7:  addl  $0xc,0xc(%esp)

    226  80488dc:  sub    $0x1,%esi

    227  80488df:  cmp    $0xffffffff,%esi

    228  80488e2:  jne    80488cb

    229  80488e4:  lea    0x4(%ebx),%eax

    230  80488e7:  mov    %eax,0x1c(%esp)

    行214~216分配124字节的内存,最后得到首地址即为ebp = p=0x804b008,行217~219分别是吧0x804b008的头四个字节置为10,然后把0x804b00c存放到edi中,然后开始10次的循环[编译器转化了这种循环,可以看前面的结构]esp = 0xbfffef50, (esp+c) = 0x0804b00c;假如228行不成立,即10次构造完成,那么执行229行下面的,否则就循环,这个循环主要做的事情是:

    第一次循环:(esp) = 0x0804b00c,然后在该地址上构造对象,地址0x0804b00c内容为:

    0x804b00c: 0x08048c60 0x0000000a 0x00000064;然后到222行,此时:

    (esp+c)=0x0804b018,原来(esp+c)=0x0804b00c,这里在该地在上加了12,即构造下一个对象...,当227行esi==-1时就结束,

    229~230行(esp+1c)=0x0804b00c,这里没有把分配到的首地址给他,而是偏移后四个字节,这四个字节存放了后面需要delete几次的信息,如果越界或者使用delete p都会造成资源泄露,或者多调用析构而修改其他数据。

    232  80488eb:  cmpl  $0x0,0x1c(%esp)

    233  80488f0:  je    804892c

    234  80488f2:  mov    0x1c(%esp),%eax

    235  80488f6:  sub    $0x4,%eax

    236  80488f9:  mov    (%eax),%eax

    237  80488fb:  lea    0x0(,%eax,8),%edx

    238  8048902:  mov    0x1c(%esp),%eax

    239  8048906:  lea    (%edx,%eax,1),%ebx

    240  8048909:  cmp    0x1c(%esp),%ebx

    241  804890d:  je    804891d

    242  804890f:  sub    $0x8,%ebx

    243  8048912:  mov    (%ebx),%eax

    244  8048914:  mov    (%eax),%eax

    245  8048916:  mov    %ebx,(%esp)

    246  8048919:  call  *%eax

    247  804891b:  jmp    8048909

    248  804891d:  mov    0x1c(%esp),%eax

    249  8048921:  sub    $0x4,%eax

    250  8048924:  mov    %eax,(%esp)

    251  8048927:  call  8048750 <_ZdaPv@plt>

    这段是delete[] p的汇编代码:

    232~233行判断p是否为空,是的话直接赋值为0[故这里delete一个空指针也是安全的];

    234~236行取得0x804b00c前四个字节即本次要循环的次数10;

    237~240行主要是:本次共执行10次析构,每次步长8字节,截止地址为ebx=0x804b05c,

    然后比较0x0804b00c和0x804b05c,相等则248~251,执行释放从0x0804b008占用的内存,而不是0x0804b00c,最后把0x0804b008的内容赋值为0;不相等则242~247行,主要做:

    从0x804b05c往0x0804b00c,以构造相反顺序析构对象,首先0x804b05c-8表示第十个对象的地址,这里+8而不是+12,由此可见编译器把它当做基类对象来析构,这里就有问题了,要么造成资源泄露[因为只从0x804b05c开始,后面的不会执行了],要么错误的指针;0x804b054: 0x08048c60 0x0000000a ;eax=0x8048c60,取得虚函数表地址,eax=0x8048afa取得析构函数地址:0x8048afa <CDerived::~CDerived()> : 0x53e58955,然后准备参数,以第十个对象地址作为析构函数执行的地方,第一次执行结果貌似正常的;

    第二次时,第九个对象地址的内容是:

    0x804b04c: 0x0000000a 0x00000064 ,所以这里误以为把头四个字节当做了虚函数表的指针,所以就core了。

    根据vec_delete原型,destructor的值是CBase的析构函数,elem_size的值是sizeof(CBase)的并非sizeof(CDerived)的,

    如何避免?手写循环析构:

    for (int i = 0; i < 10; ++ i)

        CDerived * p = &((CDerived *)p)[i];

        delete p

    多态和指针算术不能混用。也有类似的:++p,这样移动的步长是基类对象大小,也会错误。

    以上举得栗子在派生类的大小大于基类时出现了core,当sizeof(派生类)等于sizeof(基类)没有出现core但也不正常。

    这里也做了几个简单的实验,有个有趣的现象:

    类似这种简单的类型int * pInt = new int[10],并没有看到编译器分配空间存储个数信息X,释放的时候delete p 和delete[] p没有多大差别;

    类似没有析构函数的简单类,或编译器没有在必要情况下合成的析构函数:

    class CStu

    {

        public:

            CStu(){//TODO}}

        private:

            int m_iData;

    };

    CStu * pStu = new CStu[10],也没有在哪个地址的前几个字节存储个数信息X,构造时在esi中存储了循环次数,析构时只是简单的调用_ZdaPv@plt,也没有用到个数信息X,这里delete pStu和delete[] pStu也没多大关系;

    如果CStu带析构函数,那么会在申请到的空间头四个字节存放个数信息X,然后返回首地址+4的地址开始构建对象,最后delete pStu和delete[] pStu时,后者会获取首地址的个数信息X,然后开始调用X次析构函数,从后往前以每4字节的步长析构对象....

    由于new内部还是通过malloc实现的,故malloc里面也会额外多申请字节数存放为free正确释放空间时所需要的信息;

    还是建议new [] 和delete [],new和delete配对使用,防止现在的类没有析构,以后添加了就会有问题了。

    相关文章

      网友评论

          本文标题:基类指针指向派生类数组的一些问题

          本文链接:https://www.haomeiwen.com/subject/xdgkrttx.html