C++基本概念在编译器中的实现

C++基本概念在编译器中的实现,第1张

C++基本概念在编译器中的实现,第2张

相信很多程序员都很熟悉C++对象模型。本文试图通过一个简单的例子来演示C++的一些基本概念在编译器中的实现,以达到眼见为实的效果。

1.对象之间的虚函数空

1.1对象之间空

例如,当我们为一个对象分配空空间时:

c child 1 * pChild = new c child 1();

这个空房间里有什么?

当CCChild1没有虚函数时,其基类的非静态成员和自身的非静态成员依次放在CCChild1的object 空中。没有任何非静态成员的对象将有一个一字节的占位符。

如果CChild1有虚函数,VC6编译器会在objects 空前面加一个指针,就是虚函数表指针(VPTR)。让我们看看这段代码:

class cmember 1 {
public:
cmember 1(){ a = 0x 5678;printf(" construct cmember 1 \ n ");}
~ cmember 1(){ printf(" destruct cmember 1 \ n ");}
int a;
};

class c parent 1 {
public:
c parent 1(){ parent _ data = 0x 1234;printf(" construct cparent 1 \ n ");}
virtual ~ c parent 1(){ printf(" destruct c parent 1 \ n ");}
virtual void test(){ printf(" call cpart 1::test()\ n \ n ");}
void real(){ printf(" call cpart 1::test()\ n \ n ");}
int parent _ data;
};

class child 1:public cpart 1 {
public:
ccchil 1(){ printf(" construct ccchil 1 \ n ");}
virtual ~ c child 1(){ printf(" destruct c child 1 \ n ");}
Virtual void test(){ printf(" Call c child 1::test()\ n \ n ");}
void real(){ printf(" Call ccchil 1::test()\ n \ n ");}
CMember1成员;
static int b;
};

child 1对象的大小是多少?以下是演示程序的打印输出:

-->派生类对象
对象地址0x00370FE0
对象大小12
对象内容
00370 fe0:00410104 0001234 0005678
VPTR内容[/br

子1对象大小为12字节,包括:Vptr,parent_data,基类成员变量,派生类成员变量。Vptr指向的虚函数表(VTable)是虚函数地址的数组。

1.2 Vptr和VTable

如果我们用VC自带的dumPBin反汇编输出程序的调试版本:

dumpbin/disasm test _ VC6 . exe > a . txt

可以在a.txt中找到:

?test @ c child 1 @ @ UAEXXZ:
00401640:55推送ebp...
??_ ecchild 1 @ @ UAE paxi @ Z:
004016 a 0:55推送ebp

可以看到,VTable中的两个地址分别指向了ccchild1的析构函数和ccchild1的成员函数test。这两个函数是CChild1的虚函数。如果打印两个CChild1对象的内容,可以发现它们的Vptr是相同的,即每个有虚函数的类都有一个VTable,这个类中所有对象的Vptr都指向这个VTable。

这里的函数名是不是有点奇怪?附录2简单介绍了C++的名字Mangling。

1.3静态成员变量

在C++中,一个类的静态变量相当于具有访问控制的全局变量,不占用对象空之间的空间。它们的地址是在编译链接时确定的。例如,如果我们在项目的链接设置中选择“生成地图文件”,构建后,我们可以在生成的地图文件中看到:

0003:00002e18?b @ c child 1 @ @ 2HA 00414 e18 test1 . obj

从打印输出中我们可以看到,CChild1::b的地址正好是0x00414E18。实际上,类定义中变量B的声明只是一个声明。如果我们不在类定义(全局域)之外定义这个变量,这个变量根本就不存在。

1.4调用虚函数

通过在VC调试环境下设置断点,切换到汇编显示模式,可以看到调用虚函数的汇编代码:

16:pChild-> test();
(1) mov edx,dword ptr[pChild]
(2)mov eax,dword ptr [edx]
(3) mov esi,esp
(4) mov ecx,dword ptr [pChild]
(5)调用dword ptr [eax+4]

语句(1)将对象的地址放入寄存器edx,语句(2)将对象地址处的Vptr加载到寄存器eax,语句(5)跳转到Vptr指向的VTable的第二项地址,即成员函数test。

语句(4)把对象的地址放在寄存器ecx中,这就隐含了这个指针传递到非静态成员函数中。非静态成员函数通过这个指针访问非静态成员变量。

1.5虚函数和非虚函数

在演示程序中,我们打印了成员函数地址:

Printf("CParent1::测试地址0x%08p\n ",& cparent 1::test);
printf("CChild1::测试地址0x%08p\n ",& c child 1::test);
printf(" c parent 1::real address 0x % 08p \ n ",& c parent 1::real);
printf(" c child 1::real address 0x % 08p \ n ",& c child 1::real);

获得以下输出:

CParent1::测试地址0x004018F0
CChild1::测试地址0x004018F0
CParent1::实地址0x00401460
CChild1::实地址0x00401670

两个非虚函数的地址很容易理解,它们可以在dumpbin的输出中找到:

?real @ cparent 1 @ @ QAEXXZ:00401460:55推送ebp...
?real @ c child 1 @ @ QAEXXZ:00401670:55推送ebp

为什么两个虚函数的「地址」是一样的?实际上,thunk代码的地址打印在这里。通过查看dumpbin的输出,我们可以看到:

_9@$B3AE:
(6) mov eax,dword ptr[ecx]
(7)jmp dword ptr[eax+4]

如果我们在跳转到这段代码之前把对象地址放在寄存器ecx中,语句(6)会把对象地址处的Vptr加载到寄存器eax中,语句(7)会跳转到Vptr指向的VTable的第二项地址,也就是成员函数test。基类和派生类VTable的虚函数排列顺序相同,所以可以共享一段thunk代码。

这个thunk代码的目的是通过函数指针调用虚函数。如果我们不取函数地址,编译器就不会生成这段代码。请注意不要将本节中的thunk代码与VTable中的虚函数地址混淆。Thunk代码根据传入的对象指针决定调用哪个函数,VTable中的虚函数地址就是实函数地址。

1.6指向虚函数的指针

我们试试通过指针调用虚函数。非成员函数指针必须通过对象指针调用:

typedef void(Parent::* PMem)();
printf(" \ n-->由函数指针调用\ n ");
PMem pm = & Parent::test;
printf("函数指针0x%08p\n ",pm);
(p parent-> * pm)();

获得以下输出:

->通过函数指针调用

函数指针0x004018F0
调用CChild1::test()

我们从VC调试环境中复制了这段汇编代码:

13:(p parent-> * pm)();
(8) mov esi,esp
(9) mov ecx,dword ptr [pParent]
(10)调用dword ptr [pm]

语句(9)将对象指针放入寄存器ecx,语句(10)调用函数指针指向的thunk代码,也就是1.5节的语句(6)。接下来会发生什么?前面已经说过了。

1.7多态性的实现

经过前面的分析,多态的实现应该是显而易见的。当调用一个基类指针指向派生类对象的虚函数时,调用的当然是派生类的函数,因为派生类对象的Vptr指向派生类的VTable。

通过函数指针调用虚函数也需要通过VTable确定虚函数地址,所以也会发生多态性,即在当前对象VTable中调用虚函数。

位律师回复
DABAN RP主题是一个优秀的主题,极致后台体验,无插件,集成会员系统
白度搜_经验知识百科全书 » C++基本概念在编译器中的实现

0条评论

发表评论

提供最优质的资源集合

立即查看 了解详情