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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容