标题:【C++】多态进阶
@水墨不写bug
目录
(一)多态的原理
(1)虚函数表
在参加笔试的时候,你可能会见到这样的一道题:
// 这里常考一道笔试题:sizeof(Base)是多少?(x86) class Base { public: virtual void Func() { cout << "Func()" << endl; } private: int _a = 2; };
int main() { cout << sizeof(Base) << endl; return 0; }
输出:
8
你第一时间一定会感到疑惑:这个类Base就只有一个变量_a,占有4字节,为什么结果是8字节?其实,通过调试窗口观察我们会发现:Base实例化的对象内除了_a成员,还多了一个指针:
__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。
一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
虚表是一个指针,指向一段连续的空间(指针数组),这个数组内存有这个class的所有虚函数的地址。
#include<iostream> using namespace std; class Base { public: virtual void Func1() { cout << "Func1()" << endl; } virtual void func2() {} virtual void func3() {} virtual void func4() {} virtual void func5() {} void func6() {} private: int _b = 1; }; int main() { Base b; Base b2; return 0; }
Base类实例化的所有对象共用一个虚函数表。
虚函数在继承中的应用就是就是实现多态。
针对上面的代码我们做出以下改造:
1.我们增加一个派生类Derive去继承Base
2.Derive中重写Func1
3.Base再增加一个虚函数Func2和一个普通函数Func3
class Base { public: virtual void Func1() { cout << "Base::Func1()" << endl; } virtual void Func2() { cout << "Base::Func2()" << endl; } void Func3() { cout << "Base::Func3()" << endl; } private: int _b = 1; }; class Derive : public Base { public: virtual void Func1() { cout << "Derive::Func1()" << endl; } private: int _d = 2; }; int main() { Base b; Derive d; return 0; }
通过观察调试监视窗口:
1.派生类对象d中含有两部分,一部分是基类继承下来的Base成员,一类是自己的成员。
2,d对象的继承了b对象,自然有虚函数表,它的虚函数表存在于继承的Base部分中。
3.基类b对象和派生类d对象虚表的内容不完全一样,在派生类中我们对Func1完成了重写,所以d的虚表的Base::Func1的位置被Derive::Func1覆盖了。(所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法)
另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函
数,所以不会放进虚表。
虚表的本质就是一个存虚函数指针的数组,一般情况下,这个数组最后放了一个nullptr。
总结一下派生类的虚表生成:
a.先将基类中的虚表内容拷贝一份到派生类虚表中。
b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数。
c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
容易混淆的存储问题:
虚函数存在哪?虚表存在哪?
这需要仔细思考:
注意:
虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。对象中存的不是虚表,存的是虚表指针。
实际我们去验证一下会发现vs下是存在代码段的。
(2)多态的原理
通过以上的分析,你是不是能对多态的原理越来越清晰了?铺垫完了,现在我们为了便于分析多态的原理,仍然采用之前采用的例子:
class Person { public: virtual void BuyTicket() { cout << "买票-全价" << endl; } }; class Student : public Person { public: virtual void BuyTicket() { cout << "买票-半价" << endl; } }; void Func(Person& p) { p.BuyTicket(); } int main() { Person per; Func(per); Student stu; Func(stu); return 0; }
在《初识多态》中,我们讲过:实现多态,需要满足-->基类的指针或者引用调用重写后的虚函数。
在上面这个例子中,我们实现Person基类,声明Buyticket为虚函数,并在派生类Student中重写了虚函数Buyticket。
多态的原理:
1.per调用Func:传引用,p是指向per对象的,p->BuyTicket在per的虚表中找到虚函数是Person::BuyTicket。
2.stu调用Func:传引用,p是指向stu对象时,p->BuyTicket在stu的虚表中找到虚函数是Student::BuyTicket。
这就解释了多态的具体调用的过程原理。
总结:
多态其实是一种动态绑定:在程序运行期间,根据虚表内函数的(经过重写后)地址来调用对应的函数,多态实现了不同对象进行同一行为而产生了不同的结果。
(3)动态绑定与静态绑定
1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载。
2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
完~
未经作者同意禁止转载