我们知道, 高级的源代码首先要被编译成汇编码,然后汇编码被汇编器编译成机器码。
我们转成汇编看看
是编译阶段就已经确定好的函数调用,生成指令了,指定哪个作用域哪个名字的哪个函数了。
编译器看到调用方法的pb指针是基类类型的,它就去基类类型里面去查看这个方法,直接进行调用,这就是静态绑定。
#include
#include
using namespace std;
class Base
{
public:
Base(int data = 10) :ma(data) {}
void show() { cout << "Base::show()" << endl; }//#1
void show(int) { cout << "Base::show(int)" << endl; }//#2
protected:
int ma;
};
class Derive : public Base
{
public:
Derive(int data = 20) :Base(data), mb(data) {}
void show() { cout << "Derive::show()" << endl; }//#3
private:
int mb;
};
int main()
{
Derive d(50);
Base* pb = &d;
pb->show(); //静态(编译时期)的绑定(函数调用) Call Base::show (0EA1037h)
pb->show(10); //静态绑定 Call Base::show (0EA12F3h)
cout << sizeof(Base) << endl; //4
cout << sizeof(Derive) << endl; //8
cout << typeid(pb).name() << endl; //class Base*
cout << typeid(*pb).name() << endl; //class Base
/*
Base::show()
Base::show(int)
4
8
class Base *
class Base
*/
return 0;
}
我们在基类Base中的两个show方法的最前面加上virtual关键字
这2个show函数就变成虚函数了
一个类添加了虚函数,对这个类有什么影响?
总结一:
一个类里面定义了虚函数,那么在编译阶段,编译器给这个类类型产生一个唯一的vftable虚函数表,虚函数表中主要存储的内容就是RTTI指针和虚函数的地址。当程序运行时,每一张虚函数表都会加载到内存的.rodata区(这张表是只能读而不能改的)。
RTTI指针指向一个类型字符串(这张表是哪个类型产生的,这个字符串就是哪个类型的),这张表是编译Base类产生的,所以这个RTTI指向的是一个Base类型的字符串。
另外,虚函数表还存储一个整数(偏移量),偏移量在大部分情况下等于0(0表示vfptr在对象内存中的偏移量,因为vfptr的排列优先级非常高,在没有虚继承的情况下,vfptr永远在对象的前4个字节放着,所以就是在对象的起始部分,就是0)。
虚函数表还存储一个整数(地址),类里有2个虚函数,所以在这里存了这2个虚函数的地址。
虚函数表结构如下:(一个类型产生一个虚函数表)
在编译器终端,先看Derive类:
再看Base类:
上面我们的虚函数表中的虚函数排列有问题:
运行的时候,定义了b1这个Base类型的对象。
b1对象的内存大小不仅仅是4个字节了。
不仅存储一个ma,还要存储一个vfptr虚函数指针(指向Base类型的虚函数表)
现在基类定义的对象占8个字节大小了。
我们再定义一个b2的Base类型对象,内存也是8字节哦,但是它们两个都是Base类型,所以它们两个的虚函数指针vfptr都是指向Base类型的虚函数表,指向的是同一个虚函数表。
总结二:
总结三:
接下来我们来看看派生类有什么变化?
总结四:
如果派生类中的方法,和基类继承来的某个方法,返回值、函数名、参数列表都相同,而且基类的方法是virtual虚函数,那么派生类的这个方法,自动处理成虚函数。
那么这两个函数的关系就是覆盖的关系!
图中的最上面的那个show方法和最下面的那个show方法是覆盖的关系。
派生类的show方法被自动处理成虚函数了。
我们定义2个派生类对象d1和d2
现在派生类自己也有虚函数了,从记录Base也继承过来2个虚函数;
在编译阶段
我们会个Derive这个类型产生虚函数表,因为从基类继承来了虚函数。
虚函数表先放的是RTTI指针,RTTI指向的是类型字符串(Derive类型)。
然后存储0
然后存储虚函数的地址。
因为从基类继承来2个虚函数,所以记录的是继承来的这2个虚函数的地址
但是编译器转眼一看,这个派生类里面对这个基类的show方法进行了覆盖了,或者叫做重写了,重写了这个同名的覆盖方法。
编译器就知道,不要基类的那个show方法了,因为派生类把这个不带参数的show方法重写了,所以在派生类的虚函数表里面,就要放派生类重写的show方法的虚函数地址了,这就是覆盖关系。
覆盖: 虚函数表中的虚函数地址的覆盖!
但是派生类并没有对基类的带int参数的show虚函数进行重写,所以派生类的虚函数表还是存储的是基类的带int参数的show虚函数地址。
派生类对象的内存结构:新增一个虚函数指针,指向当前类型的虚函数表。
覆盖:
编译阶段,编译器看pb是基类Base类型,然后就会跑到基类的作用域里面看这个不带参数的show是什么情况,如果发现这个show是普通函数,就进行静态绑定,直接生成call:Base::show,也就是说,经过编译就知道,这里无论如何调用的都是基类Base下的show方法!
但是,如果在编译阶段,编译器发现pb是Base类型的指针, 然后它跑去Base的作用域下去查看show,发现这个show是虚函数,如果发现show是虚函数,就进行动态绑定了:
把pb指向的派生类对象的前4个字节(虚函数指针,即虚函数的地址)放到eax寄存器,再将eax内容放到edx中, edx放的是虚函数的地址;再通过edx+4找到show这个函数在虚函数表中的位置,call eax调用函数
call eax我们不知道最终调用的是哪个函数,寄存器放的是最终从虚函数表中取出来的虚函数的地址。
但是到底是哪个虚函数的地址,只有在运行的时候,通过指令在虚函数表里面找到谁的地址,调用的就是哪个函数。
在编译阶段生成的指令中,无法判断最终调用的是哪个函数,我们称作动态绑定
样的这句代码,编译器在编译的时候看到这个指针pb是Base类型的,跑去Base类型的作用域下查看,发现这个带int类型的show方法是虚函数,就得进行动态绑定了。和刚才叙述的情况一样。
虚函数的定义的顺序和在虚函数的表的位置有关。先定义的放在虚函数表的前面。
cout << sizeof(Base) << endl;//8
cout << sizeof(Derive) << endl;//12
cout << typeid(pb).name() << endl;//class Base*
/*
pb的类型:编译器查看是Base -> 然后看它有没有虚函数
如果Base没有虚函数,*pb识别的就是编译时期的类型 *pb <=> Base类型
如果Base有虚函数,*pb识别的就是运行时期的类型 --》就是RTTI类型 在虚函数表中存着
通过pb->d(vfptr)->Derive vftable 因为指向的是派生类对象,
所以最终访问的就是class Derive 虚函数表 访问的就是派生类类型
*/
cout << typeid(*pb).name() << endl;//class Derive
我们在回答时,可以从指令上说: