当前位置: 首页 > 编程日记 > 正文

虚函数与虚继承寻踪

封装、继承、多态是面向对象语言的三大特性,熟悉C++的人对此应该不会有太多异议。C语言提供的struct,顶多算得上对数据的简单封装,而C++的引入把struct“升级”为class,使得面向对象的概念更加强大。继承机制解决了对象复用的问题,然而多重继承又会产生成员冲突的问题,虚继承在我看来更像是一种“不得已”的解决方案。多态让对象具有了运行时特性,并且它是软件设计复用的本质,虚函数的出现为多态性质提供了实现手段。

如果说C语言的struct相当于对数据成员简单的排列(可能有对齐问题),那么C++的class让对象的数据的封装变得更加复杂。所有的这些问题来源于C++的一个关键字——virtual!virtual在C++中最大的功能就是声明虚函数和虚基类,有了这种机制,C++对象的机制究竟发生了怎样的变化,让我们一起探寻之。

为了查看对象的结构模型,我们需要在编译器配置时做一些初始化。在VS2010中,在项目——属性——配置属性——C/C++——命令行——其他选项中添加选项“/d1reportAllClassLayout”。再次编译时候,编译器会输出所有定义类的对象模型。由于输出的信息过多,我们可以使用“Ctrl+F”查找命令,找到对象模型的输出。

一、基本对象模型

首先,我们定义一个简单的类,它含有一个数据成员和一个虚函数。

复制代码
class MyClass
{
    int var;
public:
    virtual void fun()
    {}
};
复制代码

编译输出的MyClass对象结构如下:

复制代码
1>  class MyClass    size(8):
1>      +---
1>   0    | {vfptr}
1>   4    | var
1>      +---
1>  
1>  MyClass::$vftable@:
1>      | &MyClass_meta
1>      |  0
1>   0    | &MyClass::fun
1>  
1>  MyClass::fun this adjustor: 0
复制代码

从这段信息中我们看出,MyClass对象大小是8个字节。前四个字节存储的是虚函数表的指针vfptr,后四个字节存储对象成员var的值。虚函数表的大小为4字节,就一条函数地址,即虚函数fun的地址,它在虚函数表vftable的偏移是0。因此,MyClass对象模型的结果如图1所示。

图1 MyClass对象模型

MyClass的虚函数表虽然只有一条函数记录,但是它的结尾处是由4字节的0作为结束标记的。

adjust表示虚函数机制执行时,this指针的调整量,假如fun被多态调用的话,那么它的形式如下:

*(this+0)[0]()

总结虚函数调用形式,应该是:

*(this指针+调整量)[虚函数在vftable内的偏移]()

二、单重继承对象模型

我们定义一个继承于MyClass类的子类MyClassA,它重写了fun函数,并且提供了一个新的虚函数funA。

复制代码
class MyClassA:public MyClass
{
    int varA;
public:
    virtual void fun()
    {}
    virtual void funA()
    {}
};
复制代码

它的对象模型为:

复制代码
1>  class MyClassA    size(12):
1>      +---
1>      | +--- (base class MyClass)
1>   0    | | {vfptr}
1>   4    | | var
1>      | +---
1>   8    | varA
1>      +---
1>  
1>  MyClassA::$vftable@:
1>      | &MyClassA_meta
1>      |  0
1>   0    | &MyClassA::fun
1>   1    | &MyClassA::funA
1>  
1>  MyClassA::fun this adjustor: 0
1>  MyClassA::funA this adjustor: 0
复制代码

可以看出,MyClassA将基类MyClass完全包含在自己内部,包括vfptr和var。并且虚函数表内的记录多了一条——MyClassA自己定义的虚函数funA。它的对象模型如图2所示。

图2 MyClassA对象模型

我们可以得出结论:在单继承形式下,子类的完全获得父类的虚函数表和数据。子类如果重写了父类的虚函数(如fun),就会把虚函数表原本fun对应的记录(内容MyClass::fun)覆盖为新的函数地址(内容MyClassA::fun),否则继续保持原本的函数地址记录。如果子类定义了新的虚函数,虚函数表内会追加一条记录,记录该函数的地址(如MyClassA::funA)。

使用这种方式,就可以实现多态的特性。假设我们使用如下语句:

MyClass*pc=new MyClassA;
pc->fun();

编译器在处理第二条语句时,发现这是一个多态的调用,那么就会按照上边我们对虚函数的多态访问机制调用函数fun。

*(pc+0)[0]()

因为虚函数表内的函数地址已经被子类重写的fun函数地址覆盖了,因此该处调用的函数正是MyClassA::fun,而不是基类的MyClass::fun。

如果使用MyClassA对象直接访问fun,则不会出发多态机制,因为这个函数调用在编译时期是可以确定的,编译器只需要直接调用MyClassA::fun即可。

三、多重继承对象模型

和前边MyClassA类似,我们也定义一个类MyClassB。

复制代码
class MyClassB:public MyClass
{
    int varB;
public:
    virtual void fun()
    {}
    virtual void funB()
    {}
};
复制代码

它的对象模型和MyClassA完全类似,这里就不再赘述了。

为了实现多重继承,我们再定义一个类MyClassC。

复制代码
class MyClassC:public MyClassA,public MyClassB
{
    int varC;
public:
    virtual void funB()
    {}
virtual void funC()
    {}
};
复制代码

为了简化,我们让MyClassC只重写父类MyClassB的虚函数funB,它的对象模型如下:

复制代码
1>  class MyClassC    size(28):
1>      +---
1>      | +--- (base class MyClassA)
1>      | | +--- (base class MyClass)
1>   0    | | | {vfptr}
1>   4    | | | var
1>      | | +---
1>   8    | | varA
1>      | +---
1>      | +--- (base class MyClassB)
1>      | | +--- (base class MyClass)
1>  12    | | | {vfptr}
1>  16    | | | var
1>      | | +---
1>  20    | | varB
1>      | +---
1>  24    | varC
1>      +---
1>  
1>  MyClassC::$vftable@MyClassA@:
1>      | &MyClassC_meta
1>      |  0
1>   0    | &MyClassA::fun
1>   1    | &MyClassA::funA
1>   2    | &MyClassC::funC
1>  
1>  MyClassC::$vftable@MyClassB@:
1>      | -12
1>   0    | &MyClassB::fun
1>   1    | &MyClassC::funB
1>  
1>  MyClassC::funB this adjustor: 12
1>  MyClassC::funC this adjustor: 0
复制代码

和单重继承类似,多重继承时MyClassC会把所有的父类全部按序包含在自身内部。而且每一个父类都对应一个单独的虚函数表。MyClassC的对象模型如图3所示。

图3 MyClassC对象模型

多重继承下,子类不再具有自身的虚函数表,它的虚函数表与第一个父类的虚函数表合并了。同样的,如果子类重写了任意父类的虚函数,都会覆盖对应的函数地址记录。如果MyClassC重写了fun函数(两个父类都有该函数),那么两个虚函数表的记录都需要被覆盖!在这里我们发现MyClassC::funB的函数对应的adjust值是12,按照我们前边的规则,可以发现该函数的多态调用形式为:

*(this+12)[1]()

此处的调整量12正好是MyClassB的vfptr在MyClassC对象内的偏移量。

四、虚拟继承对象模型

虚拟继承是为了解决多重继承下公共基类的多份拷贝问题。比如上边的例子中MyClassC的对象内包含MyClassA和MyClassB子对象,但是MyClassA和MyClassB内含有共同的基类MyClass。为了消除MyClass子对象的多份存在,我们需要让MyClassA和MyClassB都虚拟继承于MyClass,然后再让MyClassC多重继承于这两个父类。相对于上边的例子,类内的设计不做任何改动,先修改MyClassA和MyClassB的继承方式:

class MyClassA:virtual public MyClass
class MyClassB:virtual public MyClass
class MyClassC:public MyClassA,public MyClassB

由于虚继承的本身语义,MyClassC内必须重写fun函数,因此我们需要再重写fun函数。这种情况下,MyClassC的对象模型如下:

复制代码
1>  class MyClassC    size(36):
1>      +---
1>      | +--- (base class MyClassA)
1>   0    | | {vfptr}
1>   4    | | {vbptr}
1>   8    | | varA
1>      | +---
1>      | +--- (base class MyClassB)
1>  12    | | {vfptr}
1>  16    | | {vbptr}
1>  20    | | varB
1>      | +---
1>  24    | varC
1>      +---
1>      +--- (virtual base MyClass)
1>  28    | {vfptr}
1>  32    | var
1>      +---
1>  
1>  MyClassC::$vftable@MyClassA@:
1>      | &MyClassC_meta
1>      |  0
1>   0    | &MyClassA::funA
1>   1    | &MyClassC::funC
1>  
1>  MyClassC::$vftable@MyClassB@:
1>      | -12
1>   0    | &MyClassC::funB
1>  
1>  MyClassC::$vbtable@MyClassA@:
1>   0    | -4
1>   1    | 24 (MyClassCd(MyClassA+4)MyClass)
1>  
1>  MyClassC::$vbtable@MyClassB@:
1>   0    | -4
1>   1    | 12 (MyClassCd(MyClassB+4)MyClass)
1>  
1>  MyClassC::$vftable@MyClass@:
1>      | -28
1>   0    | &MyClassC::fun
1>  
1>  MyClassC::fun this adjustor: 28
1>  MyClassC::funB this adjustor: 12
1>  MyClassC::funC this adjustor: 0
1>  
1>  vbi:       class  offset o.vbptr  o.vbte fVtorDisp
1>           MyClass      28       4       4 0
复制代码

虚继承的引入把对象的模型变得十分复杂,除了每个基类(MyClassA和MyClassB)和公共基类(MyClass)的虚函数表指针需要记录外,每个虚拟继承了MyClass的父类还需要记录一个虚基类表vbtable的指针vbptr。MyClassC的对象模型如图4所示。

图4 MyClassC对象模型

虚基类表每项记录了被继承的虚基类子对象相对于虚基类表指针的偏移量。比如MyClassA的虚基类表第二项记录值为24,正是MyClass::vfptr相对于MyClassA::vbptr的偏移量,同理MyClassB的虚基类表第二项记录值12也正是MyClass::vfptr相对于MyClassA::vbptr的偏移量。

和虚函数表不同的是,虚基类表的第一项记录着当前子对象相对与虚基类表指针的偏移。MyClassA和MyClassB子对象内的虚表指针都是存储在相对于自身的4字节偏移处,因此该值是-4。假定MyClassA和MyClassC或者MyClassB内没有定义新的虚函数,即不会产生虚函数表,那么虚基类表第一项字段的值应该是0。

通过以上的对象组织形式,编译器解决了公共虚基类的多份拷贝的问题。通过每个父类的虚基类表指针,都能找到被公共使用的虚基类的子对象的位置,并依次访问虚基类子对象的数据。至于虚基类定义的虚函数,它和其他的虚函数的访问形式相同,本例中,如果使用虚基类指针MyClass*pc访问MyClassC对象的fun,将会被转化为如下形式:

*(pc+28)[0]()

通过以上的描述,我们基本认清了C++的对象模型。尤其是在多重、虚拟继承下的复杂结构。通过这些真实的例子,使得我们认清C++内class的本质,以此指导我们更好的书写我们的程序。本文从对象结构的角度结合图例为大家阐述对象的基本模型,和一般描述C++虚拟机制的文章有所不同。作者只希望借助于图表能把C++对象以更好理解的形式为大家展现出来,希望本文对你有所帮助。

相关文章:

信息记录拉取失败_天猫入驻为什么失败?猫店侠做详细解读

天猫入驻为什么失败?这是很多商家都想要知道的一件事情,猫店侠想说其实这也很正常,只要不是一味盲目的入驻,就还有机会。首先失败商家要看看失败的反馈内容,看看是哪方面不达标,再着重进行补充,…

Linux查看目录挂载点

用命令 df 即可 # df /var/lib/ Filesystem 1K-blocks Used Available Use% Mounted on /dev/sda3 135979984 66905292 62055896 52% /加上-kh更容易看些: # df /var/lib/ -kh Filesystem Size Used Avail Use% Mounted o…

Android:你好,androidX!再见,android.support

190325 补充:莫名问题的解决 181106 补充:修改未迁移成功的三方库 1、AndroidX简介 点击查看Android文档中对androidx的简介 按照官方文档说明 androidx 是对 android.support.xxx 包的整理后产物。由于之前的support包过于混乱,所以&#xf…

设计模式:简单工厂、工厂方法、抽象工厂之小结与区别

简单工厂,工厂方法,抽象工厂都属于设计模式中的创建型模式。其主要功能都是帮助我们把对象的实例化部分抽取了出来,优化了系统的架构,并且增强了系统的扩展性。 本文是本人对这三种模式学习后的一个小结以及对他们之间的区别的理解…

云计算和大数据时代网络技术揭秘(八)数据中心存储FCoE

数据中心存储演化——FCoE数据中心三大基础:主机 网络 存储在云计算推动下,存储基础架构在发生演变传统存储结构DAS、SAN在发展中遇到了布线复杂、能耗增多的缺点(原生性),需要对架构做根本的改变。FCoE是业界无可争议…

在plsql里面怎么去掉空行_盐渍樱花怎么做?详细做法告诉您,一年都不会坏,学会再也不用买...

盐渍樱花怎么做?详细做法告诉您,一年都不会坏,赶紧收藏学会它!樱花季说的就是现在,虽然到了飘落的季节,但是还是到处可见的樱花朵朵。俗话说:花无百日红。真的是啊,每年的三四月是最…

Linux基础知识——常用shell命令介绍(三)

一、改变文件权限 chmod:change mode 语法:# chmod [选项-option] 权限 FILE 选项:-R 递归修改权限 --reference 参照文件或目录给予权限 权限定义方式: 1.同时修改三类用户的权限: 8进制数字方式 # chmod 666 /abc /*将/abc的权限改为…

免费版CloudFlare CDN基本设置参考

CDN有很多,网上都有介绍,用户比较多的CloudFlare CDN大家都知道,配置起来也比较简单,合理的配置,才能提升网站的速度和网站安全。不同的网站需求配置不一样,以下是我的配置情况,仅供参考。 我网…

从Java类库看设计模式

//From http://www.uml.org.cn/j2ee/201010214.asp 很多时候,对于一个设计来说(软件上的,建筑上的,或者它他工业上的),经验是至关重要的。好的经验给我们以指导,并节约我们的时间;坏的经验则给我们以借鉴,可…

method=post 怎么让查看源代码看不到_网站文档不能复制怎么办?教你3个小妙招,1分钟轻松化解...

不知道大家平常在查找资料时,碰到网页资料不能下载时,是怎么样进行处理的。那么笔者今天就来分享我查找不能复制文档时,所用的3个小妙招,帮助轻松化解,一起来看看吧。1、保存网页当我们遇到一个不能直接复制的文档&…

Missing number

题目链接:http://acm.hust.edu.cn/vjudge/problem/visitOriginUrl.action?id114468 题目大意: 多组案例T,每个案例含n2个数据,这n2个数据构成一组有序列,现在已知这组数据中的n个,请找出缺失的两个数据。 …

[leetcode]Surrounded Regions @ Python

原题地址:https://oj.leetcode.com/problems/surrounded-regions/ 题意: Given a 2D board containing X and O, capture all regions surrounded by X. A region is captured by flipping all Os into Xs in that surrounded region. For example, X X …

Android Drawable 详解(教你画画!)

参考 1、Android中的Drawable基础与自定义Drawable2、android中的drawable资源3、Android开发之Shape详细解读 Drawable分类 Noxml标签Class类含义1shapeShapeDrawable特定形状,模型的图样2selectorStateListDrawable不同状态选择不同的图样3layer-listLayerDrawabl…

Carrier frequency 和 EARFCN的关系

Carrier frequency 和 EARFCN的关系 我们处理UE log时,看到LTE cell 都是用EARFCN/PCI来标示的,那么EARFCN和frequency 之间是什么关系呢? 1. EARFCN: 缩写: E-UTRA Absolute Radio Frequency Channel Number, 取值范围: 0…

十五天精通WCF——第六天 你必须要了解的3种通信模式

十五天精通WCF——第六天 你必须要了解的3种通信模式 原文:十五天精通WCF——第六天 你必须要了解的3种通信模式wcf已经说到第六天了,居然还没有说到这玩意有几种通信模式,惭愧惭愧,不过很简单啦,单向,请求-响应&#…

从未在一起更让人遗憾_明明是真爱,却又不能在一起

深爱一个人,若是无缘成为夫妻相偎相依在一起,在分开的很长一段时间里,一定会在深夜难眠,在梦中哭醒,因为你太爱他,太想他。人生那么长,在我们的一生中,总会有一个人,在你…

使用 IntraWeb (8) - 系统模板

我们可以自定义系统错误模板, 编辑 IWError.html 放到模板文件夹后, 它将替换默认的模板.{在主页面, 这是要模拟一个系统错误} procedure TIWForm1.IWButton1Click(Sender: TObject); beginRelease; end;修改前后的 IWError.html 对比:我想办法抠出了 IWError.html 源文件, 从里…

Java中常量定义的几种方式

编程中使用常量的优点: 常量提取出来有利于代码阅读,而且下次再做这种判断不用手写或复制并且提高代码的复用率,方便修改,直接通过常量类就能得到。不过我觉得提取出来并不会有利于代码性能提升,因为常量分配在内存的常…

PCT-36.523

LTE v 36 523-1:Part 1: Protocol conformance specification 定义了每个Case 运行的流程 v 36.523-2:Part 2: Implementation Conformance Statement(ICS) proforma specification. Table 4-1 定义了每个Case的详细背景: 1. 最早那个release 引入的这一c…

测试笔试题之相关概念

1、对手机软件的压力测试通常包括: (1)存储压力 (2)响应能力压力 (3)网络流量压力 (4)边界压力 2、针对手机应用软件的系统测试,我们通常从如下几个角度开展&…

ios 代码设置控件宽高比_用宽高比调整UIImage的大小?

我知道这很老了,但是感谢那篇文章-它使我从尝试使用比例尺重定向到绘制图像。万一对任何人都有利,我做了一个扩展类,我将在这里进行介绍。它允许您调整图像的大小,如下所示:UIImage imgNew img.Fit(40.0f, 40.0f);我不…

jquery入门 修改网页背景颜色

我们在浏览一些网站&#xff0c;尤其是一些小说网站的时候&#xff0c;都会有修改页面背景颜色的地方&#xff0c;这个功能使用jquery很容易实现。 效果图&#xff1a; show you code: <!doctype html> <html> <head> <meta charset"utf-8">…

对于装饰器Decorator的理解

装饰器是用来描述函数&#xff0c;记录日志&#xff0c;提供信息的函数&#xff0c;是一个为了更好的服务主函数的副函数&#xff1a; 详情还需查看&#xff1a;廖雪峰装饰器 关键在于&#xff1a;【import functools是导入functools模块。模块的概念稍候讲解。现在&#xff0c…

UE的注册流程

协议36.508 4.5节 有个表格写的很清楚&#xff1a; Table 4.5.2.3-1: UE registration procedure (state 1 to state 2)

五大主流数据库模型

转载自 五大主流数据库模型 导读&#xff1a;无论是关系型数据库还是非关系型数据库&#xff0c;都是某种数据模型的实现。本文将为大家简要介绍5种常见的数据模型&#xff0c;让我们来追本溯源&#xff0c;窥探现在流行的数据库解决方案背后的神秘世界。 什么是数据模型&#…

laytpl语法_layui语法基础

一.按钮区分​ 1.按照主题划分​ 原始&#xff1a;class "layui-btn layui-btn-primary"​ 默认&#xff1a;class "layui-btn"​ 百搭&#xff1a;class "layui-btn layui-btn-normal"​ 暖色&#xff1a;class "layui-btn layui-btn-…

黑马程序员-张老师基础加强3-内省

内省&#xff1a;javaBean JavaBean是一种特殊的Java类&#xff0c;主要用于传递数据信息&#xff0c;这种java类中的方法主要用于访问私有的字段&#xff0c;且方法名符合某种命名规则。 JavaBean的属性是根据其中的setter和getter方法来确定的&#xff0c;而不是根据其中的成…

shell 中长命令的换行处理

考察下面的脚本&#xff1a; emcc -o ./dist/test.html --shell-file ./tmp.html --source-map-base dist -O3 -g4 --source-map-base dist -s MODULARIZE1 -s "EXPORT_NAME\"Test\"" -s USE_SDL2 -s LEGACY_GL_EMULATION1 --pre-js ./pre.js --post-js ./…

golang bufio.newscanner如何超时跳出_Golang微服务的熔断与限流

(给Go开发大全加星标)来源&#xff1a;Che Danhttps://medium.com/dche423/micro-in-action-7-cn-ce75d5847ef4【导读】熔断和限流机制对于大流量高并发服务来说不可或缺&#xff0c;尤其在微服务架构下更需要在服务中配置熔断限流机制。对可用性要求高的系统&#xff0c;熔断和…

JasperReport报表设计4

在JRXML模板&#xff08;或JRXML文件&#xff09;中的JasperReport 都是标准的 XML文件&#xff0c;以.JRXML扩展。所有JRXML文件包含标签<jasperReport>&#xff0c;作为根元素。这反过来又包含许多子元素&#xff08;所有这些都是可选的&#xff09;。JasperReport框架…