C++通过继承(inheritance)虚函数(virtual function)来实现多态性。所谓多态,简单地说就是,将基类的指针或引用绑定到子类的实例,然后通过基类的指针或引用调用实际子类的成员函数(虚函数)。本文将介绍单继承、多重继承下虚函数的实现机制。

一、虚函数表

为了支持虚函数机制,编译器为每一个拥有虚函数的类的实例创建了一个虚函数表(virtual table),这个表中有许多的槽(slot),每个槽中存放的是虚函数的地址。虚函数表解决了继承、覆盖、添加虚函数的问题,保证其真实反应实际的函数。

为了能够找到 virtual table,编译器在每个拥有虚函数的类的实例中插入了一个成员指针 vptr,指向虚函数表。下面是一个例子:

  1. class Base
  2. {
  3. public:
  4. virtual void x() { cout << "Base::x()" << endl; }
  5. virtual void y() { cout << "Base::y()" << endl; }
  6. virtual void z() { cout << "Base::z()" << endl; }
  7. };
  8.  
  9. typedef void(*pFun)(void);
  10.  
  11. int main()
  12. {
  13. Base b;
  14. int* vptr = (int*)&b; // 虚函数表地址
  15.  
  16. pFun func1 = (pFun)*((int*)*vptr); // 第一个函数
  17. pFun func2 = (pFun)*((int*)*vptr+1); // 第二个函数
  18. pFun func3 = (pFun)*((int*)*vptr+2); // 第三个函数
  19.  
  20. func1(); // 输出Base::x()
  21. func2(); // 输出Base::y()
  22. func3(); // 输出Base::z()
  23. return 0;
  24. }

上面定义了一个Base类,其中有三个虚函数。我们将Base类对象取址 &b 并强制转换为 int*,取得虚函数表的地址。然后对虚函数表的地址取值 *vptr 并强转为 int*,即取得第一个虚函数的地址了。将第一个虚函数的地址加1,取得第二个虚函数的地址,再加1即取得第三个虚函数的地址。

注意,之所以可以通过对象实例的地址得到虚函数表,是因为 vptr 指针位于对象实例的最前面(这是由编译器决定的,主要是为了保证取到虚函数表有最高的性能——如果有多层继承或是多重继承的情况下)。如图所示:

在VS2012中加断点进行Debug可以查看到虚函数表:

二、单继承时的虚函数表

1、无虚函数覆盖

假如现有单继承关系如下:

  1. class Base
  2. {
  3. public:
  4. virtual void x() { cout << "Base::x()" << endl; }
  5. virtual void y() { cout << "Base::y()" << endl; }
  6. virtual void z() { cout << "Base::z()" << endl; }
  7. };
  8.  
  9. class Derive : public Base
  10. {
  11. public:
  12. virtual void x1() { cout << "Derive::x1()" << endl; }
  13. virtual void y1() { cout << "Derive::y1()" << endl; }
  14. virtual void z1() { cout << "Derive::z1()" << endl; }
  15. };

在这个单继承的关系中,子类没有重写父类的任何方法,而是加入了三个新的虚函数。Derive类实例的虚函数表布局如图示:

  • Derive class 继承了 Base class 中的三个虚函数,准确的说,是该函数实体的地址被拷贝到 Derive 实例的虚函数表对应的 slot 之中。

  • 新增的 虚函数 置于虚函数表的后面,并按声明顺序存放。

2、有虚函数覆盖

如果在继承关系中,子类重写了父类的虚函数:

  1. class Base
  2. {
  3. public:
  4. virtual void x() { cout << "Base::x()" << endl; }
  5. virtual void y() { cout << "Base::y()" << endl; }
  6. virtual void z() { cout << "Base::z()" << endl; }
  7. };
  8.  
  9. class Derive : public Base
  10. {
  11. public:
  12. virtual void x() { cout << "Derive::x()" << endl; } // 重写
  13. virtual void y1() { cout << "Derive::y1()" << endl; }
  14. virtual void z1() { cout << "Derive::z1()" << endl; }
  15. };

则Derive类实例的虚函数表布局为:

相比于无覆盖的情况,只是把 Derive::x() 覆盖了Base::x(),即第一个槽的函数地址发生了变化,其他的没有变化。

这时,如果通过绑定了子类对象的基类指针调用函数 x(),会执行 Derive 版本的 x(),这就是多态。

三、多重继承时的虚函数表

1、无虚函数覆盖

现有如下的多重继承关系,子类没有覆盖父类的虚函数:

  1. class Base1
  2. {
  3. public:
  4. virtual void x() { cout << "Base1::x()" << endl; }
  5. virtual void y() { cout << "Base1::y()" << endl; }
  6. virtual void z() { cout << "Base1::z()" << endl; }
  7. };
  8.  
  9. class Base2
  10. {
  11. public:
  12. virtual void x() { cout << "Base2::x()" << endl; }
  13. virtual void y() { cout << "Base2::y()" << endl; }
  14. virtual void z() { cout << "Base2::z()" << endl; }
  15. };
  16.  
  17. class Derive : public Base1, public Base2
  18. {
  19. public:
  20. virtual void x1() { cout << "Derive::x1()" << endl; }
  21. virtual void y1() { cout << "Derive::y1()" << endl; }
  22. };

对于 Derive 实例 d 的虚函数表布局,如下图:

可以看出:

  • 每个基类子对象对应一个虚函数表。
  • 派生类中新增的虚函数放到第一个虚函数表的后面。

测试代码(VS2012):

  1. typedef void(*pFun)(void);
  2.  
  3. int main()
  4. {
  5. Derive b;
  6. int** vptr = (int**)&b; // 虚函数表地址
  7.  
  8. // virtual table 1
  9. pFun table1_func1 = (pFun)*((int*)*vptr+0); // vptr[0][0]
  10. pFun table1_func2 = (pFun)*((int*)*vptr+1); // vptr[0][1]
  11. pFun table1_func3 = (pFun)*((int*)*vptr+2); // vptr[0][2]
  12. pFun table1_func4 = (pFun)*((int*)*vptr+3); // vptr[0][3]
  13. pFun table1_func5 = (pFun)*((int*)*vptr+4); // vptr[0][4]
  14.  
  15. // virtual table 2
  16. pFun table2_func1 = (pFun)*((int*)*(vptr+1)+0); // vptr[1][0]
  17. pFun table2_func2 = (pFun)*((int*)*(vptr+1)+1); // vptr[1][1]
  18. pFun table2_func3 = (pFun)*((int*)*(vptr+1)+2); // vptr[1][2]
  19.  
  20. // call
  21. table1_func1();
  22. table1_func2();
  23. table1_func3();
  24. table1_func4();
  25. table1_func5();
  26.  
  27. table2_func1();
  28. table2_func2();
  29. table2_func3();
  30. return 0;
  31. }

不同编译器对 virtual table 的实现不同,经测试,在 g++ 中需要这样:

  1. // virtual table 1
  2. pFun table1_func1 = (pFun)*((int*)*vptr+0); // vptr[0][0]
  3. pFun table1_func2 = (pFun)*((int*)*vptr+2); // vptr[0][2]
  4. pFun table1_func3 = (pFun)*((int*)*vptr+4); // vptr[0][4]
  5. pFun table1_func4 = (pFun)*((int*)*vptr+6); // vptr[0][6]
  6. pFun table1_func5 = (pFun)*((int*)*vptr+8); // vptr[0][8]
  7.  
  8. // virtual table 2
  9. pFun table2_func1 = (pFun)*((int*)*(vptr+1)+0); // vptr[1][0]
  10. pFun table2_func2 = (pFun)*((int*)*(vptr+1)+2); // vptr[1][2]
  11. pFun table2_func3 = (pFun)*((int*)*(vptr+1)+4); // vptr[1][4]

2、有虚函数覆盖

将上面的多重继承关系稍作修改,让子类重写基类的 x() 函数:

  1. class Base1
  2. {
  3. public:
  4. virtual void x() { cout << "Base1::x()" << endl; }
  5. virtual void y() { cout << "Base1::y()" << endl; }
  6. virtual void z() { cout << "Base1::z()" << endl; }
  7. };
  8.  
  9. class Base2
  10. {
  11. public:
  12. virtual void x() { cout << "Base2::x()" << endl; }
  13. virtual void y() { cout << "Base2::y()" << endl; }
  14. virtual void z() { cout << "Base2::z()" << endl; }
  15. };
  16.  
  17. class Derive : public Base1, public Base2
  18. {
  19. public:
  20. virtual void x() { cout << "Derive::x()" << endl; } // 重写
  21. virtual void y1() { cout << "Derive::y1()" << endl; }
  22. };

这时 Derive 实例的虚函数表布局会变成下面这个样子:

相比于无覆盖的情况,只是将Derive::x()覆盖了Base1::x()Base2::x()而已,你可以自己写测试代码测试一下,这里就不再赘述了。

注:若虚函数是 private 或 protected 的,我们照样可以通过访问虚函数表来访问这些虚函数,即上面的测试代码一样能运行。

附:编译器对指针的调整

在多重继承下,我们可以将子类实例绑定到任一父类的指针(或引用)上。以上述有覆盖的多重继承关系为例:

  1. Derive b;
  2. Base1* ptr1 = &b; // 指向 b 的初始地址
  3. Base2* ptr2 = &b; // 指向 b 的第二个子对象
  • 因为 Base1 是第一个基类,所以 ptr1 指向的是 Derive 对象的起始地址,不需要调整指针(偏移)。
  • 因为 Base2 是第二个基类,所以必须对指针进行调整,即加上一个 offset,让 ptr2 指向 Base2 子对象。
  • 当然,上述过程是由编译器完成的。

当然,你可以在VS2012里通过Debug看出 ptr1 和 ptr2 是不同的,我们可以这样子:

  1. Base1* b1 = (Base1*)ptr2;
  2. b1->y(); // 输出 Base2::y()
  3. Base2* b2 = (Base2*)ptr1;
  4. b2->y(); // 输出 Base1::y()

其实,通过某个类型的指针访问某个成员时,编译器只是根据类型的定义查找这个成员所在偏移量,用这个偏移量获取成员。由于 ptr2 本来就指向 Base2 子对象的起始地址,所以b1->y()调用到的是Base2::y(),而 ptr1 本来就指向 Base1 子对象的起始地址(即
Derive对象的起始地址),所以b2->y()调用到的是Base1::y()

个人站点:http://songlee24.github.com


参考:1、《Inside The C++ Object Model》

2、http://blog.csdn.net/haoel/article/details/1948051

C++进阶之虚函数表的更多相关文章

  1. C++ 虚函数表解析

    转载:陈皓 http://blog.csdn.net/haoel 前言 C++中 的虚函数的作用主要是实现了多态的机制.关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实 ...

  2. C++ 多态、虚函数机制以及虚函数表

    1.非virtual函数,调用规则取决于对象的显式类型.例如 A* a  = new B(); a->display(); 调用的就是A类中定义的display().和对象本体是B无关系. 2. ...

  3. C++迟后联编和虚函数表

    先看一个题目: class Base { public: virtual void Show(int x) { cout << "In Base class, int x = & ...

  4. C++ 知道虚函数表的存在

    今天翻看陈皓大大的博客,直接找关于C++的东东,看到了虚函数表的内容,找一些能看得懂的地方记下笔记. 0 引子 类中存在虚函数,就会存在虚函数表,在vs2015的实现中,它存在于类的头部. 假设有如下 ...

  5. C++虚函数和虚函数表

    前导 在上面的博文中描述了基类中存在虚函数时,基类和派生类中虚函数表的结构. 在派生类也定义了虚函数时,函数表又是怎样的结构呢? 先看下面的示例代码: #include <iostream> ...

  6. C++ Daily 《5》----虚函数表的共享问题

    问题: 包含一个以上虚函数的 class B, 它所定义的 对象是否共用一个虚函数表? 分析: 由于含有虚函数,因此对象内存包含了一个指向虚函数表的指针,但是这个指针指向的是同一个虚函数表吗? 实验如 ...

  7. C++虚函数表

    大家知道虚函数是通过一张虚函数表来实现的.在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承.覆盖的问题,其内容真是反应实际的函数.这样,在有虚函数的类的实例中,这个表分配在了这个实例的内存中 ...

  8. 对C++虚函数、虚函数表的简单理解

    一.虚函数的作用 以一个通用的图形类来了解虚函数的定义,代码如下: #include "stdafx.h" #include <iostream> using name ...

  9. 深入理解C++虚函数表

    虚函数表是C++类中存放虚函数的一张表,理解虚函数表对于理解多态很重要. 本次使用的编译器是VS2013,为了简化操作,不用去操作函数指针,我使用到了VS的CL编译选项来查看类的内存布局. CL使用方 ...

随机推荐

  1. Laravel 的 API 认证系统 Passport 三部曲(二、passport的具体使用)

    GQ1994 关注 2018.04.20 09:31 字数 1152 阅读 1316评论 0喜欢 1 参考链接 Laravel 的 API 认证系统 Passport 三部曲(一.passport安装 ...

  2. MySQL ORDER BY IF() 条件排序

    源 在做sqlzoo的时候,碰到一个SQL的排序问题,他把符合条件的单独几行,可以放在查询结果的开始,或者查询结果的尾部 通过的方法就是IN语句(也可以通过IF语句) 自己做了个测试,如下,这个是表的 ...

  3. CREATE TABLE AS - 从一条查询的结果中创建一个新表

    SYNOPSIS CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } ] TABLE table_name [ (column_name [, ...] ...

  4. delphi 7 生成 调用 bat文件的exe文件

    unit Unit1; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms ...

  5. java线程池 多线程 搜索包含关键字的文件路径

    package org.jimmy.searchfile20180807.main; public class ThreadMain implements Runnable{ private int ...

  6. Linux运维到底是做什么的?在开始学习之前,你必须了解这些!

    首先祝贺你选择学习Linux,你可能即将踏上Linux的工作之旅,出发之前,让我带你来看一看关于Linux和Linux运维的一切. Linux因其高效率.易于裁剪.应用广等优势,成为了当今中高端服务器 ...

  7. android中ListView的定位:使用setSelectionFromTop

    如果一个ListView太长,有时我们希望ListView在从其他界面返回的时候能够恢复上次查看的位置,这就涉及到ListView的定位问题: 解决的办法如下: 1 2 3 4 5 6 7 // 保存 ...

  8. Sql语句的一些事(二)

    与sql语句的书写顺序并不是一样的,而是按照下面的顺序来执行 from--where--group by--having--select--order by, from:需要从哪个数据表检索数据 wh ...

  9. Python 面向对象 组合-多态与多态性-封装-property

    面向对象-组合 1.什么是组合 组合指的是某一个对象拥有一个属性,该属性的值是另外一个类的对象 class Foo: xxx = 111 class Bar: yyy = 222 obj = Foo( ...

  10. String 工具类

    package com.mytripod.util; import sun.rmi.runtime.Log; import java.io.UnsupportedEncodingException; ...