记录几个C++多继承中,this指针与多虚表间编译与处理的疑问,看

简单无理的的测试代码:

#include <iostream>#include <stdio.h>using namespace std;class A{public:int x;int y;public:A(){cout<<"构造函数A传入的this指针得值是:"<<std::hex<<std::showbase<<this<<endl;}virtual void F1(){this->x = 1;this->y = 2;cout<<"A F1 this指针得值是:"<<std::hex<<std::showbase<<this<<endl;F3();//虚表入口地址} virtual void F3() = 0;};class B{public:int z;public:B(){cout<<"构造函数B传入的this指针得值是:"<<std::hex<<std::showbase<<this<<endl;}virtual void F2(){cout<<"B F2 this指针得值是:"<<std::hex<<std::showbase<<this<<endl;} };class C : public A, public B{public:int a;public:C(){cout<<"构造函数C传入的this指针得值是:"<<std::hex<<std::showbase<<this<<endl;}virtual void F3(){this->z = 10;cout<<"构造函数C传入的this指针得值是:"<<a<<endl;cout<<"this指针得值是:"<<std::hex<<std::showbase<<this<<endl;}};class D : public B, public A{public:int a;public:D(){cout<<"构造函数D传入的this指针得值是:"<<std::hex<<std::showbase<<this<<endl;}virtual void F3(){this->z = 1;//分析继承的函数关系,确定他的位置a = (int)this;}void F4(){this->z = 1;a = (int)this;}};void diaplay(A *a){a->F1();}typedef void(C::*pFun)(void); int main(int argc, char** argv){//A* pc = new C();// A* pd = new D();A* pc = new C(); C* pc1 = new C();A* pd = new D(); D* pd1 = new D();// diaplay(pc);// diaplay(pd);// pc->F1();// pd->F1(); pd1->F3();//先切换为pd1中基类B的位置 pd1->F4();cin>>argc;return 0;}简单无理的测试code:

类A具有一个F1与F3虚函数,其中F3为纯虚函数

类B具有一个F2虚函数

其中类C的继承顺序为A、B,而类D的继承顺序为B、A。

则类C与D的对象在内存中的虚表指针存储位置也是前后相反的,如下图。

疑问与总结:

1. 如何在基类中调用派生类的成员函数

可以通过虚函数的方法来实现,纯虚函数最好。在基类的成员函数中调用派生类,进而让派生类的实现函数去执行。

本质上需要说明的是:这里是虚表指针来做调度,当前this只能访问到基类自己的成员变量、函数以及一张虚表,虚表中的函数通过基类中声明的位置就可以被编译器定位到。故无论虚函数位置在虚表哪里,这个是可以在编译阶段确定的,为静态编译。对于具体的虚表,那就是随着this的建立而动态运行的,即C++多态的本质,延迟执行。本质上还是静态编译,只是通过this来访问虚表,在虚表中偏移位置后确定函数入口。

这里要说明的是虚表中call时存储的不一定是函数执行的直接入口,而是一些列函数导出符号表的入口地址,内部通过jump到实际的函数执行入口,这其中都是由编译器来决定的,个人猜测是通过符号表可以更快速的链接到外部模块的函数,先通过符号表名来绑定函数入口后,后续再添加可执行文件中的函数入口地址到符号表中完成初始化。

2. 基类如果存在基类虚表,则所在的this值应当就是虚表所在的位置,即每个基类对象的0地址位置。

3.C++下多继承下的内存布局

每一个基类都需要占据一个内存区域,对含有虚函数的基类,对象内存的堆中需要含有一个基类虚表指针,其中虚表中的函数根据类继承的覆盖关系会被派生类覆盖。

4. this指针在多继承中的灵活使用,基类与派生类函数调用时实际的this指针是不一样的

首先编译器在生成对象时,依次调用基类与派生类的构造函数,如上述C类与D类中,在基类A与B对象的构建过程中传入的this指针值和派生类的对象是不一致的,这里面编译器做了处理。对于C类来说,如果其this是0xxxx00,则A类构造函数传入的也是0xxxx00,,因为两者在内存分布上空间位置是一样的。反之对D类来说,如果其this是0xxxx00,传入到A类构造函数的this值则是0xxxx08,偏移的量刚好是B类所占的虚表指针与成员变量的大小。

所以每次在编译基类函数时,编译器都是将基类内存对象所在的地址压入到栈帧中,作为当前函数中的this值,其根本目的是当前基类中this值一旦确定后,编译器就可以根据虚表位置调用虚表函数(无论是否被派生类覆盖)以及所以的成员变量。

5. 编译器如何处理派生类中无继承的虚函数以及继承的虚函数或纯虚函数

类似上述D类中的F3与F4,通过汇编代码可以知道,在F3中的this还是是0xxxx08,而F4中确是0xxxx00.本质目的是因为编译器知道了F3是继承了基类A的,而基类A的虚表位置是在0xxxx08的,故传入的this指针需要调整派生类对象的this值。

而对于F4而言,实际传入的this值就是0xxxx00.

对于上述两个一样的代码逻辑,编译器在处理是确是不一样的:

void F4(){00CE20F0 pushebp 00CE20F1 movebp,esp 00CE20F3 subesp,0CCh 00CE20F9 pushebx 00CE20FA pushesi 00CE20FB pushedi 00CE20FC pushecx 00CE20FD leaedi,[ebp-0CCh] 00CE2103 movecx,33h 00CE2108 moveax,0CCCCCCCCh 00CE210D rep stos dword ptr es:[edi] 00CE210F popecx 00CE2110 movdword ptr [ebp-8],ecxthis->z = 1;00CE2113 moveax,dword ptr [this] 00CE2116 movdword ptr [eax+4],1a = (int)this;00CE211D moveax,dword ptr [this] 00CE2120 movecx,dword ptr [this] 00CE2123 movdword ptr [eax+14h],ecx }

在给基类B的成员变量赋值是,F4采用的是取当前派生类的this值到eax, eax+4为z所在的位置,即传入的是派生类对象的this值。

每个人心中,都会有一个古镇情怀,流水江南,烟笼人家。

记录几个C++多继承中,this指针与多虚表间编译与处理的疑问,看

相关文章:

你感兴趣的文章:

标签云: