动态多态性原理及含虚函数的类的对象内存布局剖析
什么是动态多态性?
静态多态性通过函数重载实现,即在编译阶段即确定了函数的调用地址。动态多态性又称为运行时多态,是指在程序运行时才确定这次调用哪个类的成员函数。
动态多态性的实现原理?
基类通过将自己的成员函数声明为虚函数,构造一个虚函数表(函数指针数组),所有被声明为虚函数的成员函数的地址都将被存储在虚函数表中,编译器给每一个含有虚函数的类的对象一个指针成员变量用来存储该类对应的虚函数表的地址。当使用基类的指针或引用调用方法时,调用哪个方法不再取决于指针或引用的类型(编译时便能确定),而是指针或引用所绑定对象的类型(运行时才能确定),通过访问该对象的虚函数表指针vfptr,获取被绑定对象的虚函数的地址,从而实现运行时的多态。
含虚函数的类的对象内存分布?
对于含有虚函数的基类,其对象中的内存分布为: 虚函数表指针+ 成员变量1 + 成员变量2 + … +成员变量n ,成员变量在内存中的排列顺序与该变量在类中的定义顺序一致。
对于含有虚函数的派生类,其虚函数表指针变量的数量等于其派生类的数量,指针顺序也与继承顺序对应。内存分布为:虚函数表指针1 + 继承自基类1的成员变量 … + 虚函数表指针2 + 继承自基类2的成员变量 … + 派生类自定义的成员变量 … 。
虚函数表(虚函数指针数组)中虚函数地址排列顺序?
对于多重继承的派生类,其第一个虚函数表中内存分布为: 继承自第一个基类的虚函数 …(按照在基类中的定义顺序排列)+ 自己定义的虚函数… 。剩下的虚函数表则只存储继承自对应基类的虚函数。
注意:重写基类继承而来的虚函数时,继承而来的虚函数在派生类中定义的顺序不会影响其在虚函数表中顺序。
以下是我写的几个类的测试代码:
class Father {
public:
char c = '!';
public:
virtual void func1()
{
cout << "Base::func1" << endl;
}
virtual void func2(char c)
{
cout << "Base::func2 -->" << c <<endl;
}
virtual void func3(int argv)
{
cout << "Base::func3 -->" << argv << endl;
}
};
class Mother {
public:
string s = "字符串";
public:
virtual void funca()
{
cout << "Mother::funca" << endl;
}
virtual void funcb()
{
cout << "Mother::funcb" << endl;
}
};
class Son :public Father,public Mother {
public:
int i = 12;
public:
void func1()override
{
cout << "- Son::func1" << endl;
}
void func2(char c)override
{
cout << "- Son::func2 ==>" << c << endl;
}
void funca()override
{
cout << "Son::funca" << endl;
}
void funcb()override
{
cout << "Son::funcb" << endl;
}
virtual void func4()
{
cout << "非继承的 - Son::func4" << endl;
}
};
以下是派生类Son的对象及其虚函数表的内存布局图示:
vfptr_arr1是派生类第一个虚函数指针数组,vfptr_F存放着它的首地址;
vfptr_arr2是派生类第二个虚函数指针数组, vfptr_M存放着它的首地址。
定义几个虚函数的指针的类型别名:
using func1_t = void (*)(); //与继承自Mother类虚函数funca和funcb类型相同。
typedef void(__stdcall* func2_t)(char);
using func3_t = void (__stdcall*)(int);
下面演示如何利用对象和虚函数表的内存布局任意调用虚函数:
int main()
{
Son d1; //定义一个派生类对象。
/*将派生类对象地址强制转换为int*类型指针,(因为vfptr_F占4字节)这样该
指针就指向了d1对象中的虚函数表指针成员vfptr_F,然后解引用即取得vfptr_F
的值(也就是虚函数指针数组或虚函数表的首地址),此时该地址值是一个int型
数据。又因为虚函数指针数组中的元素是指针(函数指针),每个元素占4字节(
64位系统下),与int类型大小一致,故可将该地址值强制转换为int*类型(目的是
进行指针的算数运算时,指针每移动一个单位都是4个字节,即刚好指向数组中的
下一个元素的首地址)并赋值给vfptr_F,令vfptr_F指向虚函数表首地址。*/
int* vfptr_F = (int*)*(int*)&d1;
//通过vfptr_F指针调用派生类Son的第一个虚函数表中的虚函数(vfptr_F指针
//指向虚函数表中的元素,解引用该指针即取得对应元素的值(即虚函数地址)):
((func1_t) * (vfptr_F + 0))(); //调用func1
((func2_t) * (vfptr_F + 1))('#'); //调用func2
((func3_t) * (vfptr_F + 2))(199); //调用func3
((func1_t) * (vfptr_F + 3))(); //调用func4
//通过vfptr_M指针调用派生类Son的第二个虚函数表中的函数:
//((int)&d1 + 4 * 2)的意思是取得d1的地址并强制转换为int型数据后加上8
//个字节,此时该地址是d1中第二个虚函数表指针的地址,后面的操作与上文类似。
int* vfptr_M = (int*)*(int*)((int)&d1 + 4 * 2);
((func1_t) * (vfptr_M + 0))(); //调用funca
((func1_t) * (vfptr_M + 1))(); //调用funcb
return 0;
}
以下是上面代码的运行效果图:
下面,我们用C++代码来模拟编译器在实现动态多态性时的工作过程:
1.首先定义一些函数模拟派生类Son中包括继承自基类和自定义的全部虚函数:
//第一个虚函数表中的成员对应的虚函数。
void func1()
{
cout << "重写Father类 - Son::func1" << endl;
}
void func2(char c)
{
cout << "重写Father类 - Son::func2 ==>" << c << endl;
}
void func3(int argv)
{
cout << "Base::func3 -->" << argv << endl;
}
void func4()
{
cout << "非继承的 - Son::func4" << endl;
}
//第二个虚函数表中的成员对应的虚函数。
void funca()
{
cout << "重写Mother类 - Son::funca" << endl;
}
void funcb()
{
cout << "重写Mother类 - Son::funcb" << endl;
}
2.定义这些函数的函数指针的类型别名,增强代码可读性:
(注意此处代码与上面给类的成员函数的函数指针定义类型别名的细节区别,少了_stdcall关键字)。
using func1_t = void (*)(); //与继承自Mother类虚函数funca和funcb类型相同。
typedef void (* func2_t)(char);
using func3_t = void (*)(int);
3.生成两个虚函数表(虚函数指针数组):
void* vfptr1 = func1;
void* vfptr2 = func2;
void* vfptr3 = func3;
void* vfptr4 = func4;
//第一个虚函数表(虚函数指针数组):
void* vfptr_arr1[] = { vfptr1 ,vfptr2 ,vfptr3 ,vfptr4 };
//------------------------------------------------------------------------
void* vfptra = funca;
void* vfptrb = funcb;
//第二个虚函数表(虚函数指针数组):
void* vfptr_arr2[] = { vfptra, vfptrb };
4.定义两个二级指针分别模拟派生类Son中的两个虚函数表指针,利用这两个虚函数表指针调用虚函数:
void** vfptr_F = vfptr_arr1;
((func1_t) * (vfptr_F + 0))(); //调用func1
((func2_t) * (vfptr_F + 1))('#'); //调用func2
((func3_t) * (vfptr_F + 2))(199); //调用func3
((func1_t) * (vfptr_F + 3))(); //调用func4
void** vfptr_M = vfptr_arr2;
((func1_t) * (vfptr_M + 0))(); //调用funca
((func1_t) * (vfptr_M + 1))(); //调用funcb
以下是模拟代码的运行效果,可以看到,与上面利用派生类地址布局调用虚函数的效果一模一样: