先讲一下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 第二次时,第九个对象地址的内容是: 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配对使用,防止现在的类没有析构,以后添加了就会有问题了。