剖析VC6.0中为类对象申请内存的过程
来源:互联网 发布:jsp连接mysql 编辑:程序博客网 时间:2024/06/07 18:38
摘要:在用VC开发时常常需要对内存进行操作,其中C++中提供了运算符new与delete;而C语言库中提供有malloc与free。它们在具体使用时存在着一些差别,本文主要剖析一下VC6.0中为类对象申请内存的过程。方便大家理解在为类对象申请或释放内存时的一个问题。
关键词:VC6.0,内存申请,C++,new,delete
1. 总述
在用VC开发时常常需要对内存进行操作,其中C++中提供了运算符new与delete;而C语言库中提供有malloc与free。它们在具体使用时存在着一些差别,大家也可能一些疑问再里面,如:为什么C++运算符new再为类对象申请内存时会调用类的构造函数而malloc则不会,为什么用new申请一个对象数组时就必须用delete[]释放而申请一个long类型的数组就能用delete释放,new一个对像与new一个对象数组以及new一个基本数据类型的数组有什么不同,为什么用new申请一个对像用delete[]释放有时会出错而有时就成功出错是随机的吗等等;本文主要剖析一下VC中为类对象申请内存的过程同时解答上述几个疑问;希望能给大家予以一定帮助。这里说明一下试验的环境:VC6.0编译环境,IDE5.0。
2. malloc VS new
这个两操作的区别主要是在申请一个类的对象时,malloc不会调用类对象的构造函数而new会调用,我们这里定义一个类用来实际测试,下面我们将以这个类为基础讨论malloc与new的差别。
long g_Number = 0;
class CMemoryItem
{
public:
CMemoryItem()
{
m_info.Format("Construct CMemoryItem Nember[%d] /n", ++g_Number);
OutputDebugString(m_info);
}
virtual ~CMemoryItem()
{
m_info.Replace("Construct","Destructor");
OutputDebugString(m_info);
g_Number--;
}
private:
CString m_info;
};
下面我看几段代码与它们的执行结果:
代码1:
CMemoryItem* pItem = new CMemoryItem;
delete pItem;
执行输出:
Construct CMemoryItem Nember[1]
Destructor CMemoryItem Nember[1]
从这个输出中我们可以看到当new时同时调用了类的构造函数当调用delete时调用了类的析构函数那么我们再看看C语言中malloc与free的执行情况。
代码2:
CMemoryItem* pItem = (CMemoryItem*)malloc(sizeof(CMemoryItem));
free(pItem);
执行输出:
无
这时我们没有看到有代码1那样的输出,可见malloc时并没有去调用类的构造函数,free时也没有调用类的析构函数;这样我们就验证的了new和delete在为类申请内存时会调用类的构造与析构函数,而malloc和free不会。下面我们一起看看它们汇编代码是什么样一个情况,首先来看代码1片段:
00401D76 push edx ;
00401D77 push offset THIS_FILE (00416690)
00401D7C push 8
00401D7E call operator new (004021c4)
00401D83 add esp,0Ch
00401D86 mov dword ptr [ebp-28h],eax
00401D89 mov dword ptr [ebp-4],0
00401D90 cmp dword ptr [ebp-28h],0
00401D94 je CTestMemAllocateDlg::OnDelete+93h (00401da3)
00401D96 mov ecx,dword ptr [ebp-28h]
00401D99 call @ILT+15(CMemoryItem::CMemoryItem) (00401014)
00401D9E mov dword ptr [ebp-38h],eax
其中edx里存放的是CMemoryItem* pItem = new CMemoryItem;这句代码在CPP文件中的行号,offset THIS_FILE (00416690)则是CPP文件的路径(因为我们调试的是DEBUG版本所以会有这个两个参数,大家可以用IDE反译一下RELEASE版本就能看到是没有这两个参数的),8就是这个类的长度,通过对sizeof(CMemoryItem)的输出可以证实这类的长度是8,至于为什么找度会是8我们在这里不做深入的讨论,接下来就是对new的一个调用。当调用完成后对调用的反回值做一是否为0的一个验证;紧接着就调用了CMemoryItem类的构造函数,构造函数会再返回这个对象的起始内存地址。
让我们再来看一下用malloc申请内存的汇编代码,以加深理解两者的区别。下面的malloc申请内存的汇编代码:
00401D2D mov esi,esp
00401D2F push 8
00401D31 call dword ptr [__imp__malloc (004176c8)]
00401D37 add esp,4
00401D3A cmp esi,esp
00401D3C call _chkesp (004021ca)
00401D41 mov dword ptr [ebp-8],eax
00401D44 mov esi,esp
00401D46 mov eax,dword ptr [ebp-8]
00401D49 push eax
00401D4A call dword ptr [__imp__free (004176cc)]
00401D50 add esp,4
00401D53 cmp esi,esp
00401D55 call _chkesp (004021ca)
大家可以看到这里除对堆栈做了两次较验外我们没有看到任何有关构造和析构函数的调用;从以上分析可以看出两者的区别就在于:用new申请类对象时编译器加入对类构造函数的调用而malloc则没有,这是因为malloc是C语言中的函数在C语言中没有类的概念所实现时也不会为其加入对构造函数的调用,如果我们单步调试到00401D7E call operator new (004021c4)时,按F11进入它的内部就能看到它的内部仍然是把调用转给了malloc函数(这里只针对未在类中重载new操作而言是这样,如果类中重载了new操作符这句则是对新重载函数的调用);free 和delete区别也类似是这样的。
3. 剖析new操作的过程
这节一节我们来详细看一下申请一个类对象时的构造过程和申请一个类对象数组时的构造过程以及为基本数据类型申请数组时的一些情况并对比三者的不同;试验时用的类还是上面的CMemoryItem类。
3.1. NEW单个类对象的过程
3.1.1. 类中有虚函数的情况
在第二节中我们了解new一个类对象过程但并未提及它的构造过程,这里我们接着上面的内容剖析一下单个类的构造过程。看下面的示例代码:
代码1:
CMemoryItem* pItem = new CMemoryItem;
delete pItem;
在上一节我们已经了解这个类对象new的过程,这里我们不再重复而是要着重看一下在这个类的构造函数里做了哪些操作。在调用类的构造函数时,代码段是这样的:
00401D96 mov ecx,dword ptr [ebp-28h]
00401D99 call @ILT+15(CMemoryItem::CMemoryItem) (00401014)
00401D9E mov dword ptr [ebp-38h],eax
在dword ptr [ebp-28h]中存放着为CMemoryItem类对象申请的内存起始地址,我们现在跟踪一下类的构造函数来看看内部的执行流程,看现在的汇编代码:
00401DF0 push ebp
00401DF1 mov ebp,esp
00401DF3 push 0FFh
00401DF5 push offset __ehhandler$??0CMemoryItem@@QAE@XZ (004037ec)
00401DFA mov eax,fs:[00000000]
00401E00 push eax
00401E01 mov dword ptr fs:[0],esp
00401E08 sub esp,44h
00401E0B push ebx
00401E0C push esi
00401E0D push edi
00401E0E push ecx
00401E0F lea edi,[ebp-50h]
00401E12 mov ecx,11h
00401E17 mov eax,0CCCCCCCCh
00401E1C rep stos dword ptr [edi]
00401E1E pop ecx
00401E1F mov dword ptr [ebp-10h],ecx
00401E22 mov ecx,dword ptr [ebp-10h]
00401E25 add ecx,4
00401E28 call CString::CString (00402164)
00401E2D mov dword ptr [ebp-4],0
00401E34 mov eax,dword ptr [ebp-10h]
00401E37 mov dword ptr [eax],offset CMemoryItem::`vftable' (0041501c)
00401E3D mov ecx,dword ptr [g_Number (00416900)]
00401E43 add ecx,1
00401E46 mov dword ptr [g_Number (00416900)],ecx
00401E4C mov edx,dword ptr [g_Number (00416900)]
00401E52 push edx
00401E53 push offset string "Construct CMemoryItem Nember[%d]"... (004152c0)
00401E58 mov eax,dword ptr [ebp-10h]
00401E5B add eax,4
00401E5E push eax
00401E5F call CString::Format (0040119a)
00401E64 add esp,0Ch
00401E67 mov ecx,dword ptr [ebp-10h]
00401E6A add ecx,4
00401E6D call CString::operator char const * (0040214c)
00401E72 mov esi,esp
00401E74 push eax
00401E75 call dword ptr [__imp__OutputDebugStringA@4 (004173e0)]
00401E7B cmp esi,esp
00401E7D call _chkesp (004021ca)
00401E82 mov dword ptr [ebp-4],0FFFFFFFFh
00401E89 mov eax,dword ptr [ebp-10h]
00401E8C mov ecx,dword ptr [ebp-0Ch]
00401E8F mov dword ptr fs:[0],ecx
00401E96 pop edi
00401E97 pop esi
00401E98 pop ebx
00401E99 add esp,50h
00401E9C cmp ebp,esp
00401E9E call _chkesp (004021ca)
00401EA3 mov esp,ebp
00401EA5 pop ebp
00401EA6 ret
因为代码较多我们只选择其中关键部分的代码来讨论这样也方便大家理解。为了方便讨论我们这里把构造函数的整代码分成三部分函数初始化代码、类初始化代码、函数体代码
1).函数初始化代码
首先我们来简单看一下00401DF0到00401E1C的这段代码,其主要是一些保存当前状态的操作,大家可以看到最后一个入栈的寄存器ECX,那也就是说代码一直执行00401E1C处 ECX都是栈顶第一个元素。下面代码接着是00401E1E位置的代码,它把栈顶的一个元素弹出并保存到了ECX寄存器中,这个寄存器中当前值什么呢?可能大家都一直这个疑问,我们返去再去看看构造函数调用前的那一句汇编代码:
00401D96 mov ecx,dword ptr [ebp-28h]
这一下大家应该明白了,ECX寄存器中存放的就是CMemoryItem对象的起始地址,这时它被保存入dword ptr [ebp-10h] 内存区(我们要记住这个内存区后面的分析中会常用到这个内存区)。
2).类初始化代码
这个阶段从00401E1E一直到00401E37,在这段汇编代码中间我们没有看到有关我们在类的构造函数中写的操作。它首先把对象的起始地址存入了dword ptr [ebp-10h],然后将对象指针偏移4个字节,因为这个地方存放了类的成员变量m_info;由于它是CString类的对象所以这里调用了它的默认构造函数来构造这个成员变量。接着我们再看00401E34和00401E37这两句汇编代码;先把对象的起始地址放到EAX寄存器中,然后把offset CMemoryItem::`vftable' (0041501c)放入对象前四个字节中,这里大家肯定有一个疑问那就是这个偏移地址到底是什么?让我们再返回去看CMemoryItem类的声明,可以看到它的构造函数被声明成了一个虚函数。一些编译器支持虚函数的方法就是为这个类建立一个虚函数表。可见这个偏移就是这个虚函数表了(这里我们也要注意一下,着重记一下对象的前四个字节是虚函数表的偏移,因为在后面释放对象时会用到)。
3).函数体代码
最后大家可以看00401E3D一直到最后的代码就是我们在构造函数里面对全局变量g_Number加一并输出的过程了,最后构造函数将对像的起始地址放入EAX寄存器中并返回。
3.1.2. 类中没有虚函数的情况
当我们申请一个类对像时,如果这个类中没有虚函数情况则会发现一些变化。还是基于上面的CMemoryItem类,但是让我们修改一下它的析构函数详细如下:
virtual ~CMemoryItem()这个原来类中析构函数的声明现在让我们把virtual关键字去掉再来跟踪一下程序的执情况,下面列出关键部分汇编代码。
00402739 add eax,0Ah
0040273C push eax
0040273D push offset THIS_FILE (00416690)
00402742 push 4
00402744 call operator new (00402310)
00402749 add esp,0Ch
上面是程序编译后申请内存部份的汇编代码,前三句与上面第一节中的汇编代码没有什么不同,但让我们注意一下第四句,这句我们知道是告诉NEW函数需要为对象申请内存的长度,让我们再回顾一下上面汇编代码它压栈的是8,但为什么这里是4呢? 让我们保留这个问题再向下跟踪代码来寻找答案。看看下面这部分代码:
0040275C mov ecx,dword ptr [ebp-1Ch]
0040275F call @ILT+15(CMemoryItem::CMemoryItem) (00401014)
00402764 mov dword ptr [ebp-28h],eax
显然这部分代与出上节的代码是没有区别的,别着急让我们进入构造函数去看看能不能有什么发现。仔细看下面的代码:
00401E30 push ebp
00401E31 mov ebp,esp
00401E33 push 0FFh
00401E35 push offset __ehhandler$??0CMemoryItem@@QAE@XZ (00403959)
00401E3A mov eax,fs:[00000000]
00401E40 push eax
00401E41 mov dword ptr fs:[0],esp
00401E48 sub esp,44h
00401E4B push ebx
00401E4C push esi
00401E4D push edi
00401E4E push ecx
00401E4F lea edi,[ebp-50h]
00401E52 mov ecx,11h
00401E57 mov eax,0CCCCCCCCh
00401E5C rep stos dword ptr [edi]
00401E5E pop ecx
00401E5F mov dword ptr [ebp-10h],ecx
00401E62 mov ecx,dword ptr [ebp-10h]
00401E65 call CString::CString (004022b6)
00401E6A mov dword ptr [ebp-4],0
00401E71 mov eax,[g_Number (00416900)]
00401E76 add eax,1
00401E79 mov [g_Number (00416900)],eax
00401E7E mov ecx,dword ptr [g_Number (00416900)]
00401E84 push ecx
00401E85 push offset string "Construct CMemoryItem Nember[%d]"... (004153f0)
00401E8A mov edx,dword ptr [ebp-10h]
00401E8D push edx
00401E8E call CString::Format (00402316)
00401E93 add esp,0Ch
225: OutputDebugString(m_info);
00401E96 mov ecx,dword ptr [ebp-10h]
00401E99 call CString::operator char const * (0040229e)
00401E9E mov esi,esp
00401EA0 push eax
00401EA1 call dword ptr [__imp__OutputDebugStringA@4 (004173ec)]
00401EA7 cmp esi,esp
00401EA9 call _chkesp (00402328)
00401EAE mov dword ptr [ebp-4],0FFFFFFFFh
00401EB5 mov eax,dword ptr [ebp-10h]
00401EB8 mov ecx,dword ptr [ebp-0Ch]
00401EBB mov dword ptr fs:[0],ecx
00401EC2 pop edi
00401EC3 pop esi
00401EC4 pop ebx
00401EC5 add esp,50h
00401EC8 cmp ebp,esp
00401ECA call _chkesp (00402328)
00401ECF mov esp,ebp
00401ED1 pop ebp
00401ED2 ret
这部分汇编代码与上节的相比较看一下有什么不同,和上面的一样这部份汇编代码也是函数初始化代码、类初始化代码、函数体代码三部份。首先让我们看一下第一部分(函数执行前的初始化代码)显然没有什么不同;接下来让我们看看类初始代码部分开始是对成员变量的构造,但是少了虚函数表的初始化(我们这个类里没有虚函数的),从这里我们可以发现没有虚函数的类会比有虚函数的类少四个字节,这也能解释我们上节的类是8个字节而这里是4个字节了;最后看看剩一下的汇编代码也差不多一样。
从这里我们可以总结出没有虚函数的类会比有虚函数的类少4个字节,在类的构造代码中也不会有对虚函数表的初始化。这点很重要这将影响它们在释放内存时的一些流程的不同。下面讲delete操作时我们再详细讨论。
3.2. NEW一个类对象数组的过程
上面详细剖析了为一个对像申请内存的过程,这里我们再来看一下为一个类申请一个对象数组时具体发现一些什么情况。还是上面CMemoryItem类,具体代码如下:
代码:
CMemoryItem* pItem = new CMemoryItem[10];
delete[] pItem;
我们在CMemoryItem* pItem = new CMemoryItem[10];设一个断点看看具体的汇编代码,代码如下:
00401D49 add eax,0Ah
00401D4C push eax
00401D4D push offset THIS_FILE (00416690)
00401D52 push 54h
00401D54 call operator new (0040233e)
我们先来分析一下这段代码,前三句是一样的不需细想,第四句将要申请的内存空间长度入栈;但是我们申请了10个元素,每个元素8个字节总长是要80个字节换算成十六进制就是50,但这里入栈时却多了四个字节,这四个字节是做什么用的呢?让我们保留这个疑问,接着看下面的代码继续分析这其中的缘由。我们再来看一段汇编代码:
00401D59 add esp,0Ch
00401D5C mov dword ptr [ebp-1Ch],eax
00401D5F mov dword ptr [ebp-4],0
00401D66 cmp dword ptr [ebp-1Ch],0
00401D6A je CTestMemAllocateDlg::OnDelete+8Ah (00401d9a)
00401D6C push offset @ILT+55(CMemeryItem::~CMemeryItem) (0040103c)
00401D71 push offset @ILT+15(CMemeryItem::CMemeryItem) (00401014)
00401D76 mov ecx,dword ptr [ebp-1Ch]
00401D79 mov dword ptr [ecx],0Ah
00401D7F push 0Ah
00401D81 push 8
00401D83 mov edx,dword ptr [ebp-1Ch]
00401D86 add edx,4
00401D89 push edx
00401D8A call `eh vector constructor iterator' (004023c0)
00401D8F mov eax,dword ptr [ebp-1Ch]
00401D92 add eax,4
00401D95 mov dword ptr [ebp-28h],eax
00401D98 jmp CTestMemAllocateDlg::OnDelete+91h (00401da1)
这代码段代码调用NEW函数之后的代码,先看00401D5C处的代码eax是NEW函数返回的内存地址,它被存入了dword ptr [ebp-1Ch],我们记下这个地址后面构造时会用到这个地址。00401D5F—00401D6A这段代码是编译器自动插入检查NEW函数申请内存是否成功的。00401D6C将类的析构函数地址入栈,00401D71将类的构造函数地址入栈;00401D76这句我们要注意一下dword ptr [ebp-1Ch](为类数组申请的内存首地址)MOV给了ECX,下面边这句把OAH(十进制10)放到入到了内存的前四个字节;00401D7F—00401D89是分别将数组元素的个数、元素的长度、数组的起始地址(申请内存块的第五个字节开始),00401D8A调用一个函数来构造这个数组,为了更深入的理解对象数组内存的申请,我们进入这个函数看看其中的详细的过程,看下面的汇编代码:
004023C0 push ebp
004023C1 mov ebp,esp
004023C3 push 0FFh
004023C5 push offset _LIBID_ATLLib+10h (004154b0)
004023CA push offset _except_handler3 (004027c0)
004023CF mov eax,fs:[00000000]
004023D5 push eax
004023D6 mov dword ptr fs:[0],esp
004023DD add esp,0F0h
004023E0 push ebx
004023E1 push esi
004023E2 push edi
004023E3 mov dword ptr [ebp-20h],0
004023EA mov dword ptr [ebp-4],0
004023F1 mov dword ptr [ebp-1Ch],0
004023F8 jmp `eh vector constructor iterator'+43h (00402403)
004023FA mov eax,dword ptr [ebp-1Ch]
004023FD add eax,1
00402400 mov dword ptr [ebp-1Ch],eax
00402403 mov ecx,dword ptr [ebp-1Ch]
00402406 cmp ecx,dword ptr [ebp+10h]
00402409 jge `eh vector constructor iterator'+5Ch (0040241c)
0040240B mov ecx,dword ptr [ebp+8]
0040240E call dword ptr [ebp+14h]
00402411 mov edx,dword ptr [ebp+8]
00402414 add edx,dword ptr [ebp+0Ch]
00402417 mov dword ptr [ebp+8],edx
0040241A jmp `eh vector constructor iterator'+3Ah (004023fa)
0040241C mov dword ptr [ebp-20h],1
00402423 mov dword ptr [ebp-4],0FFFFFFFFh
0040242A call `eh vector constructor iterator'+71h (00402431)
0040242F jmp `eh vector constructor iterator'+8Dh (0040244d)
00402431 cmp dword ptr [ebp-20h],0
00402435 jne `eh vector constructor iterator'+8Ch (0040244c)
00402437 mov eax,dword ptr [ebp+18h]
0040243A push eax
0040243B mov ecx,dword ptr [ebp-1Ch]
0040243E push ecx
0040243F mov edx,dword ptr [ebp+0Ch]
00402442 push edx
00402443 mov eax,dword ptr [ebp+8]
00402446 push eax
00402447 call __ArrayUnwind (00402510)
0040244C ret
0040244D mov ecx,dword ptr [ebp-10h]
00402450 mov dword ptr fs:[0],ecx
00402457 pop edi
00402458 pop esi
00402459 pop ebx
0040245A mov esp,ebp
0040245C pop ebp
0040245D ret 14h
这段代码比以前的代码复杂一些,同样我们分段来查看。先看004023C0—00402400这段函数调用前处理堆栈的这里略过;重点看0040240B这句这里重点是搞清楚dword ptr [ebp+8]里是什么,再回顾一下我们刚才入栈的顺序析构函数、构造函数、数组元素的个数、元素的长度、数组的起始地址、EBP(栈基址寄存器);按照栈先进后出的原则,那dword ptr [ebp+8]就是数组的起始地址(其它参数也按照这个方法计算出来)。再看下在这个调用dword ptr [ebp+14h],按照刚才的算法这地址存放着数的构造函数地址,那么这里与为单个类对象申请内存时构造是一样的。00402411—00402417这三句也很清楚,将数组的起始地址向移动了一个元素的长度。0040241A这句调转到了004023FA,而004023FA—00402406这几句计算当前元素构造的个数,从0开始计数所以我们看到构造组数时是从第一个开始的一直到最后一个元素。这里我们分析出当为一个类NEW数组时,它实际上多了四个字节(用来存放数组元素的个数)。那么如果NEW的不是一个类数组,如果一个基本数据类型或者是一个结构体的数组呢?如果 NEW的是一个基本数据类型(long 、int等)的数组,上面的那那种情况是不会出现的(不会多出四个字节来存放无素的个数),而对结构体则分两种情况如果结构体中不包函数成员那么和基本数据类型一样,具体的分析过程大家可以自己测试分析这里不再赘述。
4. delete[]与delete的差别
这节一节我们对NEW操作做了详细的讨论,这一节我们来讨论一下delete[]与delete的差别;试验时用的类还是上面的CMemoryItem类。
4.1. 释放类对像数组时的跟踪
delete[]与delete的差别主要在于前者用来释放NEW的数组后者用来释放NEW的单个对象,我们先把CMemoryItem类的析构函数的virtual修饰符去掉,然后让我们看看两段代码片段的具体执行情况。
代码1:
CMemoryItem* pItem = new CMemoryItem[10];
delete[] pItem;
执行输出:
Construct CMemoryItem Nember[1]
Construct CMemoryItem Nember[2]
Construct CMemoryItem Nember[3]
Construct CMemoryItem Nember[4]
Construct CMemoryItem Nember[5]
Construct CMemoryItem Nember[6]
Construct CMemoryItem Nember[7]
Construct CMemoryItem Nember[8]
Construct CMemoryItem Nember[9]
Construct CMemoryItem Nember[10]
Destructor CMemoryItem Nember[10]
Destructor CMemoryItem Nember[9]
Destructor CMemoryItem Nember[8]
Destructor CMemoryItem Nember[7]
Destructor CMemoryItem Nember[6]
Destructor CMemoryItem Nember[5]
Destructor CMemoryItem Nember[4]
Destructor CMemoryItem Nember[3]
Destructor CMemoryItem Nember[2]
Destructor CMemoryItem Nember[1]
大家仔细看一下new和delete时数组里元素的构造与析构的顺序是相逆的,接下来我们再来看一个示例代码片段的执行:
代码1:
CMemoryItem* pItem = new CMemoryItem[10];
delete pItem;
执行输出:
Destructor CMemoryItem Nember[1]
然后出现一个断言失败的对话框
我们单看输出的那一行就可了解大概,系统把这块内存当一个类来释放了,这样必定会出错了。
接下来我们看一下这个提示框是怎么回事。它提示我们_CrtIsValidHeapPointer返回失败了,那这个函数是做什么用的呢?我们在MDSN中可得它的解释,它是做用来验证内存指针有效性的一个功能函数(那它是怎么出现在我们程中的呢?经过跟踪调试我们发现它在delete运算符的DEBUG版本实现中被调用了)。这就很奇怪了,我们的代码就两句怎么刚申请的内存再释放就变成无效的了呢?还是让我们来看一下释放的过程吧,首先让我们看看代码1的汇编实现,下面是主要的汇编代码版段:
00401EA8 mov ecx,dword ptr [ebp-20h]
00401EAB call @ILT+165(CMemoryItem::`vector deleting destructor') (004010aa)
00401EB0 mov dword ptr [ebp-2Ch],eax
第一句执行后通ECX中的值我们探查到ECX存放着pItem指向的地址;紧接着下面就调用了DELETE[],这里列出具体的汇编代码:
004020F0 push ebp
004020F1 mov ebp,esp
004020F3 sub esp,44h
004020F6 push ebx
004020F7 push esi
004020F8 push edi
004020F9 push ecx
004020FA lea edi,[ebp-44h]
004020FD mov ecx,11h
00402102 mov eax,0CCCCCCCCh
00402107 rep stos dword ptr [edi]
00402109 pop ecx
0040210A mov dword ptr [ebp-4],ecx
0040210D mov eax,dword ptr [ebp+8]
00402110 and eax,2
00402113 test eax,eax
00402115 je CMemoryItem::`vector deleting destructor'+5Fh (0040214f)
00402117 push offset @ILT+160(CMemoryItem::~CMemoryItem) (004010a5)
0040211C mov ecx,dword ptr [ebp-4]
0040211F mov edx,dword ptr [ecx-4]
00402122 push edx
00402123 push 4
00402125 mov eax,dword ptr [ebp-4]
00402128 push eax
00402129 call `eh vector destructor iterator' (004028f0)
0040212E mov ecx,dword ptr [ebp+8]
00402131 and ecx,1
00402134 test ecx,ecx
00402136 je CMemoryItem::`vector deleting destructor'+57h (00402147)
00402138 mov edx,dword ptr [ebp-4]
0040213B sub edx,4
0040213E push edx
0040213F call operator delete (00402786)
00402144 add esp,4
00402147 mov eax,dword ptr [ebp-4]
0040214A sub eax,4
0040214D jmp CMemoryItem::`vector deleting destructor'+80h (00402170)
0040214F mov ecx,dword ptr [ebp-4]
00402152 call @ILT+160(CMemoryItem::~CMemoryItem) (004010a5)
00402157 mov eax,dword ptr [ebp+8]
0040215A and eax,1
0040215D test eax,eax
0040215F je CMemoryItem::`vector deleting destructor'+7Dh (0040216d)
00402161 mov ecx,dword ptr [ebp-4]
00402164 push ecx
00402165 call operator delete (00402786)
0040216A add esp,4
0040216D mov eax,dword ptr [ebp-4]
00402170 pop edi
00402171 pop esi
00402172 pop ebx
00402173 add esp,44h
00402176 cmp ebp,esp
00402178 call _chkesp (004027d4)
0040217D mov esp,ebp
0040217F pop ebp
00402180 ret 4
在这个调用里循环为执行了每一个数组成员的析构函数,最后释放整块内存返回。而且大家可以从00402161这句看出,系统把我们的pItem指向的地址向前移了四个字节,上节的讨论中我们知道,为对像申请数组时会多申请四个字节并把前四个字节存放入数组的元素个数,那么释放这个数组也得从它的开始位置释放,所以要向前移动四个字节。这是一个正常的情况,那么出错的情况是怎么发生的呢?接下来让我们看一下代码2的汇编实现,下面是主要的汇编代码版段:
00401EA8 mov ecx,dword ptr [ebp-20h]
00401EAB call @ILT+170(CMemoryItem::`scalar deleting destructor') (004010af)
00401EB0 mov dword ptr [ebp-2Ch],eax
第一句不变,第二句变化了那这相关流程会是什么样呢?我们进入它的内部去跟踪一下。看下面跟踪的汇编代码:
004021B0 push ebp
004021B1 mov ebp,esp
004021B3 sub esp,44h
004021B6 push ebx
004021B7 push esi
004021B8 push edi
004021B9 push ecx
004021BA lea edi,[ebp-44h]
004021BD mov ecx,11h
004021C2 mov eax,0CCCCCCCCh
004021C7 rep stos dword ptr [edi]
004021C9 pop ecx
004021CA mov dword ptr [ebp-4],ecx
004021CD mov ecx,dword ptr [ebp-4]
004021D0 call @ILT+160(CMemoryItem::~CMemoryItem) (004010a5)
004021D5 mov eax,dword ptr [ebp+8]
004021D8 and eax,1
004021DB test eax,eax
004021DD je CMemoryItem::`scalar deleting destructor'+3Bh (004021eb)
004021DF mov ecx,dword ptr [ebp-4]
004021E2 push ecx
004021E3 call operator delete (00402786)
004021E8 add esp,4
004021EB mov eax,dword ptr [ebp-4]
004021EE pop edi
004021EF pop esi
004021F0 pop ebx
004021F1 add esp,44h
004021F4 cmp ebp,esp
004021F6 call _chkesp (004027d4)
004021FB mov esp,ebp
004021FD pop ebp
004021FE ret 4
我们顺着看下来一直到004021C9,这句过后可得出ECX里存放的是pItem指向的地址。紧接着一直到004021D0,它是对类析构函数的调用。再向下看程序只做了一些检查就直接调用了释放内存的调用,这也能解释为什么我们能看到那一句输出。我们再想想上面我们一起讨论了NEW的过程,pItem指向的地址向前四个字节才是我们NEW所得内存的开始,但我们看004021E2这句它直接把pItem指向的地址传给了释放内存的调用,这当参数当然不是一个有效的内存块的起始地址了。如果深入跟踪的话我们可以了解到在DEBUG版本在堆内存的管理中,为每一块申请的内存建立了一个链表项并存放在一个链表中,释放时就去链表里找。而我们传入的那个地址显然不是一个已申请内存块的正确地址,所以系统会报错。
我们在上面实验的类中把CMemoryItem析构函数的virtual修饰符去掉了,我们现在再把它加做上面的实验看一下情况会有变化吗?大家可以试验一下,是没有差别的。那么在析构函数中加不加virtual修饰符真的在释放时没有差别吗?那么再让我们看一下单个对像的释放。
4.2. 释放类对像时的跟踪
同样我们先把CMemoryItem类的析构函数的virtual修饰符去掉,然后让我们来看两段代码片段的具体执行情况。
代码1:
CMemoryItem* pItem = new CMemoryItem;
delete pItem;
执行正常,再看下面的代码。
代码2:
CMemoryItem* pItem = new CMemoryItem;
delete[] pItem;
弹出了我们上面贴出的那个出错框。大家可能要说这很正常啊,那我们来看看一段不正常的吧。现在我们再把类析构函数的virtual修饰符加做上面的实验看一下情况。代码1没错(肯定的),我们再执行代码2也没错。是不是有些奇怪了,为了了解其中原因那我们就看它的汇编代码吧。下面列出代码:
00401E89 mov edx,dword ptr [ebp-20h]
00401E8C mov eax,dword ptr [edx]
00401E8E mov ecx,dword ptr [ebp-20h]
00401E91 call dword ptr [eax]
EDX 和ECX存放是的pItem指向的地址,因为这个类的析构函数加了virtual修饰符,那么这个类的前四个字节就是析构函数的指针(上节我们讨论)。00401E91这句也就调用了类的析构函数,这自然不会错。那么为什么类的析构函数不加virtual修饰符时就会出错呢?下面也列出一段汇编代码大家便可明白。
00401E87 mov ecx,dword ptr [ebp-20h]
00401E8A call @ILT+170(CMemoryItem::`vector deleting destructor') (004010af)
类的析构函数不加virtual修饰符时, delete[] pItem;系统把它当成一个对像数组去释放自然会出错。这也不是说我们只要我们为类的析构函数加了virtual修饰符就可以不分数组还是单个对像都用delete[]释放了。因为不同编译器或相同编译器的不同版会有不同的编译方法,我们只是对VC6.0编译后的程序做了剖析。大家可以试试其它编译下的情况。
5. 总结
这里和大家一起讨论了VC6.0中NEW和DELETE对像时的过程,希望能给大家的编程带来一些帮助。同时也由于笔者个人水平原因文中可能会出现一些谬误,希望大家不吝赐教。
- 剖析VC6.0中为类对象申请内存的过程
- 解析VC6.0中为类对象申请内存的过程
- 如何在VC6中申请分配大容量内存
- 如何在VC6中申请分配大容量内存
- 详细讲解从用户空间申请内存到内核如何为其分配内存的过程
- 为app申请更大的内存
- 内存申请过程
- VC6.0中编译和使用OpenSSL的过程
- 在vc6.0中除数为零的验证
- VC6 使用过程中遇到的问题
- C++中申请内存的正确方法
- Java中对象的创建过程(内存分析)
- Java创建对象过程中,内存的分配
- Java创建对象过程中,内存的分配
- vc6.0中如何查找内存泄露
- jvm中对象的内存布局(HotSpot为例)
- 为你的Android应用申请更多内存
- 关于为结构体指针申请内存的问题
- create window 7 wim
- java io调优 Tuning Java I/O Performance
- 多线程例子(2)
- asp破解(百度,网易,新浪,搜狐,QQ空间)图片防盗链代码
- PSP《大众高尔夫2P》XB资源包算法分析(1)
- 剖析VC6.0中为类对象申请内存的过程
- Linux DNW烧写实例
- 破解百度空间、新浪相册、网易、搜狐等博客图片防盗链的方法
- 房屋出租管理系统用到的数据库(视图、存储过程、触发器)
- PSP《大众高尔夫2P》XB资源包算法分析(2)
- ubuntu 图形界面与字符界面切换
- select into outfile和load data infile的问题
- 典型的几种机器人控制架构介绍
- MMS5