简述C++虚函数作用及底层实现原理
1.foreword
C++是面向对象程序设计,其包括3项特点:
(1)数据抽象:接口和实现分离
(2)继承:父类和子类
(3)多态:动态绑定
本文讨论多态。
当父类希望子类重新定义某些函数时,用virtual关键字声明为虚函数。
当我们使用一个基类类型的引用或者指针,调用一个虚函数时就引发动态绑定/多态的发生。函数运行版本由传入的实参类型决定。可以用父类型的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。
父类指针好像有“多种类型”。这是一种泛型技术,试图用不变的代码实现可变的算法,比如模板技术,虚函数技术等。
引用或者指针的静态类型与动态类型不同这一事实,是C++支持多态的核心。
上述这句话选自《C++ Primer》,颇有玄门正宗之感。复杂的技术根植于底层的原理,所以搞清原理方可深入浅出。
当我们使用基类的引用、指针去调用基类中定义的一个虚函数时,并不知道该函数真正作用的对象是什么类型,可能是一个基类对象,也可能是一个派生类对象,这一切直到运行时才可知道。所以多态又称运行时绑定/动态绑定。
对于非虚函数的调用在编译时即可确定,哪个对象,哪个方法,地址是确定的。
2.虚函数底层实现原理
当你记住上面的概念时,它仅是概念。但当真正了解底层原理后,发现其中妙不可言。
如果让你来实现虚函数的功能?你会怎么实现?
复杂的代码?高深的原理?其实C++中实现的非常接地气。
虚函数是通过虚函数表和虚函数指针来实现的。
该表一般位于某类型的对象实例在内存中的最开始的位置。
单类继承
class Base {public:virtual void f() { cout << "Base::f()" << endl; }virtual void g() { cout << "Base::g()" << endl; }virtual void h() { cout << "Base::h()" << endl; }};
父类对象其在内存中布局示意如下:
虚函数表的尾部为虚函数表的结束结点。
再定义一个子类,此时并不覆盖父类的虚函数:
class Derived :public Base {public:virtual void f1() { cout << "Derived::f1()" << endl; }virtual void g1() { cout << "Derived::g1()" << endl; }virtual void h1() { cout << "Derived::g1()" << endl; }};
此时可以得知:
(1)虚函数按照声明顺序放在表中;
(2)父类的虚函数,排在子类虚函数之前。
当我们把子类中的函数覆盖时:
class Derived :public Base {public:// f() overridevoid f() { cout << "Derived::f1()" << endl; }virtual void g1() { cout << "Derived::g1()" << endl; }virtual void h1() { cout << "Derived::g1()" << endl; }};
此时可以得知:
(1)子类覆盖的虚函数,放在原来父类该虚函数的位置;所以当父类指针指向该子类对象时,调用方法就会调用子类重载的方法;
(2)没有被覆盖的虚函数依旧。
多类继承
当发生多类继承时:
虚函数表内存排列示意如下:
此时可以得知:
(1)每个继承的父类,都有自己的虚函数表;
(2)子类虚函数,放在第一个声明顺序的父类的表中。
当子类虚函数重写时,此时类的继承为:
子类将f( )函数重写,则内存中的排列为:
此时我们可以得知:
(1)三个父类虚函数表中的f( )函数被替换为子类函数指针。因此当我们用任一静态类型的父类指针来指向子类时,调用f( )时就调用的子类的f( )。
参考资料:
[1] /webary/p/4731457.html