虚数?虚继承?虚函数? 从内存分布角度解释”虚“原理? 看到这个题目的时候,学法的我表情是这样的。 然而,我朴素的法感情告诉我这样的内容难不倒我一个法律人。 幸好,我还有搜索引擎还有同组同学的帮助。 让我们看看”虚“到底是什么~ 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 孟令寰 |
|