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

先讲一下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"<

10    }

11    virtual ~CBase()

12    {

13        cout<<"CBase dtor"<

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"<

25    }

26    virtual ~CDerived()

27    {

28        cout<<"CDerived dtor"<

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 : 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配对使用,防止现在的类没有析构,以后添加了就会有问题了。

你可能感兴趣的:(基类指针指向派生类数组的一些问题)