摘要: 在用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