目录
一. 多态的原理
1.1 虚函数表
1.2 多态的实现原理
1.3 动态绑定与静态绑定
二. 多继承中的虚函数表
2.1 虚函数表的打印
2.2 多继承中虚函数表中的内容存储情况
对于一个含有虚函数的的类,在实例化出来对象以后,对象所存储的内容包含两部分:
下段代码定义了一个Base类,其中包含虚函数func1以及一个int型数据,在main函数中,使用sizeof(Base)计算这个类实例化出来的对象大小为8bytes而不是4bytes,这正是因为虚函数表指针占了4bytes的存储空间(32位编译环境)。
class Base
{
public:
virtual void func() { std::cout << "Base::func()" << std::endl; }
int _b = 1;
};
int main()
{
Base b;
std::cout << sizeof(b) << std::endl; //8
return 0;
}
如果要调用Base中定义的虚函数func,那么程序会在运行时根据虚函数指针找到虚函数表,虚函数表中存有函数指针(函数所在地址),程序会根据虚函数表中存储的虚函数所在地址,找到对应的函数进行调用。
多态的实现,是通过虚函数的重写来实现的。对于一个包含虚函数的基类Base,设有一派生类Derive继承了基类Base,那么Derive会将Base的虚函数表一并继承下来。
演示代码1.2中定义了一个基类Base和一个派生类Derive,Base中定义了两个虚函数func1和func2,在派生类Derive中,func2被重写了,func1没有被重写。运行代码,打开内存监视窗口,可以看到,在Derive对象的虚函数表中,func2的地址和Base对象中的不一样,而func1的地址一样,这证明了func2被重写后,其记录在虚函数表中的地址被覆盖了。
演示代码1.2:
#include
class Base
{
public:
virtual void func1()
{
std::cout << "Base::func1()" << std::endl;
}
virtual void func2()
{
std::cout << "Base::func2()" << std::endl;
}
};
class Derive : public Base
{
public:
virtual void func2()
{
std::cout << "Derive::func2()" << std::endl;
}
int _d = 1;
};
int main()
{
Base b;
Derive d;
return 0;
}
我们知道,多态的条件之一,就是通过父类的指针或引用去调用,可以从这个调用条件为切入点,分析多态中函数调用的流程,来探索多态的底层原理,多态中函数调用流程为:
正是由于子类对象中完成了对父类对象虚函数的重写,所以在子类对象完成虚函数操作时,会执行子类中定义的虚函数。多态中的虚函数调用,是通过获取虚函数表中的函数指针来确定具体调用哪个函数的,由于父类对象和子类对象的虚函数表中存储不同的虚函数指针,所以会调用不同的虚函数,从而实现了多态。
关于多态中虚函数表的生成和覆盖,总结出以下几点关键内容:
为了探索多继承中虚函数的行为,我们需要定义一个PrintVFTalbe函数,来打印虚函数所存储的地址,并通过虚函数表中存储的函数指针调用对应的函数,来观察虚函数表中的函数指针与子类和父类虚函数的指向关系。
因为VS编译器会在虚函数表末尾位置存储nullptr,所以使用Table[i] != nullptr作为循环结束的判断条件(Linux gcc编译器不会将在虚函数表最后放nullptr,必须显示地给定虚函数表中存储的函数指针的个数)。在每层循环内部,先打印函数指针(函数首条指令地址),然后将函数指针变量赋值给ptr,通过函数指针调用函数。
演示代码2.1:(虚函数表打印函数)
typedef void (*VFPTR)(); //将指向无参数、返回void的函数的函数指针类型重定义为VFPTR
void PrintfVFTable(VFPTR* table)
{
for (size_t i = 0; table[i] != nullptr; ++i)
{
printf("第%d个虚函数的地址:%p -> ", i, table[i]);
VFPTR ptr = table[i]; //获取函数指针
ptr(); //通过函数指针调用函数
}
std::cout << std::endl;
}
编写演示代码2.2,其中定义了两个父类Base1和Base2,两个父类中都定义了func1和func2虚函数,并且,在子类Derive中,重写func1函数,并且定义了一个新的虚函数func3。
调试代码,打开监视窗口,我们可以发现,子类对象中包含的亮哥父类对象各有一张虚函数表,但是,VS的监视窗口并没有显示出虚函数func3的地址,这并不是说func3的地址没有进虚函数表,而是VS编译器没有将其显示出来,可以认为这是编译器的一个小BUG。
演示代码2.2:
class Base1
{
public:
virtual void func1() { std::cout << "Base1::func1" << std::endl; }
virtual void func2() { std::cout << "Base1::func2" << std::endl; }
private:
int _b1 = 1;
};
class Base2
{
public:
virtual void func1() { std::cout << "Base2::func1" << std::endl; }
virtual void func2() { std::cout << "Base2::func2" << std::endl; }
private:
int _b2 = 2;
};
class Derive : public Base1, public Base2
{
public:
virtual void func1() { std::cout << "Derive::func1" << std::endl; }
virtual void func3() { std::cout << "Derive::func3" << std::endl; }
private:
int _d1 = 3;
};
int main()
{
Derive d;
Base1* ptr1 = &d;
Base2* ptr2 = &d;
PrintfVFTable((VFPTR*)*(int*)ptr1); //打印Base1的虚函数表
PrintfVFTable((VFPTR*)*(int*)ptr2); //打印Base2的虚函数表
return 0;
}
由于VS编译器的这个小“bug”,就要求我们显示的打印虚函数表,将虚函数指针作为参数,传给虚函数表打印函数。可见。Base1的虚函数表中存储了三个韩式指针,从前到后依次为:子类定义的func1、Base1中定义的func2、func3,Base2的虚表中存储了3个函数指针,从前到后依次为:子类中定义的func1、Base2的func2。
根据图2.2所示的虚表打印情况,总结出多继承体系中如下的规律: