记录几个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  push        ebp  
00CE20F1  mov         ebp,esp  
00CE20F3  sub         esp,0CCh  
00CE20F9  push        ebx  
00CE20FA  push        esi  
00CE20FB  push        edi  
00CE20FC  push        ecx  
00CE20FD  lea         edi,[ebp-0CCh]  
00CE2103  mov         ecx,33h  
00CE2108  mov         eax,0CCCCCCCCh  
00CE210D  rep stos    dword ptr es:[edi]  
00CE210F  pop         ecx  
00CE2110  mov         dword ptr [ebp-8],ecx  
		this->z = 1;
00CE2113  mov         eax,dword ptr [this]  
00CE2116  mov         dword ptr [eax+4],1  
       a = (int)this;
00CE211D  mov         eax,dword ptr [this]  
00CE2120  mov         ecx,dword ptr [this]  
00CE2123  mov         dword ptr [eax+14h],ecx  
	}

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


   virtual void F3()
	{
00CE20A0  push        ebp  
00CE20A1  mov         ebp,esp  
00CE20A3  sub         esp,0CCh  
00CE20A9  push        ebx  
00CE20AA  push        esi  
00CE20AB  push        edi  
00CE20AC  push        ecx  
00CE20AD  lea         edi,[ebp-0CCh]  
00CE20B3  mov         ecx,33h  
00CE20B8  mov         eax,0CCCCCCCCh  
00CE20BD  rep stos    dword ptr es:[edi]  
00CE20BF  pop         ecx  
00CE20C0  mov         dword ptr [ebp-8],ecx  //ebp-8为this指针所在位置
		this->z = 1;//分析继承的函数关系,确定他的位置
00CE20C3  mov         eax,dword ptr [this]  
00CE20C6  mov         dword ptr [eax-4],1  
       a = (int)this;
00CE20CD  mov         eax,dword ptr [this]  
00CE20D0  sub         eax,8  
00CE20D3  mov         ecx,dword ptr [this]  
00CE20D6  mov         dword ptr [ecx+0Ch],eax  
	}
在给基类B的成员变量赋值是,F3采用的是取当前基类所在的this值到eax, eax-4为z所在的位置,即很明确this值应当是基类A虚表所在的位置。

两者的区别就在于前者F4是无继承形的虚函数,后者是继承形的虚函数,编译器处理的方式是不一样的,继承性的处理需要以基类为服从方式,因为基类是派生的基础。


6 编译自主识别继承关系,如F3在基类中的调用。

传入到F3中的this指针值还是基类指针,但当内部需要操作读写this的值时,编译器会自动加入代码修改this指针为派生类自己的地址。所以无论是基类指针还是派生类指针如果指向一个派生类对象,当两者分别调用同一个继承的虚函数时,则其压入到栈帧中的当前函数this值理论都是指向基类的,这是编译器的行为,函数内部的处理是一致对基类指针而言的(F3函数的代码段只有一份)。故对于一个派生类指针,在执行F3时,还是要经历虚表的调用,在调用前会先将this值转为基类所在的地址,作为参数传入到栈帧之中,确保在F3函数内部都是以基类指针为基础来处理

pd1->F3()
00F417D3  mov         ecx,dword ptr [ebp-20h]  
00F417D6  add         ecx,8  //切换pd1的this指针需要+8偏移到基类指针,ecx保存着this指针
00F417D9  mov         eax,dword ptr [ebp-20h]  
00F417DC  mov         edx,dword ptr [eax+8]  
00F417DF  mov         esi,esp  
00F417E1  mov         eax,dword ptr [edx+4]  
00F417E4  call        eax  
00F417E6  cmp         esi,esp  
00F417E8  call        @ILT+485(__RTC_CheckEsp) (0F411EAh)  
pd->F3();
00F417ED  mov         eax,dword ptr [ebp-14h]  
00F417F0  mov         edx,dword ptr [eax]  
00F417F2  mov         esi,esp  
00F417F4  mov         ecx,dword ptr [ebp-14h]  
00F417F7  mov         eax,dword ptr [edx+4]  
00F417FA  call        eax  
00F417FC  cmp         esi,esp  
00F417FE  call        @ILT+485(__RTC_CheckEsp) (0F411EAh)  


7 不同根据类型的继承与派生的关系,this指针在传递过程中,本质是一个动态变化的过程,但要明确的是每个对象就一个this指针,在函数间传递的是this以一个输入参数的形式在栈帧中传入。


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值