最近不知哪来的好奇心,对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我们就很容易想到他的用处,应该是记录数组大小的。事实也确实如此。
这里明确交代,给分配内存的第一个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里面找吧。