分享

深入研究constructor / destructor与operator new / delete (转)

 汇英四方 2014-02-27

关于operator new/delete的重载,应该算是老生常谈了。大家都知道C++new除了分配内存(调用operator new),还会调用构造函数,delete除了回收内存(调用operator delete),还会调用析构函数。然而其中真正的流程却不一定完全明了。

在最近的项目中,我就发现了一个少有人关注的奇异现象。那就是,在简单跟踪调试operator new/delete时(VS2005),如果是new一个类对象,且该类定义了析构函数,你会发现程序调用了你重载的operator new,却似乎根本不会调用operator delete

其实说来,这应该是自己学艺不精导致对C++的误解,但我估计大多数人对于本文所述内容的理解都是似是而非的,因为我跟多位同事以及网友们的进行了多次讨论,均没有得到结果。

事情还要从近期的项目说起。

忙里得闲,近日有心留意了下现在项目中的部分代码,发现很多同事编码很不严谨,诸如数据没有初始化就是用、参数合法性检查、空指针、野指针、内存泄露等现象非常严重。这些问题如果coding的时候不加注意,之后再来查找问题都是繁琐且困难的,除了内存泄露似乎还有个比较方便的解决方案,即使用宏重定义malloc/allocfree、重载newdelete等方法,跟踪内存的分配和释放过程,并将其记录在案。

我很快写出了几段代码,添加到项目工程中,编译运行程序,初步测试运行,结果令人汗颜:短短几分钟时间,内存泄露近5M

乖乖!手机上跑的小程序啊!于是根据泄露记录定位查看代码,跟踪调试……

这一回让我发现了更大的问题!我发现某些地方的delete操作似乎根本就没有调用到我重载的operator delete,而operator new调用却是用的一点问题也没有。有现象就开始找规律。从普通的内建数据类型intcharclassvirtual class,经过多次尝试发现内建类型没有该问题,classvirtual class在没有析构函数(Destructor)时也没问题,而一旦给class定义了destructor,调试就再也跟不进operator代码了。

好了,罗嗦了太多的过程,下面看示例代码(测试工程附在文后)。注意,这里的初衷是跟踪内存泄露,所以operator new重载添加了两个参数,及源文件名及调用new的行号。

 

下载示例工程

operator new/delete 重载头文件

 

 

operator new/delete 重载头文件

 

 

 下面看测试代码:

 

 

 

VS2005F11单步跟踪调试上面的代码,前面12两项都可以顺利跟进重载的operator newoperator 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

 

查了一天资料,都没有找到关于此问题的详细说明,莫非这是VCbug?不太可能!

就在今晚无奈之余,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 deleteIn 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却是如此隐晦。

不是VCbug,只是自己学艺不精,不明就里。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).

  • If we are defining a composite on the stack(the lifetime/scope of the composite is automatic(the auto storage class specifier has no linkage)) or if we are defining a composite on the data-segment(static or extern storage class specfier has internal or external linkage) then the call to the operator new is not made by the called constructor function, thus the constructor function only initializes the composite.
  • If we are defining the composite on the heap(the composite has an external linkage or internal linkage) then the call to the operator new is made by the called constructor function, thus the constructor first constructs and then it initializes.

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...

      T obj;                 //default constructor will be called
      T cp(obj);             //copy constructor will be called

Defining composite on the heap...

      T * ptr = new T();    //default constructor will be called
      T * cp  = new T(obj)  //copy constructor will be called

The above detail is reflected/passed through this-> pointer to the constructors

  • If this-> pointer is null then call to the operator new is made by the called constructor.
  • If this-> pointer is not null then no call to the operator new is made by the called constructor.

与析构函数调用operator delete类似,构造函数同样会调用operator new

如上引文所说,构造函数会根据对象是否已被构造决定是否调用operator new,只是这里看来VC的编译器对constructor和destructor的处理有所不同而已(笔者自己的理解,如有不对,恳请看管指正)。从前面的new和delete一个对象的汇编代码片段,我们可以看出,编译器是在编译期通过以下的方式分别体现了这些区别。

对栈上构造的对象(自动变量对象)只调用构造函数,而对堆上构造的对象(new出来的对象)调用operator new,然后再调用构造函数。这算是一种优化吧?

而对析构函数,编译器则是自己生成了一段函数代码,将destructor与operator delete捆绑编译。

正式由于上述的原因,所以才会出现我们在调试期间看到的程序执行流程跟想象中的差别。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多