1. 虚函数表
想要弄懂多态的原理,首先要了解一下虚函数表。
先来做一道笔试题:下面代码的运行结果是多少?
class Base { public: virtual void Func1() { cout << "Func1()" << endl; } private: int _b = 1; }; int main() { Base b; cout << sizeof(Base) << endl; return 0; }
(vs2019下的x86程序)答案是:8字节
我们把上述代码的virtual关键字注释掉得到的答案却是:4字节
这是为什么呢?打开监视窗口看一下b对象:
发现b对象里不仅存在_b,还有一个_vfptr,这个_vfptr是什么东西呢?
其实,多出的一个_vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
为了更好的理解这段话中的概念,在如上代码的基础上,我们再添加一个虚函数Func2和一个正常函数Func3:
class Base { public: virtual void Func1() { cout << "Func1()" << endl; } virtual void Func2() { cout << "Func2()" << endl; } void Func3() { cout << "Func3()" << endl; } private: int _b = 1; };
知道了基类的虚函数表里面存放了什么,下面我们再来探究一下派生类的这个表里面又存放的什么:
在此代码的基础上:①增加一个派生类Base1继承基类Base;②Base1中重写Func1
class Base { public: virtual void Func1() { cout << "Func1()" << endl; } virtual void Func2() { cout << "Func2()" << endl; } void Func3() { cout << "Func3()" << endl; } private: int _b = 1; }; class Base1:public Base { public: virtual void Func1() { cout << "子类Func1()" << endl; } private: int _d = 2; }; int main() { Base b; Base1 d; return 0; }
从监视的结果来看:
①子类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员(Func1、Func2、_b),虚表指针也正是存这部分的,另一部分是自己的成员(_d);
②基类b对象和派生类d对象虚表是不一样的,这里的原因是Func1完成了重写,所以d的虚表中存的是重写的Base1::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法;
③另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但不是虚函数,所以不会放进虚表;
④虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr;
派生类虚表的生成总结:
a.先将基类中的虚表内容拷贝一份到派生类虚表中;
b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数;
c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
另外需要分清的是:虚函数、虚函数表都是存在哪里的(堆、栈、常量区)?
既然我们知道常量是存在代码区(常量区);静态变量是存在静态区;局部变量存在栈区;动态开辟的变量存在堆区,那我就以此设计一个程序看一下虚函数表是存在哪里的:
class Base { public: virtual void Func1() { cout << "Func1()" << endl; } virtual void Func2() { cout << "Func2()" << endl; } void Func3() { cout << "Func3()" << endl; } private: int _b = 1; }; class Base1 :public Base { public: virtual void Func1() { cout << "子类Func1()" << endl; } private: int _d = 2; }; int main() { Base b; Base1 d; int i = 0; static int j = 0; int* p1 = new int; const char* p2 = "xxxxxxxxx"; printf("栈区:%p\n", &i); printf("静态区:%p\n", &j); printf("堆区:%p\n", p1); printf("常量区:%p\n", p2); //虚函数表的地址存在对象b、d的前4个byte上,那怎么取到对象的前4个byte呢? //很显然Base类型无法强转为int类型 //那我们就取b、d的地址,将b、d的地址强转为int* //然后再解引用int*,此时就取到了b、d对象的前4个byte Base* p3 = &b; Base1* p4 = &d; printf("Base的虚表地址:%p\n", *(int*)p3); printf("Base1的虚表地址:%p\n", *(int*)p4); return 0; }
很明显,结果显示虚表地址与常量区的地址是最近的,所以我们可以判断出虚表地址是存放在代码段的(常量区)。
⑤注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样,都是存在代码段(常量区)的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。
如果说虚函数存在虚表,虚表存在对象,那么这种说法就是错误的,正确的说法应该为:
虚函数的指针存在虚表,虚表的指针存在对象!
2. 多态的原理
说了这么个老半天,我还是虚头八脑,多态的原理到底是什么呢?
class Luck_Peo { public: virtual void Red_Packet() { cout << "五块红包" << endl; } }; class Unluck_Peo:public Luck_Peo { public: virtual void Red_Packet() { cout << "五毛红包" << endl; } }; void Func(Luck_Peo& p) { p.Red_Packet(); } int main() { Luck_Peo lp; Unluck_Peo up; Func(lp); Func(up); return 0; }
在这串代码里:Func函数传lp调用的是Luck_Peo::Red_Packet,传up调用的是Unluck_Peo::Red_Packet,怎么就这么神?怎么就做到指向父类调父类,指向子类调子类?what!?
1. 上图的红色箭头我们看到,p是指向lp对象时,p->Red_Packet在lp的虚表中找到的虚函数是Luck_Peo::Red_Packet。
2. 上图的蓝色箭头我们看到,p是指向up对象时,p->Red_Packet在up的虚表中找到的虚函数是Unluck_Peo::Red_Packet。
3. 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
另外我们需要了解的是:
满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中去找的。不满足多态的函数调用时是编译时确认好的。
即:
多态调用:运行时,去虚函数表中找函数的地址进行调用,所以指向父类调的是父类虚函数,指向子类调的是子类虚函数;
普通调用:编译时,通过调用者类型确定函数地址。
3.动态绑定与静态绑定
1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态, 比如:函数重载;
2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体 行为,调用具体的函数,也称为动态多态。