4.多态的原理
4.1虚函数表
大家认为bb的大小是多少?如果按照内存对齐的概念,大小是否是8呢?正确的答案是12,因为有一个虚函数,所以这个类里面存在一个指针指向虚函数表,这个虚函数表就是一个函数指针数组,里面存放的是虚函数的指针。
class Base { public: virtual void Func2() { cout << "Base::Func2()" << endl; } private: int _b = 1; char _ch = 'a'; }; int main() { Base bb; cout << sizeof(bb) << endl; return 0; }
这个虚函数表只存放虚函数的指针。
一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表,。那么派生类中这个表放了些什么呢?我们接着往下分析
class Base { public: void Func1() { cout << "Base::Func1()" << endl; } virtual void Func2() { cout << "Base::Func2()" << endl; } virtual void Func3() { cout << "Base::Func3()" << endl; } private: int _b = 1; char _ch = 'a'; }; class Derive : public Base { public: virtual void Func3() { cout << "Derive::Func1()" << endl; } private: int _d = 2; }; int main() { Base bb; cout << sizeof(bb) << endl; Derive dd; cout << sizeof(dd) << endl; return 0; }
当基类Base的派生类Derive重写了Func3之后,虚表的地址就不一样了。,Func3的地址也不同了,因为已经覆盖掉了。
完成多态的条件之后,这个f函数就就是多态调用,就会去找相对应的虚表。
通过观察和测试,我们发现了以下几点问题:
1. 派生类对象dd中也有一个虚表指针,dd对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
2. 基类bb对象和派生类dd对象虚表是不一样的,这里我们发现Func3完成了重写,所以d的虚表中存的是重写的Derive::Func3,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func1也继承下来了,但是不是虚函
数,所以不会放进虚表。
4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
5. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
6. 这里还有一个童鞋们很容易混淆的问题:虚函数存在哪的?虚表存在哪的? 答:虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。但是很多童鞋都是这样深以为然的。注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。
4.2多态的原理
上面分析了这个半天了那么多态的原理到底是什么?还记得这里Func函数传Person调用的Person::BuyTicket,传Student调用的是Student::BuyTicket 。
p是指向mike对象时,p->BuyTicket在mike的虚表中找到虚函数是Person::BuyTicket
p是指向johnson对象时,p->BuyTicket在johson的虚表中找到虚函数是Student::BuyTicket
这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。
4.3 动态绑定与静态绑定
1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
5.单继承和多继承关系的虚函数表
5.1 单继承中的虚函数表
观察下图中的监视窗口中我们发现看不见func3和func4。这里是编译器的监视窗口故意隐藏了这两个函数,也可以认为是他的一个小bug。那么我们如何查看d的虚表呢?下面我们使用代码打印出虚表中的函数。
class Base { public: virtual void func1() { cout << "Base::func1" << endl; } virtual void func2() { cout << "Base::func2" << endl; } private: int a; }; class Derive :public Base { public: virtual void func1() { cout << "Derive::func1" << endl; } virtual void func3() { cout << "Derive::func3" << endl; } virtual void func4() { cout << "Derive::func4" << endl; } private: int b; }; int main() { Base b; Derive d; return 0; }
思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数
指针的指针数组,这个数组最后面放了一个nullptr
1.先取b的地址,强转成一个int*的指针
2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
4.虚表指针传递给PrintVTable进行打印虚表
typedef void(*VFPTR) (); void PrintVTable(VFPTR vTable[]) { // 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数 cout << " 虚表地址>" << vTable << endl; for (int i = 0; vTable[i] != nullptr; ++i) { printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]); VFPTR f = vTable[i]; f(); } cout << endl; }
然后我们就可以发现func1被重写之后,将新的地址覆盖了旧的地址存放在Derve的虚表里面,这就是多态的原理,可以使得多态调用能够完成,
5.2 多继承中的虚函数表
观察下图可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中
今天的分享到这里就结束了,感谢大家的阅读!