c++多态的原理 以及虚函数表详解
c++中多态的原理
要实现多态,必然在背后有自己的实现机制,如果不了解其背后的机制,就很难对其有更深的理解。
一个多态的例子
class Person{
public:
virtual void Drink() {
cout << "drink water" << endl;
};
};
class Children : public Person{
public:
virtual void Drink() {
cout << "drinking juice " << endl;
}
};
class Parents : public Person{
public:
virtual void Drink() override{
cout << "drinking wine" << endl;
};
};
int main()
{
Person per;
Person* per1 = new Parents;
Person* per2 = new Children;
per1->Drink(); //drinking wine
per2->Drink();//drinking juice
return 0;
}
通过调试我们以看到在一个包含虚函数的Person类的对象per中有着一个隐藏的成员_vfptr虚函数指针,通过计算Person类的大小,我们发现该类的大小为4 ,说明,该指针是存在的。
我们所看到的指针为虚函数表指针,指向一个虚函数。一个含有虚函数的表中都含有一个虚函表,简称虚表,存放的是该类中虚函数的地址。
以上我们所看到的是基类对象,现在我们来看派生类中的对象的虚表
我们在基类Person中增加两个函数,一个虚函数fun1,一个普通函数fun2.
class Person{
public:
virtual void Drink() {
cout << "drink water" << endl;
};
virtual void fun1() {
cout << "fun1" << endl;
}
void fun2() {
cout << "fun2" << endl;
}
};
class Children : public Person{
public:
virtual void Drink() {
cout << "drinking juice " << endl;
}
};
class Parents : public Person{
public:
virtual void Drink() override{
cout << "drinking wine" << endl;
};
virtual void fun3() {
cout << "fun3" << endl;
}
};
typedef void(*Fun) ();
void PrintVtable(Fun Vtable[]){
for (int i = 0; Vtable[i] != nullptr; i++)
{
printf("虚函数 %d : 0X%x " ,i, Vtable[i]);
Fun f = Vtable[i];
f();
}
}
int main()
{
Parents pare;
PrintVtable((Fun*)*(int*)&pare); //虚函数表指针存放在对象的前4个字节,将其转为int*,解引用,因为其中存放的为函数指针,所以再将其强转为函数指针
return 0;
}
通过派生类对象的内存模型,可以发现,在派生类的对象中也有了个虚函数表指针,但是在监视窗口中的虚函数表中并没有派生类自己的虚函数的地址。
- 虚函数表指针一般存放在对象的开头的位置
- 虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。
通过虚函数表指针我们可以将虚函数表打印出来看,通过打印出来的虚函数表,可以看出派生类自己的虚函数被放在最后,只不过在监视窗口没有显示出来。
- 派生类中虚表的布局:首先存放的是拷贝自基类的虚表中的虚函数,如果对其中的虚函数进行了重写,就用重写的将原来基类的虚函数进行覆盖,最后再按派生类中虚函数声明的顺序,将派生类的虚函数的地址增加到最后。
实际上的多态就是不同的对象,在调用时查找其虚函数表,找到要调用的函数,因为在派生类的虚函数表中已将完成了重写,所以尽管调用的是同一个函数,但完成的却是不同的动作,一个行为有多种状态。
动态绑定与静态绑定
- 静态绑定:静态绑定又叫早绑定,是在编译期间,编译器已经确定好了函数的调用情况,在编译阶段已经确定好了程序的行为。
函数的重载实际上一种静态的多态。 - 动态绑定:动态绑定并不是在编译阶段确定函数的调用的,而是在运行阶段来确定的。
多态的调用实质上就是动态的绑定,即函数的调用并不是在编译期间确定的,而是在运行期间根据函数的调用情况在相应的对象中查找并进行调用。
通过汇编代码我们可以看到多态的函数调用是在运行时去确定的。
先在调用第二个虚函数fun1
单继承下的虚函数表
- 在单继承的关系下,派生类的虚函数表中先存放的是拷贝自基类的虚函数表,并用重写的函数将基类的相应的函数覆盖。在表的后面,按派生类自己的声明顺序,加入自己的虚函数地址。
- 在vs的编译器下监视的窗口中在虚函数表中无法看到派生类自己的虚函数,可以通过打印虚函数表看到
- 虚函数表存放在对象的前四个字节,以nullptr结尾,相当于一个函数指针数组 (前面打印过单继承关系下的虚函数表)
多继承下的虚函数表
- 在多继承关系下,派生类对象中有多个虚函数指针,指向多个虚函数表,每个基类都有一个,按继承的顺序依次存放虚函数指针。而派生类自己的虚函数存放在第一个虚函数表的后面。
class Mother{
public:
virtual void fun1(){
cout << "Mother :: fun1" << endl;
}
};
class Father{
public:
virtual void fun2() {
cout << "Father :: fun2" << endl;
}
};
class Son : public Mother ,public Father{
public:
virtual void fun1() {
cout << "Son :: fun1 " << endl;
}
virtual void fun2(){
cout << "Son :: fun2 " << endl;
}
virtual void fun3(){
cout << "Son :: fun3 " << endl;
}
};
int main()
{
Son son;
PrintVtable((Fun*)*(int*)&son);
PrintVtable((Fun*)*(int*)((char*)&son + sizeof(Mother)));
return 0;
}
虚函数表存放在哪里
虚函数指针是存放在对象中,所以虚函数指针的位置是跟着对象的位置走的
- 对象在栈上被创建,虚函数指针存在于栈上
- 对象被创建在堆上,虚函数指针就存在于堆上
虚函数表的位置是不确定的所以我们自己手动的测试,通过手动的测试,可以发现通过打印出的虚函数表的地址,与代码区的地址极为相似,所以大致可以确定虚函数表是存放在代码区的。
int a = 10;
int main()
{
Son son;
Son* s1 = new Son;
/*PrintVtable((Fun*)*(int*)&son);
PrintVtable((Fun*)*(int*)((char*)&son + sizeof(Mother)));*/
const int b = 20;
static int c = 30;
char* pch = "hello world";
printf("虚函数表的地址: %x \n", (Fun*)*(int*)&son);
printf("a : %x \n", &a);
printf("b : %x \n", &b);
printf("堆区:s1: %x \n", s1);
printf("栈区:son: %x \n", &son);
printf("静态区: c: %x \n", &c);
printf("代码段: pch : %x \n", pch);
return 0;
}