分享

从内存分布角度解释“虚”原理

 海阔天空7815 2019-08-22

虚数?虚继承?虚函数?

从内存分布角度解释”虚“原理?

看到这个题目的时候,学法的我表情是这样的。

然而,我朴素的法感情告诉我这样的内容难不倒我一个法律人。

幸好,我还有搜索引擎还有同组同学的帮助。

让我们看看”虚“到底是什么~

1.虚继承

我们从目的和实现原理方面来看虚继承

什么?虚继承还有目的?来人啊,快把这要谋害我朴素法感情的人拿下!

1.1虚继承的目的

        虚继承是解决C++多重继承问题的一种手段,从不同途径继承来的同一基类,会在子类中存在多份拷贝。

这将存在两个问题:

其一,浪费存储空间。

其二,存在二义性问题。

通常可以将派生类对象的地址赋值给基类对象,实现的具体方式是,将基类指针指向继承类(继承类有基类的拷贝)中的基类对象的地址,但是多重继承可能存在一个基类的多份拷贝,这就出现了二义性。

1.2一般继承的实现原理

那么虚继承是通过什么方式解决这两个问题的呢?

我们要从内存分布的角度来解释原理。

因此需要知道一点visual studio开发人员命令行的知识。

我们首先从VS安装目录中打开开发人员命令行

进入到对应项目的目录,然后输入命令

cl -d1reportAllClassLayout yuan.cpp

这样命令行界面就会给出该cpp里定义的所有类的内存布局

如果想要特定类的内存布局,可以输入命令

cl -d1reportSingleClassLayout[类名] yuan.cpp

有了这样的技术支持,我们就可以来探究虚继承的内存分布了。


1.2.1普通继承的内存布局

我们可以用visual studio新建项目,新建yuan.cpp,输入如下代码

打开开发人员命令行,进入项目的目录,输入

cl -d1reportAllClassLayout yuan.cpp

可以得到我们定义的ABCD类的布局

可以看到,普通继承时

类D包含的成员有(DataA,DataB,DataA,DataC,DataD)

int DataA出现了两次,因此类D的内存大小为20

因此出现了内存的浪费和二义性问题

1.2.2虚继承的内存布局

我们将继承方式改为虚继承,代码如下

使用命令行查看类ABCD新的内存布局

可以预见A的内存布局并不会改变。

而B和C占用的内存从8变为12,因为其中多了一个指针成员vbptr,vbptr指向了vbtable,我们将vbptr称为虚指针,将vbtable称为虚表

再来看看D的内存分布

D占用的内存从20变为24,但D中的成员相比于普通继承只有一个DataA,而多了两个分别继承自B和C的虚指针vbptr。

那么虚指针到底是什么呢?

D的内存布局其实已经为我们提供了解答,我们可以看到两个指针分别指向两个虚表,虚表中记录了vbptr与本类的偏移地址。

在本例子中,类B的vbptr指向了虚表D::$vbtable$@B@,虚表表明公共基类A的成员变量dataA距离类B开始处的位移为20,这样就找到了成员变量dataA,类C的指针也对应指向虚表,而虚表最终表示的偏移和B的虚表相同,这样D中的成员就只有一个DataA,解决了二义性的问题。

但是我们可以看到,实际上使用虚继承后BCD类的占用的内存都变得更多了,并没有解决内存浪费的问题。

这是因为我们类A的成员只有一个int类变量,如果A中的成员更复杂,占用更多的内存,使用虚继承时内存的占用要比普通继承少很多。

在写完这些,我陷入的对自己人生的怀疑“我是谁,我在哪,我在干什么?然而这些都不能阻挡我接下来学习虚函数的热情

2.虚函数

2.1虚函数的目的

在同一类中是不能定义两个名字相同、参数个数和类型都相同的函数的,否则就是“重复定义”。

但是在类的继承层次结构中,在不同的层次中可以出现名字相同、参数个数和类型都相同而功能不同的函数。

人们提出这样的设想,能否用同一个调用形式,既能调用派生类又能调用基类的同名函数。在程序中不是通过不同的对象名去调用不同派生层次中的同名函数,而是通过指针调用它们。

例如,我们在析构类的时候,一定是想要调用该类的析构函数,而不是基类的析构函数,否则有可能导致部分内存没有释放。

C++中的虚函数就是用来解决这个问题的。虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。简单来说,虚函数是为了实现多态性

2.2虚函数的实现原理

我们仍从内存分布的角度来探究虚函数的实现

2.2.1一般继承

我们仍打开之前的项目,更改yuan.cpp的代码如下

B类并没有对继承自A类函数进行更改,我们调用VS开发者命令行

在项目目录下执行cl –d1reportAllClassLayout yuan.cpp

得到对应AB类的内存布局

可以看到,和虚继承类似,有虚函数的类中的成员会多一个虚指针vfptr,虚指针指向虚函数表vftable

在本例中,类A的虚指针指向A::$vftable@,这个虚表顺序排列着我们在类A中定义的f(),g(),h()函数。

由于我们没有在子类B中对函数进行定义的覆盖,所以B的虚表中函数仍是A类中的f(),g(),h()函数。

现在我们更改代码如下,在B类的定义中重新定义继承的f()函数。

我们在类B中对f()函数进行了重新定义

再来看类AB的内存布局

A类的虚表并不会变化,但B类的虚表发生了变化,具体是B类原本0位置的&A::f&B::f覆盖了。

因此如果我们声明一个基类的指针,使指针指向子类,通过指针调用f()函数,我们会调用B类虚表中的&B::f,从而实现了多态

如果我们只对g()进行重新定义,虚表会变成怎样呢?

可以看出,虚表中函数的顺序是与基类中定义顺序相同,如果我们在子类中对某个函数进行重新定义,新的函数会覆盖原本函数但并不会改变顺序。

2.2.2多重继承

假如子类继承自多个父类,虚函数表又是如何实现的

更改代码如下

我们定义了新的基类C,并在B中对父类的f()进行覆盖定义,并新定义虚函数k()

我们重新来看B的内存布局

B中的成员理所当然的有继承自A和C的两个虚指针。

继承自A的虚指针指向虚函数表B::$vftable@A@

其中的成员不仅有&B::f,&A::g,&A::h,还有B中新定义的虚函数

而继承自C的虚指针指向虚函数表C::$vftable@C@

其中的成员为C的两个虚函数。

也就是说B新定义的虚函数只会存放在继承的第一个虚函数表中,并且在虚表中的位置要放到基类的虚函数之后

3.总结

总的来说

虚继承是为了解决多代继承中的二义性和内存浪费的问题。

虚函数是为了解决多态性的问题。

二者都是通过虚指针虚表来实现的。

当然以上的话以我朴素的法感情看来他们依然是天书,但是我相信我对虚函数与虚继承的理解已经比之前好很多了。

另外预祝大家大作业写bugDebug顺利!

by 伊藤優香

and   孟令寰

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多