C++中的虚函数的作用主要是实现了多态的机制,基类定义虚函数,子类可以重写该函数,在派生类中对基类定义的虚函数进行重写时,需要在派生类中声明该方法为虚方法,这篇文章主要给大家介绍了关于如何通过一篇文章彻底弄懂C++虚函数的实现机制,需要的朋友可以参考下
1、虚函数简介
C++中有两种方式实现多态,即重载和覆盖。
- 重载:是指允许存在多个同名函数,而这些函数的参数表不同(参数个数不同、参数类型不同或者两者都不同)。
- 覆盖:是指子类重新定义父类虚函数的做法,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针拥有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法,比如:模板元编程是在编译期完成的泛型技术,RTTI、虚函数则是在运行时完成的泛型技术。
关于虚函数的具体使用方法,建议大家先去阅读相关的C++的书籍,本文只剖析虚函数的实现机制,让大家对虚函数有一个更加清晰的认识,并不对虚函数的具体使用方法作过多介绍。本文是依据个人经验和查阅相关资料最终编写的,如有错漏,希望大家多多指正。
2、虚函数表简介
学过C++的人都应该知道虚函数(Virtual Function)是通过虚函数表(Virtual Table,简称为V-Table)来实现的。虚函数表主要存储的是指向一个类的虚函数地址的指针,通过使用虚函数表,继承、覆盖的问题都都得到了解决。假如一个类有虚函数,当我们构建这个类的实例时,将会额外分配一个指向该类虚函数表的指针,当我们用父类的指针来操作一个子类的时候,这个指向虚函数表的指针就派上用场了,它指明了此时应该使用哪个虚函数表,而虚函数表本身就像一个地图一样,为编译器指明了实际所应该调用的函数。指向虚函数表的指针是存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能――如果有多层继承或是多重继承的情况下),这就意味着理论上我们可以通过对象实例的地址得到这张虚函数表(实际上确实可以做到),然后对虚函数表进行遍历,并调用其中的函数。
前面说了一大堆理论,中看不中用,下面还是通过一个实际的例子验证一下前面讲的内容,首先定义一个Base
类,该类有三个虚函数,代码如下:
#include #include typedef void (*Fun)(void); class Base { public: virtual void f() { std::cout << "Base::f()" << std::endl; } virtual void g() { std::cout << "Base::g()" << std::endl; } virtual void h() { std::cout << "Base::h()" << std::endl; } };
接下来按照前面的说法,我们通过Base类的实例对象base来获取虚函数表,代码如下:
int main(int argc, char* argv[]) { Base base; Fun fun = nullptr; std::cout << "指向虚函数表指针的地址:" << (long*)(&base) << std::endl; std::cout << "虚函数表的地址:" << (long*)*(long*)(&base) << std::endl; fun = (Fun)*((long*)*(long*)(&base)); std::cout << "虚函数表中第一个函数的地址:" << (long*)fun << std::endl; fun(); fun = (Fun)*((long*)*(long*)(&base) + 1); std::cout << "虚函数表中第二个函数的地址:" << (long*)fun << std::endl; fun(); fun = (Fun)*((long*)*(long*)(&base) + 2); std::cout << "虚函数表中第三个函数的地址:" << (long*)fun << std::endl; fun(); }
运行结果图2-1所示(Linux 3.10.0 + GCC 4.8.5):
图2-1 程序运行结果
在上面的例子中我们通过把&base强制转换成long *,来取得指向虚函数表的指针的地址,然后对这个地址取值就可以得到对应的虚函数表了。得到对应虚函数表的首地址后,就可以通过不断偏移该地址,依次得到指向真实虚函数的指针了。这么说有点绕也有点晕,下面通过一幅图解释一下前面说的内容,详见图2-2
图2-2 基类虚函数表内存布局
当然,上述内容也可以在GDB中调试验证,后续的内容也将全部在GDB下直接验证,调试的示例见图2-3:
图2-3 GDB查看基类虚函数表内存布局
3、有继承关系的虚函数表剖析
前面分析虚函数表的场景是没有继承关系的,然而在实际开发中,没有继承关系的虚函数纯属浪费表情,所以接下来我们就来看看有继承关系下虚函数表会呈现出什么不一样的特点,分析的时候会分别就单继承无虚函数覆盖、单继承有虚函数覆盖、多重继承、多层继承这几个场景进行说明。
3.1、单继承无虚函数覆盖的情况
先定义一个Base类,再定义一个Derived类,Derived类继承于Base类,代码如下:
#in<p style="color:transparent">来源gao!daima.com搞$代!码网</p>clude #include class Base { public: virtual void f() { std::cout << "Base::f()" << std::endl; } virtual void g() { std::cout << "Base::g()" << std::endl; } virtual void h() { std::cout << "Base::h()" << std::endl; } }; class Derived : public Base { public: virtual void f1() { std::cout << "Derived::f1()" << std::endl; } virtual void g1() { std::cout << "Derived::g1()" << std::endl; } virtual void h1() { std::cout << "Derived::h1()" << std::endl; } };
继承关系如图3-1所示:
图3-1 类继承关系UML图
测试的代码如下,因为等下要使用GDB来验证,所以就随便写点,定义个Derived
类实例就行了
int main(int argc, char* argv[]) { Derived derived; derived.f(); }
派生类Derived的虚函数表内存布局如图3-2所示:
图3-2 单继承无虚函数覆盖情况下派生类虚函数表内存布局
接下来就用GDB调试一下,验证上图的内存布局是否正确,如图3-3所示:
图3-3 GDB查看单继承无虚函数覆盖情况下派生类虚函数表内存布局
从调试结果可以看出图3-2是正确的,Derived的虚函数表中先放Base的虚函数,再放Derived的虚函数。
3.2、单继承有虚函数覆盖的情况
派生类覆盖基类的虚函
以上就是一篇文章彻底弄懂C++虚函数的实现机制的详细内容,更多请关注gaodaima搞代码网其它相关文章!