您的位置:首页 > 编程语言 > C语言/C++

C++虚函数的实现细节、虚析构函数 汇编解析

2008-10-24 17:43 253 查看
C++里多态的实现,依靠的是虚函数的运行时函数地址确定,不过真正的实现过程,还是在编译阶段。编译器究竟对虚函数做了怎样的处理?这就是本文所描述的。然后又对虚函数中最特殊的虚析构函数的运行情况进行了分析。
1、类的存储空间
在INTEL 32 CPU,VC6环境下,空类的一个实例占一个字节(特例);
一个C++类本身(注意:不是对象),在内存里是有信息的, 比如虚函数表、静态成员变量。
函数虚函数的类,它的每一个对象实例,在内存中,头四个字节存放的都是虚函数表指针。
2、虚函数的实现过程
对虚函数,编译器不给出直接的函数调用地址,而是关于一个未知量的表达式,这个参数就是虚函数表的指针。使用虚函数实现C++多态的方法,网上很多讲解,这里不多讲,本文结合源代码和部分汇编代码,分析了编译过程中对虚函数的处理。
3、虚析构函数
无论基类的析构函数是否为虚析构函数. 基类的析构函数总是会被自动调用的;但是, 如果用基类指针去操作一个了派生类对象,那么在delete这个基类指针时,派生类的析构函数将不会被调用。
4. 本文为原创,转载或其他用途请注明出处:http://blog.csdn.net/ydbcsdn/archive/2008/10/24/3137384.aspx

// VC测试代码

#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers

#include <stdio.h>

class Base;

class Derived;

void GFunction(void);

int main(int argc, char* argv[])

{

GFunction();

char a = 127;

a+=1;

printf("new a = %d/n", a);

getchar();

return 0;

}

class Base

{

public:

Base::Base()

{

};

virtual Base::~Base()

{

printf("Base deconstruct/n");

};

virtual void Fun()

{

};

int a ;

};

class Derived : public Base

{

public:

Derived::Derived()

{

};

virtual Derived::~Derived()

{

printf("Derived deconstruct/n");

};

virtual void Fun()

{

};

};

void GFunction(void)

{

printf("Class Base Sizeof =%d /n", sizeof(Base));

printf("Class Derived Sizeof =%d /n/n", sizeof(Derived));

Base* pA = (Base*)new Derived;

pA->Fun(); // 虚函数调用

delete pA;

}

pA->Fun()的汇编代码如下:

59: pA->Fun();

0040D75C mov edx,dword ptr [ebp-10h] // edx为pA

0040D75F mov eax,dword ptr [edx] // eax为pA对象的虚表指针pVTable

0040D761 mov esi,esp

0040D763 mov ecx,dword ptr [ebp-10h] // this指针存入ecx

0040D766 call dword ptr [eax+4] // 函数地址:虚表指针+4, 就是虚表中第二项

0040D769 cmp esi,esp

0040D76B call __chkesp (00401b20)

1. 如果成员函数不是虚函数,那么编译的时候,就直接指定了调用函数的入口;
2. 如果是虚函数,那么编译时不直接指定函数入口,而是先在对象的内存空间里取一个值(这个值就是虚函数表的地址,放在对象内存空间的最前面4个字节里)。汇编代码中会有取值的过程;
3. 虚函数表中按顺序存放着虚函数在地址空间中的地址:的第一个DWORD存储的就是第一个虚函数的地址,第二个DWORD存储的就是第二个虚函数的地址;
3. 编译器在编译过程中已经知道你调的那个函数在虚函数表中的序号。汇编代码中会有体现;
4. 在运行时,就能正确找到调用函数的地址,并调用它.

析构函数的一点补充:

在一个项目中,如果有N层派生类,编译器总是保证所有基类的析构函数都被依次调用,但问题是,究竟从那层开始调用呢?对于非虚析构函数,显然是在编译期间就直接确定的,对虚析构函数,在运行时,才能确定是从哪一层开始往下层调用(基类)。

事实上,和一般虚函数一样,运行时,才确定要调用的析构函数,不过有些不同的是,析构函数执行完后,下一条指令就是基类析构函数的CALL指令,一直到最上层为止。

下面是一段debug下的反汇编,其中Base派生自BaseBase.可以看到~Base调用后,会自动调用~BaseBase

virtual Base::~Base() // 基类析构函数

{

00401740 push ebp

00401741 mov ebp,esp

00401743 sub esp,0CCh

00401749 push ebx

0040174A push esi

0040174B push edi

0040174C push ecx

0040174D lea edi,[ebp-0CCh]

00401753 mov ecx,33h

00401758 mov eax,0CCCCCCCCh

0040175D rep stos dword ptr es:[edi]

0040175F pop ecx

00401760 mov dword ptr [ebp-8],ecx

00401763 mov eax,dword ptr [this]

00401766 mov dword ptr [eax],offset Base::`vftable' (44533Ch)

printf("Base deconstruct/n");

0040176C push offset string "Base deconstruct/n" (445344h)

00401771 call printf (405760h)

00401776 add esp,4

};

00401779 mov ecx,dword ptr [this]

0040177C call BaseBase::~BaseBase (4017A0h) // 上层基类也紧跟着调用了
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: