讨论静态绑定与动态绑定,首先须要理解的是绑定,何为绑定?函数调用与函数自己的关联,以及成员访问与变量内存地址间的关系,称为绑定。 理解了绑定后再理解静态与动态。ios
在C++中动态绑定是经过虚函数实现的,是多态实现的具体形式。而虚函数是经过虚函数表实现的。这个表中记录了虚函数的地址,解决继承、覆盖的问题,保证动态绑定时可以根据对象的实际类型调用正确的函数。这个虚函数表在什么地方呢?C++标准规格说明书中说到,编译器必需要保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证正确取到虚函数的偏移量)。也就是说,咱们能够经过对象实例的地址获得这张虚函数表,而后能够遍历其中的函数指针,并调用相应的函数。c++
要想弄明白动态绑定,就必须弄懂虚函数的工做原理。C++中虚函数的实现通常是经过虚函数表实现的(C++规范中没有规定具体用哪一种方法,但大部分的编译器厂商都选择此方法)。类的虚函数表是一块连续的内存,每一个内存单元中记录一个JMP指令的地址。编译器会为每一个有虚函数的类建立一个虚函数表,该虚函数表将被该类的全部对象共享。 类的每一个虚成员占据虚函数表中的一行。若是类中有N个虚函数,那么其虚函数表将有N*4字节的大小。后端
虚函数(virtual)是经过虚函数表来实现的,在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其真实反映实际的函数。这样,在有虚函数的类的实例中分配了指向这个表的指针的内存(位于对象实例的最前面),因此,当用父类的指针来操做一个子类的时候,这张虚函数表就显得尤其重要,指明了实际所应调用的函数。它是如何指明的呢?后面会讲到。bash
JMP指令是汇编语言中的无条件跳转指令,无条件跳转指令可转到内存中任何程序段。转移地址可在指令中给出,也能够在寄存器中给出,或在储存器中指出。微信
首先咱们定义一个带有虚函数的基类函数
class Base {
public:
virtual void fun1(){
cout<<"base fun1!\n";
}
virtual void fun2(){
cout<<"base fun2!\n";
}
virtual void fun3(){
cout<<"base fun3!\n";
}
int a;
};
复制代码
查看其内存布局 布局
既然虚函数表指针一般放在对象实例的最前面的位置,那么咱们应该能够经过代码来访问虚函数表,经过下面这段代码加深对虚函数表的理解:区块链
#include "stdafx.h"
#include<iostream>
using namespace std;
class Base {
public:
virtual void fun1(){
cout<<"base fun1!\n";
}
virtual void fun2(){
cout<<"base fun2!\n";
}
virtual void fun3(){
cout<<"base fun3!\n";
}
int a;
};
int _tmain(int argc, _TCHAR* argv[])
{
typedef void(*pFunc)(void);
Base b;
cout<<"虚函数表指针地址:"<<(int*)(&b)<<endl;
//对象最前面是指向虚函数表的指针,虚函数表中存放的是虚函数的地址
pFunc pfun;
pfun=(pFunc)*((int*)(*(int*)(&b))); //这里存放的都是地址,因此才一层又一层的指针
pfun();
pfun=(pFunc)*((int*)(*(int*)(&b))+1);
pfun();
pfun=(pFunc)*((int*)(*(int*)(&b))+2);
pfun();
system("pause");
return 0;
}
复制代码
运行结果: ui
经过这个例子,对虚函数表指针,虚函数表这些有了足够的理解。下面再深刻一些。C++又是如何利用基类指针和虚函数来实现多态的呢?这里,咱们就须要弄明白在继承环境下虚函数表是如何工做的。目前只理解单继承,至于虚继承,多重继承待之后再理解。 单继承代码以下:this
class Base {
public:
virtual void fun1(){
cout<<"base fun1!\n";
}
virtual void fun2(){
cout<<"base fun2!\n";
}
virtual void fun3(){
cout<<"base fun3!\n";
}
int a;
};
class Child:public Base
{
public:
void fun1(){
cout<<"Child fun1\n";
}
void fun2(){
cout<<"Child fun2\n";
}
virtual void fun4(){
cout<<"Child fun4\n";
}
};
复制代码
内存布局对比:
另外,咱们注意到,类Child和类Base中都只有一个vfptr指针,前面咱们说过,该指针指向虚函数表,咱们分别输出类Child和类Base的vfptr:
int _tmain(int argc, _TCHAR* argv[])
{
typedef void(*pFunc)(void);
Base b;
Child c;
cout<<"Base类的虚函数表指针地址:"<<(int*)(&b)<<endl;
cout<<"Child类的虚函数表指针地址:"<<(int*)(&c)<<endl;
system("pause");
return 0;
}
复制代码
运行结果:
能够看到,类Child和类Base分别拥有本身的虚函数表指针vfptr和虚函数表vftable。
下面这段代码,说明了父类和基类拥有不一样的虚函数表,同一个类拥有相同的虚函数表,同一个类的不一样对象的地址(存放虚函数表指针的地址)不一样。
int _tmain(int argc, _TCHAR* argv[])
{
Base b;
Child c1,c2;
cout<<"Base类的虚函数表的地址:"<<(int*)(*(int*)(&b))<<endl;
cout<<"Child类c1的虚函数表的地址:"<<(int*)(*(int*)(&c1))<<endl; //虚函数表指针指向的地址值
cout<<"Child类c2的虚函数表的地址:"<<(int*)(*(int*)(&c2))<<endl;
system("pause");
return 0;
}
复制代码
运行结果:
在定义该派生类对象时,先调用其基类的构造函数,而后再初始化vfptr,最后再调用派生类的构造函数( 从二进制的视野来看,所谓基类子类是一个大结构体,其中this指针开头的四个字节存放虚函数表头指针。执行子类的构造函数的时候,首先调用基类构造函数,this指针做为参数,在基类构造函数中填入基类的vfptr,而后回到子类的构造函数,填入子类的vfptr,覆盖基类填入的vfptr。如此以来完成vfptr的初始化)。也就是说,vfptr指向vftable发生在构造函数期间完成的。
动态绑定例子:
#include "stdafx.h"
#include<iostream>
using namespace std;
class Base {
public:
virtual void fun1(){
cout<<"base fun1!\n";
}
virtual void fun2(){
cout<<"base fun2!\n";
}
virtual void fun3(){
cout<<"base fun3!\n";
}
int a;
};
class Child:public Base
{
public:
void fun1(){
cout<<"Child fun1\n";
}
void fun2(){
cout<<"Child fun2\n";
}
virtual void fun4(){
cout<<"Child fun4\n";
}
};
int _tmain(int argc, _TCHAR* argv[])
{
Base* p=new Child;
p->fun1();
p->fun2();
p->fun3();
system("pause");
return 0;
}
复制代码
运行结果:
其实,在new Child时构造了一个子类的对象,子类对象按上面所讲,在构造函数期间完成虚函数表指针vfptr指向Child类的虚函数表,将这个对象的地址赋值给了Base类型的指针p,当调用p->fun1()时,发现是虚函数,调用虚函数指针查找虚函数表中对应虚函数的地址,这里就是&Child::fun1。调用p->fun2()状况相同。调用p->fun3()时,子类并无重写父类虚函数,但依旧经过调用虚函数指针查找虚函数表,发现对应函数地址是&Base::fun3。因此上面的运行结果如上图所示。
到这里,你是否已经明白为何指向子类实例的基类指针能够调用子类(虚)函数?每个实例对象中都存在一个vfptr指针,编译器会先取出vfptr的值,这个值就是虚函数表vftable的地址,再根据这个值来到vftable中调用目标函数。因此,只要vfptr不一样,指向的虚函数表vftable就不一样,而不一样的虚函数表中存放着对应类的虚函数地址,这样就实现了多态的”效果“。
关注微信公众号,推送后端开发、区块链等技术分享!
![]()