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

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

2015-10-19 14:58 423 查看
简单无理的的测试代码:
#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以一个输入参数的形式在栈帧中传入。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: