C++虚函数 多态 虚表 动态联编 基本原理

虚函数的实现基本原理


首先要明白的一点是 多态是什么
多态就是用父类的指针 指向子类的实例,也就是泛型技术的一种

泛型技术 就是试图用不变的代码 实现可变的算法

而泛型技术,要么在编译的时绑定,要么在运行时绑定 C++的多态 也可以从这两个方向入手 静态多态 动态多态

静态多态

编译的时候的多态 通过重载 模板实现

动态多态

绑定的时候在运行的时候绑定 通过虚函数实现

虚函数通过一张虚函数表来实现的,也就是虚表

虚表里有什么

虚表里有什么呢? 存的主要是一个类的虚函数的地址 。这个表用来解决继承,覆盖的问题,保证其能指向真实存在的函数

所以当你实例化一个类时,如果这个类有虚函数,那么这表就会分配在这个实例的内存里

那么虚函数表是什么创建的?编译阶段,编译阶段就已经填充好了虚函数表里的内容, 可是为什么又说是在运行的时候确定的呢?因为vptr 虚函数指针是在实例对象后出现的,运行的时候,就是确定这个vptr的走向 所以表指针是存在于这个实例的内存里

如此依赖,用父类指针操作一个子类,虚函数表就由此体现了。 通过这个表 指明应该调用的函数。

那么,一个对象A,在内存中的分布是什么样的呢?

内存分布

定义一个对象, A*a=new A 他的分布长这样:

main函数里,栈帧上,要有一个A类型的指针,指向堆里分配好的对象A实例

A实例的头部 是一个vtable指针,紧接着是A对象按照声明顺序排列的成员变量

vtable指针 指向的是代码段A类型的虚函数表 的 第一个虚函数的起始地址

(为什么要强调是第一个虚函数的起始地址呢? 虚函数表有一个头部,紧接着才是按照声明顺序排列的虚函数)

(再去看看虚函数表里有什么 有两个析构函数,这是因为对象的构造方式 有两种,栈构造 堆构造 两种析构方式 栈内存的析构不需要delete 还有一个typeinfo,存储A的类基础信息 ,包括父类与类的名称,C++关键字 typeid 返回的就是这个对象 typeinfo也是一个类,没有父类的A来说,tinfo是class type info类型 )

综上,虚函数表不是单独存在,而是虚表的一部分
虚表里

  • 会有虚拟继承
  • 会有对象起始地址的偏移值
  • 会有RTTI information 对象指针
  • 会有虚函数表 存放虚函数指针列表

虚表的存在只是虚拟函数的50%,还需要能够指出每个对象的对应vtbl,这就是virtual table pointer 建立这种联系 每个声明了虚函数的对象 都有它,他是一个看不见的数据成员,指向对应类的虚表 这个数据成员 是vptr,编译器把它加载对象里,位置只有编译器知道

虚函数的调用过程

调用一个虚函数 通过对象内存中的vptr 找到虚表,通过虚表找到虚函数的实现区域来调用

被执行的代码 必须和调用函数对象的动态类型相一致。

编译器就是要探求如何高效提供这种特性

大多数编译对象 都是 虚表+虚表指针实现 类声明了虚函数 继承了虚函数 ,就会有一个虚表,虚表关键在于有一个函数指针数组

(链表?数组 无伤大雅)

这个数组里每个元素都是函数指针,都指向这个类的虚函数 ,同时该类的每个对象 都有一个vptr,指向虚表的地址

那么 如果继承呢?

  • 子类的虚函数表 先把父类的虚函数放在前面,再放自己的虚函数指针 ,这就是覆盖
  • 多继承的情况下,每个父类都有一个虚表,子成员函数放在第一个父类的表中,多继承的情况下,实例对象的内存结构 不只是存在一个虚函数表的指针 有几个基类中有虚函数,就会有几个虚函数表指针

虚函数性能

虚函数调用的过程:

  • 通过对象的vptr找到vtbl 因为编译器知道在对象内哪里能找到vptr嘛,编译器放置的 那这步的代价,其实也就是偏移调整

  • 然后找对应vtbl内指向被调用函数的指针
    这步也简单,编译器内为每个虚函数 分配指针,这步也就是进行一个偏移

  • 然后呢?

    单继承的情况下,虚函数的代价和基本的非虚函数调用一样 而多继承的情况下,会根据多个父类生成多个vptr,这样对象查找vptr的操作会复杂 但不是直接影响

    虚函数运行的代价是 虚函数不能内联函数

    为啥呢? 为啥不能内联 内联函数 需要在编译的时候确定被调用的函数体来替代函数调用的指令,而虚函数的虚 是指 直到具体调用才能知道被调用的是哪个函数

    所以 没法在编译的时候进行内联函数的展开

    如果通过对象直接调用虚函数 当然可以内联,但大多数的虚函数是通过对象的指针 或引用 被调用的 所以就不能内联咯

空间占用

多态机制的实现,需要给类建立一个虚函数表,所以虚函数的代价:

  • 增加类的体积

一个虚函数表的体积,相当于几个函数指针的体积

如果有大量的类,那么vtable就会占用大量的地址空间,而且类的继承,如果只重写一小部分,那么就会有大量重复的内容,浪费地址空间

拓展

静态联编 动态联编

笔者记得之前看到过动态联编和静态联编,这是什么呢

其实如果你没有采用指针和引用的方式来使用多态的话,编译器在编译的阶段就能确定你编译的是啥了,也就是可以转成内联函数来理解的。这里就是静态联编

而采用指针和引用利用多态,就会是动态联编,可以去看代码

reference

jacktang816.github.io/post/virtua…

cloud.tencent.com/developer/a…

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值