0CCh Blog

为什么必须用delete[]释放数组

最近不知哪来的好奇心,对C++产生了一些兴趣,要知道我通常情况下都是cpp文件中写c代码,c代码中嵌汇编。不过,在做了一些大点的项目之后,确确实实发现了,这种编码方式麻烦的一面。有时候甚至自己都难得维护以前写的东西。所以选择性看了google的c++编程规范,而且对scoped_ptr和auto_ptr的区别参数兴趣。然后我得出的结论是这两种实际上是其实差不多,只不过scoped_ptr拷贝构造函数和赋值构造函数都是私有的。这样就避免粗心大意的程序员调用他。其他的区别还真没看出来。

OK,这些都不是记录这篇tip的重点。重点在于boost,或者说是google的scoped_ptr代码里面实现了scoped_array。而要用在数组上使用智能指针,就必须用数组的智能指针类。而auto_ptr刚好没有数组部分,所以对于数组,就不能用auto_ptr了。(为什么不用vector?这也不是重点)

作为一个蹩脚的C++程序员,我这时候开始犯晕了。我们知道只能指针都是帮助程序员去释放资源,让程序员把精力放到更重要的地方。那么在我看来那么所需要做的就是析构的时候 delete 或者 delete[] 就行了。要知道,delete[] 就是调用的delete,他们只是单纯的释放内存。那么数组和非数组又有什么区别?

光想肯定不行,写两个例子。

首先是new 一个char数组,分别用delete和delete[]释放。结果表明,没有任何问题,而且不会产生内存泄露。坑爹么?NO,还没完,其实咱们最怀疑的一直都是数组对象,因为他们都有构造和析构函数。而char这样的系统内建类型,想象得出不会出什么问题。new一个对象数组,分别用delete和delete[] 释放。果然问题暴露了,delete的时候出了问题。

知其然,不足以满足好奇心。下面才是拿手的,精彩的要放在后面嘛。

先看测试代码

#include

using namespace std;

class A {

public:
A() {cout << "start 1" << endl;}
~A() {cout << "End 1" << endl;}
};

int main()
{
A *a = new A[10]();

delete[] a;
return 0;
}

编译后看到代码如下

0040103D push 0Eh
0040103F call operator new[] (403ED0h)

注意到这里传入的大小时0Eh,也就是说申请分配14个字节大小的内存。但是我们知道C++标准中空类的大小应该是1字节。那么多出的dword我们就很容易想到他的用处,应该是记录数组大小的。事实也确实如此。

mov dword ptr [eax],0Ah

这里明确交代,给分配内存的第一个dword传入10。

00401069 push offset A::~A (4011D0h)
0040106E push offset A::A (401120h)
00401073 push 0Ah
00401075 push 1
00401077 mov ecx,dword ptr [ebp-0F8h]
0040107D add ecx,4
00401080 push ecx
00401081 call `eh vector constructor iterator' (40A010h)

这里是调用构造函数,注意是vector版本的构造函数。参数分别是数组的this指针,sizeof(A),数组数量,构造和析构函数。这样,在这个函数内部循环10次调用构造函数,构造完毕。

004010BD mov eax,dword ptr [ebp-14h]
004010C0 mov dword ptr [ebp-0E0h],eax
004010C6 mov ecx,dword ptr [ebp-0E0h]
004010CC mov dword ptr [ebp-0ECh],ecx
004010D2 cmp dword ptr [ebp-0ECh],0
004010D9 je main+0F0h (4010F0h)
004010DB push 3
004010DD mov ecx,dword ptr [ebp-0ECh]
004010E3 call A::`vector deleting destructor' (401230h)

这里是析构部分传入数组的this指针,传入flag(3),调用析构函数。

继续看call之后的代码

00401253 mov eax,dword ptr [ebp+8]
00401256 and eax,2
00401259 je A::`vector deleting destructor'+61h (401291h)
0040125B push offset A::~A (4011D0h)
00401260 mov eax,dword ptr [this]
00401263 mov ecx,dword ptr [eax-4]
00401266 push ecx
00401267 push 1
00401269 mov edx,dword ptr [this]
0040126C push edx
0040126D call `eh vector destructor iterator' (40A920h)
00401272 mov eax,dword ptr [ebp+8]

如果flag中位1是set,那么调用eh_vector_destructor_iterator调用每个析构函数。参数分别是this,sizeof(A),数组个数(这里很明显是从eax-4中拿出来的)以及析构函数地址。

OK,明白了delete[]的做法,我们看看delete为什么失败。

004010BD mov eax,dword ptr [ebp-14h]
004010C0 mov dword ptr [ebp-0E0h],eax
004010C6 mov ecx,dword ptr [ebp-0E0h]
004010CC mov dword ptr [ebp-0ECh],ecx
004010D2 cmp dword ptr [ebp-0ECh],0
004010D9 je main+0F0h (4010F0h)
004010DB push 1
004010DD mov ecx,dword ptr [ebp-0ECh]
004010E3 call A::`scalar deleting destructor' (4012D0h)

这里调用的析构函数都不一样是一个scalar版本的函数。

继续看这个函数的关键部分

004012F3 mov ecx,dword ptr [this]
004012F6 call A::~A (4011D0h)
004012FB mov eax,dword ptr [ebp+8]
004012FE and eax,1
00401301 je A::`scalar deleting destructor'+3Fh (40130Fh)
00401303 mov eax,dword ptr [this]
00401306 push eax
00401307 call operator delete (40A890h)

这里很清楚的看到,只进行一次析构,然后就释放内存。所以我们看到的现象是只调用一次析构函数。那么为什么会崩溃呢?因为delete错了地址。看上面的对比的值,eax-4才是new返回的地址,所以delete的不应该是eax,而是eax-4。

真相大白?NO,还有一个问题,delete[] 和delete 内建类型真的成功了么?
看看我贴出的代码吧,这里不解释了。

;delete[] version
0040101E push 0Ah
00401020 call operator new[] (4014C0h)
00401025 add esp,4
00401028 mov dword ptr [ebp-0E0h],eax
0040102E mov eax,dword ptr [ebp-0E0h]
00401034 mov dword ptr [a],eax
00401037 mov eax,dword ptr [a]
0040103A mov dword ptr [ebp-0D4h],eax
00401040 mov ecx,dword ptr [ebp-0D4h]
00401046 push ecx
00401047 call operator delete[] (401600h) `

;delete version
0040101E push 0Ah
00401020 call operator new[] (4014C0h)
00401025 add esp,4
00401028 mov dword ptr [ebp-0E0h],eax
0040102E mov eax,dword ptr [ebp-0E0h]
00401034 mov dword ptr [a],eax
00401037 mov eax,dword ptr [a]
0040103A mov dword ptr [ebp-0D4h],eax
00401040 mov ecx,dword ptr [ebp-0D4h]
00401046 push ecx
00401047 call operator delete (401600h)

就像我刚刚所说的delete[]会调用delete。所以不会出任何问题。

如果汇编看的头疼的话,这里我写了两个函数的逆向代码(说了精彩的应该放在后面的):

void A::'scalar deleting destructor'(unsigned int flags)
{
this->~A();
if (flags & 1) {
A::operator delete(this);
}

void A::'vector deleting destructor'(unsigned int flags)
{
if (flags & 2) {
count = *(int *)((int)this - 4);
'eh vector destructor iterator'(this, sizeof(A), count, A::~A);
if (flags&1) {
A::operator delete((void *)((int)this - 4));
}
}
else {
this->~A();
if (flags & 1) {
A::operator delete(this);
}
}
};

分析到这,终于明白。数组一定要用delete[]释放才安全。所以千万不要用auto_ptr作为数组的智能指针,不然会死的很惨。这里还要提一点,auto_ptr也不要用到容器里面去了,也是不允许的。非要这么做就用shared_ptr吧, C++0x已经在stl中加入的这部分。从vs2008 sp1开始支持。低版本的vs的话就去boost里面找吧。