剖析VC6.0中为类对象申请内存的过程

来源:互联网 发布:jsp连接mysql 编辑:程序博客网 时间:2024/06/07 18:38

摘要:在用VC开发时常常需要对内存进行操作,其中C++中提供了运算符newdelete;而C语言库中提供有mallocfree。它们在具体使用时存在着一些差别,本文主要剖析一下VC6.0中为类对象申请内存的过程。方便大家理解在为类对象申请或释放内存时的一个问题。

    关键词:VC6.0,内存申请,C++newdelete

1.   总述

在用VC开发时常常需要对内存进行操作,其中C++中提供了运算符newdelete;而C语言库中提供有mallocfree。它们在具体使用时存在着一些差别,大家也可能一些疑问再里面,如:为什么C++运算符new再为类对象申请内存时会调用类的构造函数而malloc则不会,为什么用new申请一个对象数组时就必须用delete[]释放而申请一个long类型的数组就能用delete释放,new一个对像与new一个对象数组以及new一个基本数据类型的数组有什么不同,为什么用new申请一个对像用delete[]释放有时会出错而有时就成功出错是随机的吗等等;本文主要剖析一下VC中为类对象申请内存的过程同时解答上述几个疑问;希望能给大家予以一定帮助。这里说明一下试验的环境:VC6.0编译环境,IDE5.0。

2.   malloc VS new

这个两操作的区别主要是在申请一个类的对象时,malloc不会调用类对象的构造函数而new会调用,我们这里定义一个类用来实际测试,下面我们将以这个类为基础讨论mallocnew的差别。

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语言中mallocfree的执行情况。

代码2

CMemoryItem* pItem = (CMemoryItem*)malloc(sizeof(CMemoryItem));

free(pItem);

执行输出:

这时我们没有看到有代码1那样的输出,可见malloc时并没有去调用类的构造函数,free时也没有调用类的析构函数;这样我们就验证的了newdelete在为类申请内存时会调用类的构造与析构函数,而mallocfree不会。下面我们一起看看它们汇编代码是什么样一个情况,首先来看代码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则没有,这是因为mallocC语言中的函数在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).函数初始化代码

    首先我们来简单看一下00401DF000401E1C的这段代码,其主要是一些保存当前状态的操作,大家可以看到最后一个入栈的寄存器ECX,那也就是说代码一直执行00401E1CECX都是栈顶第一个元素。下面代码接着是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类的对象所以这里调用了它的默认构造函数来构造这个成员变量。接着我们再看00401E3400401E37这两句汇编代码;先把对象的起始地址放到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处的代码eaxNEW函数返回的内存地址,它被存入了dword ptr [ebp-1Ch],我们记下这个地址后面构造时会用到这个地址。00401D5F00401D6A这段代码是编译器自动插入检查NEW函数申请内存是否成功的。00401D6C将类的析构函数地址入栈,00401D71将类的构造函数地址入栈;00401D76这句我们要注意一下dword ptr [ebp-1Ch](为类数组申请的内存首地址)MOV给了ECX,下面边这句把OAH(十进制10)放到入到了内存的前四个字节;00401D7F00401D89是分别将数组元素的个数、元素的长度、数组的起始地址(申请内存块的第五个字节开始),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

这段代码比以前的代码复杂一些,同样我们分段来查看。先看004023C000402400这段函数调用前处理堆栈的这里略过;重点看0040240B这句这里重点是搞清楚dword ptr [ebp+8]里是什么,再回顾一下我们刚才入栈的顺序析构函数、构造函数、数组元素的个数、元素的长度、数组的起始地址、EBP(栈基址寄存器);按照栈先进后出的原则,那dword ptr [ebp+8]就是数组的起始地址(其它参数也按照这个方法计算出来)。再看下在这个调用dword ptr [ebp+14h],按照刚才的算法这地址存放着数的构造函数地址,那么这里与为单个类对象申请内存时构造是一样的。0040241100402417这三句也很清楚,将数组的起始地址向移动了一个元素的长度。0040241A这句调转到了004023FA,而004023FA00402406这几句计算当前元素构造的个数,从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]

大家仔细看一下newdelete时数组里元素的构造与析构的顺序是相逆的,接下来我们再来看一个示例代码片段的执行:

代码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.0NEWDELETE对像时的过程,希望能给大家的编程带来一些帮助。同时也由于笔者个人水平原因文中可能会出现一些谬误,希望大家不吝赐教。