关于operator new/delete的重载,应该算是老生常谈了。大家都知道C++中new除了分配内存(调用operator new),还会调用构造函数,delete除了回收内存(调用operator delete),还会调用析构函数。然而其中真正的流程却不一定完全明了。 在最近的项目中,我就发现了一个少有人关注的奇异现象。那就是,在简单跟踪调试operator new/delete时(VS2005),如果是new一个类对象,且该类定义了析构函数,你会发现程序调用了你重载的operator new,却似乎根本不会调用operator delete。 其实说来,这应该是自己学艺不精导致对C++的误解,但我估计大多数人对于本文所述内容的理解都是似是而非的,因为我跟多位同事以及网友们的进行了多次讨论,均没有得到结果。 事情还要从近期的项目说起。 忙里得闲,近日有心留意了下现在项目中的部分代码,发现很多同事编码很不严谨,诸如数据没有初始化就是用、参数合法性检查、空指针、野指针、内存泄露等现象非常严重。这些问题如果coding的时候不加注意,之后再来查找问题都是繁琐且困难的,除了内存泄露似乎还有个比较方便的解决方案,即使用宏重定义malloc/alloc和free、重载new和delete等方法,跟踪内存的分配和释放过程,并将其记录在案。 我很快写出了几段代码,添加到项目工程中,编译运行程序,初步测试运行,结果令人汗颜:短短几分钟时间,内存泄露近5M! 乖乖!手机上跑的小程序啊!于是根据泄露记录定位查看代码,跟踪调试…… 这一回让我发现了更大的问题!我发现某些地方的delete操作似乎根本就没有调用到我重载的operator delete,而operator new调用却是用的一点问题也没有。有现象就开始找规律。从普通的内建数据类型int、char到class、virtual class,经过多次尝试发现内建类型没有该问题,class和virtual class在没有析构函数(Destructor)时也没问题,而一旦给class定义了destructor,调试就再也跟不进operator代码了。 好了,罗嗦了太多的过程,下面看示例代码(测试工程附在文后)。注意,这里的初衷是跟踪内存泄露,所以operator new重载添加了两个参数,及源文件名及调用new的行号。
operator new/delete 重载头文件
operator new/delete 重载头文件
下面看测试代码:
在VS2005下F11单步跟踪调试上面的代码,前面1、2两项都可以顺利跟进重载的operator new和operator delete,但是delete 3却无法进入,直接跳到了return 0,退出了! 观察对比生成的汇编代码(片段): char* p1 = new char; 0041192D push 1 0041192F call operator new (4113B6h) // 这里显式调用了operator new 00411934 add esp,4 00411937 mov dword ptr [ebp-14Ch],eax 0041193D mov eax,dword ptr [ebp-14Ch] 00411943 mov dword ptr [ebp-14h],eax delete p1; 00411946 mov eax,dword ptr [ebp-14h] 00411949 mov dword ptr [ebp-140h],eax 0041194F mov ecx,dword ptr [ebp-140h] 00411955 push ecx 00411956 call operator delete (41117Ch) // 这里显式调用了operator delete 0041195B add esp,4 A* pTest = new A; 0041198F push 4 00411991 call operator new (4113B6h) // 这里显式调用了operator new 00411996 add esp,4 00411999 mov dword ptr [ebp-110h],eax 0041199F mov dword ptr [ebp-4],0 004119A6 cmp dword ptr [ebp-110h],0 004119AD je main+0D2h (4119C2h) 004119AF mov ecx,dword ptr [ebp-110h] 004119B5 call A::A (411479h) 004119BA mov dword ptr [ebp-154h],eax 004119C0 jmp main+0DCh (4119CCh) 004119C2 mov dword ptr [ebp-154h],0 004119CC mov eax,dword ptr [ebp-154h] 004119D2 mov dword ptr [ebp-11Ch],eax 004119D8 mov dword ptr [ebp-4],0FFFFFFFFh 004119DF mov ecx,dword ptr [ebp-11Ch] 004119E5 mov dword ptr [ebp-2Ch],ecx delete pTest; // 这里似乎没有类似上面delete p1那样的调用operator delete 004119E8 mov eax,dword ptr [ebp-2Ch] 004119EB mov dword ptr [ebp-0F8h],eax 004119F1 mov ecx,dword ptr [ebp-0F8h] 004119F7 mov dword ptr [ebp-104h],ecx 004119FD cmp dword ptr [ebp-104h],0 00411A04 je main+13Bh (411A2Bh) 00411A06 mov esi,esp 00411A08 push 1 00411A0A mov edx,dword ptr [ebp-104h] 00411A10 mov eax,dword ptr [edx] 00411A12 mov ecx,dword ptr [ebp-104h] 00411A18 mov edx,dword ptr [eax] 00411A1A call edx 00411A1C cmp esi,esp 00411A1E call @ILT+780(__RTC_CheckEsp) (411311h) // 调用A::scalar deleting destructor 00411A23 mov dword ptr [ebp-154h],eax 00411A29 jmp main+145h (411A35h) 00411A2B mov dword ptr [ebp-154h],0 查了一天资料,都没有找到关于此问题的详细说明,莫非这是VC的bug?不太可能! 就在今晚无奈之余,google到了这样一篇文章“The Destructors and the call to operator delete”(http://www./oop/des.html),其中提到: “Every container of type T has an implicit definition of a destructor-function T::~T(){}. Unlike constructor-functions, destructor-functions are callable functions(destructors are called both implicitly and explicitly). T obj; T * ptr = new T(); obj.~T(); //valid, a call to destructor ptr->~T(); //valid, a call to destructor delete ptr; //or this, the appropriate way of calling the destructors · In the case of "T obj;" the respective destructor will be automatically called, when the respective composite goes out of scope. · In the case of "T * ptr;" the destructor-function should be explicitly called at the point/place where the programmer thinks that the "memory pointed to" will no longer be used in the rest of the programm. One other aspect of this type of call to the destructor is the subsequent second or third ore more calls to the same destructor, even though the memory block of the composite was released/freed by the first call to the destructor. You can do that(rpeating calls) till the last identifier is visible or in scope(here identifier is the "ptr" and is null but even then the respective destructor can be activated by the call ptr->~T()). In every destructor T::~T() there is a call to operator delete. It can be the default operator ::delete or the overloaded operator-function T::delete().” (对于类对象来说,)类的析构函数会(自动)调用operator delete(In every destructor T::~T() there is a call to operator delete)。于是跟踪进入析构函数直到返回,发现destructor之后,程序(汇编代码)走到了这样一行代码“A::`scalar deleting destructor”,(注意,这在我的自己的代码里是看不到的,切换到反汇编代码即可),如下: A::`scalar deleting destructor': 00411B40 push ebp 00411B41 mov ebp,esp 00411B43 sub esp,0CCh 00411B49 push ebx 00411B4A push esi 00411B4B push edi 00411B4C push ecx 00411B4D lea edi,[ebp-0CCh] 00411B53 mov ecx,33h 00411B58 mov eax,0CCCCCCCCh 00411B5D rep stos dword ptr es:[edi] 00411B5F pop ecx 00411B60 mov dword ptr [ebp-8],ecx 00411B63 mov ecx,dword ptr [this] 00411B66 call A::~A (41147Eh) // 调用类的析构函数 00411B6B mov eax,dword ptr [ebp+8] 00411B6E and eax,1 00411B71 je A::`scalar deleting destructor'+3Fh (411B7Fh) 00411B73 mov eax,dword ptr [this] 00411B76 push eax 00411B77 call operator delete (41117Ch) // 这里由编译器负责调用了operator delete 00411B7C add esp,4 00411B7F mov eax,dword ptr [this] 00411B82 pop edi 00411B83 pop esi 00411B84 pop ebx 00411B85 add esp,0CCh 00411B8B cmp ebp,esp 00411B8D call @ILT+780(__RTC_CheckEsp) (411311h) 00411B92 mov esp,ebp 00411B94 pop ebp 00411B95 ret 4 终于看到了我希望的东西!原来,我们的一行delete p3代码,编译器需要拆分成两个步骤: p3->~A(); operator delete (p3); 而这两行代码,是由VC自己的名为“scalar deleting destructor”的函数来实现的。在我们重载的operator delete处以及A::~A()处都打上断点,终于我们看到我们的函数确是被正确执行的。 一切都真相大白了!operator new的重载过程简单明了,operator delete却是如此隐晦。 不是VC的bug,只是自己学艺不精,不明就里。VC顶多是设计的不完全友好,让我们在这种情况下跟踪程序过程时不太方便而已。 事实上,不只是destructor有这个特性,constructor也是一样。(摘自“The Constructors and the call to operator new ”,http://www./oop/cons.html) Constructors, they construct and initialize or they only initialize a preconstructed composit. It is the way they are called determines if they will construct or not(construction is optional). Every constructor has a built in call to the operator new(it can be the overloaded T::new() operator-function or the default ::new operator).
Every constructor expects the this-> pointer. It is the this-> pointer on which the constuctor decides to bypass or not to bypass the call to the operator new.
Defining composite on the stack... The above detail is reflected/passed through this-> pointer to the constructors
与析构函数调用operator delete类似,构造函数同样会调用operator new。 如上引文所说,构造函数会根据对象是否已被构造决定是否调用operator new,只是这里看来VC的编译器对constructor和destructor的处理有所不同而已(笔者自己的理解,如有不对,恳请看管指正)。从前面的new和delete一个对象的汇编代码片段,我们可以看出,编译器是在编译期通过以下的方式分别体现了这些区别。 对栈上构造的对象(自动变量对象)只调用构造函数,而对堆上构造的对象(new出来的对象)调用operator new,然后再调用构造函数。这算是一种优化吧? 而对析构函数,编译器则是自己生成了一段函数代码,将destructor与operator delete捆绑编译。 正式由于上述的原因,所以才会出现我们在调试期间看到的程序执行流程跟想象中的差别。 |
|