• 虚函数表
  • 一般继承(无虚函数覆写)
  • 一般继承( 有虚函数覆写)
  • 静态代码发生了什么
  • 评价多态
  • 常见问答与实战
    • 问答
      • 为什么虚函数必须是类的成员函数?
      • 为什么类的静态成员函数不能为虚函数?
      • 为什么构造函数不能为虚函数?
  • 问答实战
    • 基类构造中的虚函数
    • 父类指针间接调用

虚函数表

C++的多态是通过一张虚函数表(Virtual Table)来实现的,简称为 V-Table。在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承、覆写的问题,保证其真实反应实际的函数。这样, 在有虚函数的类的实例中这个表被分配在了这个实例的内存中,定义类的时候是没有的.所以当我们用父类的指针来操作一个子类对象的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。

这里我们着重看一下这张虚函数表。C++的编译器应该保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。 这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。

假设我们有这样的一个类:

#include <iostream>
using namespace std;class Base {public:void f() { cout << "Base::f" << endl; }void g() { cout << "Base::g" << endl; }void h() { cout << "Base::h" << endl; }int a;
};int main()
{cout << "Base size:" << sizeof(Base) << endl;return 0;
}

运行结果为:

我们对于代码进行修改:给类内第一个函数加上virtual

#include <iostream>using namespace std;class Base {public:virtual void f() { cout << "Base::f" << endl; }void g() { cout << "Base::g" << endl; }void h() { cout << "Base::h" << endl; }int a;
};int main()
{cout << "Base size:" << sizeof(Base) << endl;return 0;
}

运行结果为:

我们把类内所有的函数都加上virtual

#include <iostream>using namespace std;class Base {public:virtual void f() { cout << "Base::f" << endl; }virtual void g() { cout << "Base::g" << endl; }virtual void h() { cout << "Base::h" << endl; }int a;
};int main()
{cout << "Base size:" << sizeof(Base) << endl;return 0;
}

运行结果为:

按照上面的说法,我们可以通过 Base 的实例来得到虚函数表。
内存模型图解说明:

上面的0x4042f8地址放在实例的最前面,效率比较高,一进来就能够找得到。0x4042f8是V-t的入口地址。所有的虚函数都在这个表里面进行排位,函数的本质就是地址,上面三个函数就代表三个地址。

#include <iostream>
using namespace std;
class Base
{public:virtual void f() { cout << "Base::f" << endl; }virtual void g() { cout << "Base::g" << endl; }virtual void h() { cout << "Base::h" << endl; }
};typedef void (*FUNC)();int main()
{cout << "Base size:" << sizeof(Base) << endl;Base b;cout << "对象的起始地址:" << &b << endl;cout << "虚函数表的地址:" << (int**)*(int*)&b << endl;cout << "虚函数表第一个函数的地址:"<< *((int**)*(int*)&b) << endl;cout << "虚函数表第二个函数的地址:" << *((int**)*(int*)&b + 1) << endl;//注意不要转为 FUNC 来打印,cout 没有重载FUNC pf = (FUNC)(*((char**)*(int*)&b));pf();pf = (FUNC)(*((void**)*(int*)&b + 1));pf();pf = (FUNC)(*((void**)*(int*)&b + 2));pf();return 0;
}

运行结果为:

通过这个示例,我们可以看到,我们可以通过强行把&b 转成 int* ,取得虚函数表的地址,然后,再次取址就可以得到第一个虚函数的地址了,也就是 Base::f(),这在上面的程序中得到了验证(把 int* 强制转成了函数指针)。

画个图解释一下。如下所示:

注意:在上面这个图中,我在虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符’\0’一样,其标志了虚函数表的结束。这个结束标志的值在不同的编译器下是不同的。

下面,我将分别说明"无覆写"和"有覆写"时的虚函数表的样子。没有覆写父类的虚函数是毫无意义的。我之所以要说明没有覆写的情况,主要目的是为了给一个对比。在比较之下,我们可以更加清楚地知道其内部的具体实现。

一般继承(无虚函数覆写)

下面,再让我们来看看继承时的虚函数表是什么样的。假设有如下所示的一个继承关系:

请注意,在这个继承关系中,子类没有覆写任何父类的函数。那么,在派生类的实例中,其虚函数表如下所示:

代码演示:

#include <iostream>
using namespace std;
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 Derive :public Base {public:virtual void f1() { cout << "Derive::f1" << endl; }virtual void g2() { cout << "Derive::g1" << endl; }virtual void h2() { cout << "Derive::h1" << endl; }
};typedef void (*FUNC)();
int main()
{cout << "Base size:" << sizeof(Base) << endl;Derive d;cout << "对象的起始地址:" << &d << endl;cout << "虚函数表的地址:" << (int**)*(int*)&d << endl;cout << "虚函数表第一个函数的地址:" << *((void**)*(int*)&d) << endl;cout << "虚函数表第二个函数的地址:" << *((void**)*(int*)&d + 1) << endl;FUNC pf = (FUNC)(*((char**)*(int*)&d));pf();pf = (FUNC)(*((void**)*(int*)&d + 1));pf();pf = (FUNC)(*((void**)*(int*)&d + 2));pf();pf = (FUNC)(*((void**)*(int*)&d + 3));pf();pf = (FUNC)(*((void**)*(int*)&d + 4));pf();pf = (FUNC)(*((void**)*(int*)&d + 5));pf();return 0;
}

运行结果为:

对于实例:Derive d; 的虚函数表如下:

我们可以看到下面几点:
1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面。

一般继承( 有虚函数覆写)

覆盖父类的虚函数是很显然的事情,不然,虚函数就变得毫无意义。下面,我们来看一下,如果子类中有虚函数重载覆写了父类的虚函数,会是一个什么样子?假设,我们有下面这样的一个继承关系。

为了让大家看到被继承过后的效果,在这个类的设计中,我只覆盖了父类的一个函数:f()。那么,对于派生类的实例,其虚函数表会是下面的一个样子。

我们对于代码进行修改:让子类的覆写一个父类的虚函数:

#include <iostream>
using namespace std;
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 Derive :public Base
{public:virtual void f() { cout << "Derive::f1" << endl; }virtual void g2() { cout << "Derive::g1" << endl; }virtual void h2() { cout << "Derive::h1" << endl; }
};typedef void (*FUNC)();int main()
{cout << "Base size:" << sizeof(Base) << endl;Derive d;cout << "对象的起始地址:" << &d << endl;cout << "虚函数表的地址:" << (int**)*(int*)&d << endl;cout << "虚函数表第一个函数的地址:" << *((void**)*(int*)&d) << endl;cout << "虚函数表第二个函数的地址:" << *((void**)*(int*)&d + 1) << endl;FUNC pf = (FUNC)(*((char**)*(int*)&d));pf();pf = (FUNC)(*((void**)*(int*)&d + 1));pf();pf = (FUNC)(*((void**)*(int*)&d + 2));pf();pf = (FUNC)(*((void**)*(int*)&d + 3));pf();pf = (FUNC)(*((void**)*(int*)&d + 4));pf();pf = (FUNC)(*((void**)*(int*)&d + 5));pf();return 0;
}

运行结果为:

图解说明:

我们从表中可以看到下面几点,
1)覆写的 f()函数被放到了虚表中原来父类同名虚函数的位置。
2)没有被覆盖的函数依旧存在。

这样,我们就可以看到对于下面这样的程序:

Base *b = new Derive();
b->f();

由 b 所指的内存中的虚函数表的 f() 的位置已经被 Derive::f() 函数地址所取代,于是在实际调用发生时,是 Derive::f()被调用了。这就实现了多态。

静态代码发生了什么

当编译器看到这段代码的时候,并不知道 b 真实身份。编译器能作的就是用一段代码代替这段语句。

Base *b = new Derive();
b->f();

1,明确 b 类型。
2,然后通过指针虚函数表的指针 vptr 和偏移量,匹配虚函数的入口。
3,根据入口地址调用虚函数。

一旦发生多态就会去首先遍历虚函数表,如果符合虚函数覆写,那么子类将会覆写父类的虚函数。如果不符合虚函数覆写,那么子类只是简单的实现对于父类的继承关系。

评价多态

1,实现了动态绑定。
2,牺牲了一些空间和效率,每次new虚函数表,并且每次调用需要遍历虚函数表,但也是值重的。

常见问答与实战

问答

为什么虚函数必须是类的成员函数?

虚函数诞生的目的就是为了实现多态,多态发生在父子类中,在类外定义虚函数毫无实际用处,编译不过。

为什么类的静态成员函数不能为虚函数?

如果定义为虚函数,那么它就是动态绑定的,也就是在派生类中可以被覆盖的,这与静态成员函数的定义(在内存中只有一份拷贝;通过类名或对象引用访问静态成员)本身就是相矛盾的。== 编译不过。==

为什么构造函数不能为虚函数?

因为如果构造函数为虚函数的话,它将在执行期间被构造,而执行期则需要对象已经建立,构造函数所完成的工作就是为了建立合适的对象,因此在没有构建好的对象上不可能执行多态(虚函数的目的就在于实现多态性)的工作。在继承体系中,构造的顺序就是从基类到派生类,其目的就在于确保对象能够成功地构建。构造函数同时承担着虚函数表的建立,如果它本身都是虚函数的话,如何确保 vtbl 的构建成功呢?编译不过。注意:当基类的构造函数内部有虚函数时,会出现什么情况呢?结果是在构造函数中,虚函数机制不起作用了,调用虚函数如同调用一般的成员函数一样。当基类的析构函数内部有虚函数时,又如何工作呢?与构造函数相同,只有"局部"的版本被调用。但是,行为相同,原因是不一样的。构造函数只能调用"局部"版本,是因为调用时还没有派生类版本的信息。析构函数则是因为派生类版本的信息已经不可靠了。我们知道,析构函数的调用顺序与构造函数相反,是从派生类的析构函数到基类的析构函数。当某个类的析构函数被调用时,其派生类的析构函数已经被调用了,相应的数据也已被丢失,如果再调用虚函数的派生类的版本,就相当于对一些不可靠的数据进行操作,这是非常危险的。因此,在析构函数中,虚函数机制也是不起作用的。

问答实战

基类构造中的虚函数

如下代码输出什么?

#include <iostream>
using namespace std;class A
{public:A() {p = this;   //子类的对象赋值给父类的指针p->func();  //发生了调用}virtual void func()   //父类中有虚函数 {cout << "aaaaaaaaaaaaaaaa" << endl;}
private:A* p;
};class B :public A
{public:virtual void func()  //子类覆写父类的虚函数{cout << "bbbbbbbbbbbbbbbbb" << endl;}
};int main()
{B b;return 0;
}

运行结果为:

我们进行解释说明:要实现多态首先建立在构造结束。
构造结束意味着虚函数表生成了。虚函数表在构造彻底结束之后生成。那么也就是说B b;在创建对象的时候先执行:

class A
{public:A() {p = this;   //子类的对象赋值给父类的指针p->func();  //发生了调用}virtual void func()   //父类中有虚函数 {cout << "aaaaaaaaaaaaaaaa" << endl;}
private:A* p;
};

生成对象之后先调用父类A的构造器,这个时候子类B的构造器没有完成,那么虚函数表就没有生成那么这个时候调用func( ) 只能就近原则,不能形成多态。所以就有了上面的打印结果。

那么接下来我们给出另一种情况:让生成的对象构造完成,代码演示:

#include <iostream>
using namespace std;class A
{public:A() {}virtual void func()   //父类中有虚函数 {cout << "aaaaaaaaaaaaaaaa" << endl;}~A(){p = this;   //子类的对象赋值给父类的指针p->func();  //发生了调用}
private:A* p;
};class B :public A
{public:virtual void func()  //子类覆写父类的虚函数{cout << "bbbbbbbbbbbbbbbbb" << endl;}
};int main()
{B b;return 0;
}

运行结果为:

我们进行解释说明:
构造的时候先构造类A,再构造类B,但是在析构的时候先析构类B在析构类A。那么在析构的时候类B已经析构完毕。所以类B的存在就没有意义了,所以唯一存在有意义的就是类A。

父类指针间接调用

#include <iostream>
using namespace std;class Base
{public:void foo() {this->func();     //子类调用父类的虚函数}virtual void func()    //父类有虚函数{cout << "Base" << endl;}
};class Derive :public Base
{public:virtual void func()      //子类覆写父类的虚函数{cout << "Derive" << endl;}
};int main()
{Derive d;d.foo();return 0;
}

运行结果为:

C++多态:多态实现原理剖析,虚函数表,评价多态,常见问答与实战【C++多态】(55)相关推荐

  1. 9-3:C++多态之多态的实现原理之虚函数表,虚函数表指针静态绑定和动态绑定

    文章目录 (1)虚函数表 (2)多态原理 (3)静态多态与动态多态 A:静态多态 B:动态多态 (1)虚函数表 如下代码,计算此带有虚函数的类的大小 #include <iostream> ...

  2. 虚函数原理与虚函数表

    目录 一. 虚函数 二.虚函数原理与虚函数表 一. 虚函数 虚函数: 使用 virtual 关键字声明的函数,是动态多态实现的基础. 非类的成员函数不能定义为虚函数. 类的静态成员函数不能定义为虚函数 ...

  3. 深入剖析C++多态、VPTR指针、虚函数表

    在讲多态之前,我们先来说说关于多态的一个基石------类型兼容性原则. 一.背景知识 1.类型兼容性原则 类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代.通过公有继承,派 ...

  4. C++中的多态——理解虚函数表及多态实现原理

    多态及其实现原理 一.多态的概念 概念 构成条件 二.虚函数的重写 重写的定义 重写的特殊情况 override和final关键字 区分重写.重载.重定义 抽象类的概念 三.多态的实现原理 父类对象模 ...

  5. 【C++ Primer | 15】虚函数表剖析(一)

    一.虚函数 1. 概念 多态指当不同的对象收到相同的消息时,产生不同的动作 编译时多态(静态绑定),函数重载,运算符重载,模板. 运行时多态(动态绑定),虚函数机制. 为了实现C++的多态,C++使用 ...

  6. C++中虚继承产生的虚基类指针和虚基类表,虚函数产生的虚函数指针和虚函数表

    本博客主要通过查看类的内容的变化,深入探讨有关虚指针和虚表的问题. 一.虚继承产生的虚基类表指针和虚基类表 如下代码:写一个棱形继承,父类Base,子类Son1和Son2虚继承Base,又来一个类Gr ...

  7. C++ 面向对象(二)多态 : 虚函数、多态原理、抽象类、虚函数表、继承与虚函数表

    目录 多态 多态的概念 多态的构成条件 虚函数 虚函数的重写 协变(返回值不同) 析构函数的重写(函数名不同) final和override final override 重载, 重写, 重定义对比 ...

  8. C++多态的原理(虚函数指针和虚函数表)

    C++多态的原理 (虚函数指针和虚函数表) 1.虚函数指针和虚函数表 2.继承中的虚函数表 2.1单继承中的虚函数表 2.2多继承中的虚函数表 3.多态的原理 4.总结 1.虚函数指针和虚函数表 以下 ...

  9. 猿创征文|深入剖析多态的实现原理与虚函数指针

    学习导航 一.多态实现原理 二.不同情况下的虚函数表 (1)单继承无虚函数覆盖 (2)单继承有虚函数覆盖 (3)多继承无虚函数覆盖 (4)多继承有虚函数覆盖 三.对虚函数指针与虚函数表的深入理解 (1 ...

最新文章

  1. deepin10.15安装cuda10.1.168 cudnn7.6.1 tensorflow_gpu1.4.0
  2. asp.net防止刷新时重复提交
  3. 根据当前日期算前一年、前一月、前一天(java基础)
  4. javascript读取xml文件读取节点数据的例子
  5. 找出那个数字出现3次的数字
  6. 逗号表达式的值--最后一项的值
  7. 前端学习(1066):ES6里面的三个注意点1
  8. 制度化规范化标准化精细化_管理技巧:为什么说企业制度化管理势在必行?好处太多了...
  9. 【转】webpack中关于source map的配置
  10. 【HDU 5384】Danganronpa(AC自己主动机)
  11. Appium移动自动化测试教程
  12. eclipse启动tomcat内存溢出解决方式
  13. php解析视频_YY神曲视频PHP解析调用代码
  14. SpringMVC:生成Excel和PDF
  15. 【C#】Excel操作——两个Excel表格比较,如果相同跳过,如果不同将复制到一个表格
  16. 【Unity】Unity5.0之PBR/PBS详解
  17. 基于DEM的GIS水文分析——河网与集水区域的提取
  18. 大规模定制家具实施ERP的必要性
  19. VB生成二维码图形的控件,CSDN利用盗版卖卖会员44积分赚钱
  20. bitcoin区块结构分析

热门文章

  1. buu [BJDCTF 2nd]燕言燕语-y1ng
  2. linux kernel进程切换(寄存器保存与恢复)
  3. 使数据区“可执行”的几种常规办法
  4. angr学习笔记(6)(内存地址单元符号化)
  5. Windows驱动开发学习笔记(六)—— Inline HOOK
  6. 登录MySQL数据库
  7. 【Linux 】使用 Shell 批量重命名文件名称
  8. 图的邻接矩阵存储和邻接表存储定义方法
  9. Codeforces Round #479 (Div. 3)【完结】
  10. Acwing第 36 场周赛【完结】