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