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

  • 1.虚函数指针和虚函数表
  • 2.继承中的虚函数表
    • 2.1单继承中的虚函数表
    • 2.2多继承中的虚函数表
  • 3.多态的原理
  • 4.总结

1.虚函数指针和虚函数表

以下代码:
问类实例化出的对象占几个字节?

#include<iostream>
using namespace std;
class A {int m_a;
public:void func() {cout << "调用类A的func()函数" << endl;}
};
int main() {A a;cout <<"sizeof(a):"<<sizeof(a) << endl;system("pause");return 0;
}

结果显而易见 sizeof(a)=4,因为成员函数存放在公共的代码段, 所以只计算成员变量m_a(int型)所占字节的大小。


当我们将成员函数定义为虚函数时,结果却出现了不同的情况:

#include<iostream>
using namespace std;
class A {int m_a;
public:virtual void func() {cout << "调用类A的func()函数" << endl;}
};
int main() {A a;cout <<"sizeof(a):" <<sizeof(a) << endl;system("pause");return 0;
}



我们注意到当成员函数定义为虚函数时,同一个类的实例化对象大小变为了8个字节。多出来的4个字节是怎么回事呢?
另外在对象a中还多出了一个void**类型名为_vfptr的变量。它是一个二级指针, 指针在32位平台中占4字节, 所以这里的结果是8(m_a的4字节+_vfptr的4字节), 那么_vfptr到底是个什么东西? 类中有了虚函数之后才有了_vfptr, 它们之间到底有着什么联系?

当一个类中有虚函数时,编译期间就会为这个类分配一片连续的内存 (虚表vftable),来存放虚函数的地址。类中只保存着指向虚表的指针 (虚函数表指针_vfptr) ,当这个类实例出对象时,每个对象都会有一个虚函数表指针_vfptr 。虚函数其实和普通函数一样,存放在代码段。


一个含有虚函数的类中都至少都有一个虚函数表,因为虚函数的地址要被放到虚函数表中,那么派生类中这个表放了些什么呢?我们接着往下分析。

针对上面的代码我们做出以下改造:

1.我们增加一个派生类去继承基类
2.基类中重写Func1
3.派生类再增加一个虚函数Func2和一个普通函数Func3

#include<iostream>
using namespace std;
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中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在的部分,的另一部分是自己的成员。
  2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1()完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖, 覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  3. Func2()继承下来后是虚函数,所以放进了虚表,Func3()也继承下来了,但是不是虚函数,所以不会放进虚表。
  4. 虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。
  5. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
  6. 虚函数存在哪的?虚表存在哪的?
    答:虚函数存在虚表,虚表存在对象中。上面的回答是错的。注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现在vs下是存在代码段的。

总结:

当一个类中有虚函数时, 在编译期间,就会为这个类分配一片连续的内存 (虚表vftable), 来存放虚函数的地址, 类中只保存着指向虚表的指针 (虚函数指针_vfptr) , 虚函数其实和普通函数一样, 存放在代码段。当这个类实例出对象时, 每个对象都会有一个虚函数表指针_vfptr 。虚表本质上是一个在编译时就已经确定好了的void* 类型的指针数组 。
注意 : 虚函数表为了标志结尾, 会在虚表最后一个元素位置保存一个空指针。所以看到的虚表元素个数比实际虚函数个数多一个。

2.继承中的虚函数表

在有虚函数的类被继承后, 虚表也会被拷贝给派生类。编译器会给派生类新分配一片空间来拷贝基类的虚表, 将这个虚表的指针给派生类, 而并不是沿用基类的虚表。在发生虚函数的重写时, 重写的是派生类为了拷贝基类虚表新创建的虚表中的虚函数地址。 虚表为所有这个类的对象所共享,是通过给每个对象一个虚表指针_vfptr共享到的虚表。

2.1单继承中的虚函数表

  1. 单继承中未重写虚函数: 会继承基类的虚表, 如果派生类中新增了虚函数, 则会加继承的虚表后面。
  2. 单继承中重写虚函数: 继承的虚表中被重写的虚函数地址会在继承虚表时被修改为派生类函数的地址。(注意: 此时基类的虚表并没有被修改, 修改的是派生类自己的虚表)

所以, 重写实际上就是在继承基类虚表时, 把基类的虚函数地址修改为派生类虚函数的地址。

#include<iostream>
using namespace std;class Base {int m_a;
public:virtual void func() {cout << "类A的func" << endl;}virtual int func1() {cout << "类A的func1" << endl;return 0;}
};
class Derive :public Base {public:virtual void func() {cout << "类B的func" << endl;}virtual void func2() {cout << "类B的func2" << endl;}
};
int main() {Base a1;Base a2;Derive b;system("pause");return 0;
}

基类对象a1,a2中,虚表中的地址相同(虚函数func()和func1()的地址),是因为虚表为类的所有对象共享,是通过给每个对象一个虚表指针_vfptr共享到的虚表。
派生类对象b,继承了基类的虚表,虚函数指针_vptr却和a1,a2的不同,这是因为编译器新分配了一片空间来拷贝基类的虚表。派生类中重写了虚函数func(),由于被重写的虚函数地址会在继承虚表时被修改为派生类函数的地址。所以派生类的虚表中func()的地址被改变了。

我们还发现,派生类中的虚函数func2()却没有出现在派生类中的虚表中。按理来说, 如果派生类中新增了虚函数, 则会加继承的虚表后面。其实这个虚函数地址是存在的,我们可以发现箭头所指的虚函数表vftable[4],其中应该有四个元素,除去虚表中多出的一个空指针,还有另外三个func(),func1(),func2(),只不过这里没有显示func2()。我们可以通过调用监视窗口来查看func2()。

2.2多继承中的虚函数表

  1. 多继承中不重写虚函数: 继承的多个基类中有多张虚表, 派生类会全部拷贝下来, 成为派生类的多张虚表, 如果派生类有新的虚函数, 会加在派生类拷贝的第一张虚表的后面(拷贝的第一张虚表是继承的第一个有虚函数或虚表的基类的)
  2. 多继承中重写虚函数 : 规则与不重写虚函数相同, 但需要注意的是, 如果多个基类中含有相同的虚函数, 例如func(),当派生类重写func()这个虚函数后, 所有含有这个函数的基类虚表都会被重写 (改的是派生类自己拷贝的基类虚表, 并不是基类自己的虚表)
#include<iostream>
using namespace std;
class Base {int m_a;
public:virtual void funcA() {cout << "基类Base的funcA()" << endl;}virtual void func() {cout << "基类Base的func()" << endl;}
};
class Base2 {public:virtual void funcB() {cout << "基类Base2的funcB()" << endl;}virtual void func() {cout << "基类Base2的func()" << endl;}
};
class Derive :public Base, public Base2 {public:virtual void func() {cout << "派生类重写的func()" << endl;}virtual void funcC() {cout << "派生类中新增的虚函数funcC()" << endl;}
};
int main() {Derive d;Base2 c;Base a;system("pause");return 0;
}

派生类继承了两张虚表。派生类中重写了func()函数,派生类自己拷贝基类虚表中含func()的地址都被改变了。

第一张虚表vftable[4],说明其中含有四个元素,除了funcA()、func()、多出来的一个空指针外,还有派生类中新的虚函数funcC()。调用监视窗口可以看到,派生类中新增的虚函数funcC()被加在了派生类拷贝基类的第一张虚表的后面。

3.多态的原理

多态的构成条件:

  1. 通过基类对象的指针或者引用调用虚函数
  2. 基类中必须包含虚函数,并且派生类中一定要对基类中的虚函数进行重写。

原理: 利用虚函数可以重写的特性, 当一个有虚函数的基类有多个派生类时, 通过各个派生类对基类虚函数的不同重写, 实现指向派生类对象的基类指针或基类引用调用同一个虚函数, 去实现不同功能的特性。抽象来说就是, 为了完成某个行为, 不同的对象去完成时会产生多种不同的状态。

4.总结

  1. 当一个类中有虚函数时, 在编译期间,就会为这个类分配一片连续的内存 (虚表vftable), 来存放虚函数的地址。
  2. 对象中存放的是虚函数指针_vfptr,并非虚表。_vptr是虚表的首地址,指向虚表。
  3. 虚表中存放的是虚函数地址,不是虚函数。虚函数和普通函数一样存放在代码段。
  4. 虚表是在编译阶段生成的,一般存放在代码段中。
  5. 虚表本质上是一个在编译时就已经确定好的void* 类型的指针数组 。
  6. 派生类的虚表生成:
    单继承:
    ①先将基类中的虚表内容拷贝一份到派生类虚表中。
    ②如果派生类重写了基类中某个虚函数,派生类中的虚函数地址替换虚表中基类的虚函数地址。
    ③派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
    多继承:
    ①继承的多个基类中有多张虚表, 派生类会全部拷贝下来, 成为派生类的多张虚表 。
    ②如果派生类重写了基类中的某个虚函数,所有含有这个函数的基类虚表都会被重写 (改的是派生类自己拷贝的基类虚表, 并不是基类自己的虚表)。
    ③派生类自己新增加的虚函数加在派生类拷贝的第一张虚表后

C++多态的原理(虚函数指针和虚函数表)相关推荐

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

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

  2. 【C++】虚函数指针和虚函数列表

    本篇文章主要来讲述,C++多态的实现原理,也就是虚函数和虚函数列表是怎么回事?它们是如何实现多态的? 虚函数概述: 首先,C++多态的实现是通过关键字virtual,有了这个关键字之后,通过继承的关系 ...

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

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

  4. 【虚函数指针 虚函数表】

    文章目录 虚函数指针和虚函数表 1.虚函数的含义 2.虚函数的作用 3.虚函数的实现原理 多态的实现原理 `普通类` `当类中存在虚函数` `子类继承父类不重写虚函数` 子类继承父类重写虚函数 1.虚 ...

  5. C++经典问题_06 虚函数和纯虚函数以及多态

    文章目录 一. 虚函数和纯虚函数以及多态的概念 二. 虚函数详解 ① 虚函数的定义 ② 虚函数和普通函数重写时候的区别 ③ 虚函数的常见错误 ④ 如何避免虚函数的使用错误 三. 虚函数指针和虚函数表 ...

  6. c/c++入门教程 - 2.4.7 多态、函数地址晚绑定(重写,虚函数,纯虚函数,抽象类,虚析构,纯虚析构)

    目录 4.7 多态 4.7.1 多态的基本概念(超级重要) 4.7.2 多态的原理刨析(超级重要) 4.7.2 多态案例一:计算器类 4.7.3 纯虚函数和抽象类 4.7.4 多态案例二 - 制作饮品 ...

  7. C++虚函数表(vtable)和虚函数指针(vfptr)

    编译器会构建一张虚表( vtable ),每一个类都有自己独特的虚表.同时,在这个继承链上,编译器会为基类插入一个隐式的指针(一般是对象的首地址),指向虚表,称为__vptr.然后,子类继承父类时,会 ...

  8. C++_虚继承_虚函数_纯虚函数(多继承的二义性,多态)

    基本信息 每一个类都有一个虚表,以及虚表指针; 虚表的内容是编译器决定的,虚表中用于存放虚函数的指针, 程序运行时的类型信息等; 每个多态对象都存放着一个指向当前类型的虚表的指针, 该指针在构造函数中 ...

  9. C++ 多态 虚函数与纯虚函数

    C++ 多态 虚函数与纯虚函数 虚函数是C++重要思想-多态中不可或缺的一个知识点与用法,但初学者一般很难理解,在这里用通俗语言介绍一下. 百度百科: 在某基类中声明为 virtual 并在一个或多个 ...

最新文章

  1. AJAX跨域访问解决方案
  2. html5指南针源码,《绝秘奉献》——最新超短线指南针!源码已放!(贴图 原码)...
  3. SM01 事务代码的加锁以及解锁
  4. h5页面长按保存图片
  5. android自定义task,Android Gradle 自定义Task 详解
  6. android 机器人动画,Android 5.X与Android4.X版本机器人动画的区别以及制作动画的方法...
  7. 微软故障转移群集服务器要求,故障转移群集概述
  8. BW Delta (增量)更新方法 .
  9. 智能优化算法:黄金正弦算法 -附代码
  10. 天野学院易语言模拟脚本制作
  11. easypoi 语法_【语法微课合集】50堂英语语法微课,视频课件免费下载,建议收藏或转发朋友圈...
  12. 同学聚会幽默 , 小学生搞笑图片
  13. linux mantis安装包,Linux下安装mantis
  14. 小米5测试机软件,小米手机硬件检测软件
  15. 股票交易接口有什么优势?
  16. LZJ的Python第二次打卡
  17. ps2021神经ai滤镜无法使用,ps2021神经滤镜出现错误
  18. 安保巡逻机器人如何在公共场所提供安全应用?
  19. NLP之文本分词综述
  20. iOS Objective-C NSDateFormatter的一些使用参考

热门文章

  1. 激光雷达学习笔记-------Ubuntu 18.04 + 思岚科技 A1M8+ ROS 上手使用及基于hector_slam 建图
  2. 【转】unity 的 MonoDevelop 在Windows下显示中文不出现乱码
  3. Postgresql从MD5密码验证改为SCRAM-SHA-256
  4. 爬虫:python爬取简历模板
  5. 利用wxpython库来制作真心话大冒险小程序
  6. 请输入汉字、英文、数字或其组合/请输入6-18位字符,同时包含英文字母、数字、下划线,并且以英文字母开头/8-16位数字、字母、特殊字符组合
  7. 挖个冰块就能修自己!用「冰」做的科考机器车
  8. 下一个可扩展性障碍:大型多玩家手机AR
  9. 字符串函数 tirm 的作用
  10. oracle fx成立时间,oracle 日期格式FM/FX和日期后缀SP/TH/SPTH/THSP