前提概念: 多重继承:比如类a继承类b,类b继承类c,这类关系称为多重继承 多继承:比如类a继承类b和类c,这类关系称为多继承 典型问题: 例如: 农民类和工人类继承自人类,农民工类继承自农民类和工人类 这种菱形的继承将为带来农民工类中将有两份人类中的成员,导致数据冗余。 解决方案——虚继承: 关键字:virtual class people; class farmer:virtual public people; class worker:virtual public
如果一个类从多个类继承而来,而这些类有公共基类。那么在多该基类中定义的成员访问时会出现二义性。C++设计虚继承来解决这个问题。虚继承的本质就是子类引用父类的内存空间,而不创建自己的内存空间。 这样既解决了多重继承可能引发的二义性问题,也使得内存得以释放。 在虚继承过程中的基类被叫做:虚基类 那么实际上,虚继承的本质就是使用一个指向虚基类的指针,这样就无论你怎么继承,就只有一份基类内存空间。 C++使用关键字virtual来进行虚继承。 { }; int main() { Last L; L.a; return 0; } 通过虚继承,这样MyClass类的内存空间只有一份。 这种情况下,虚继承也无法帮到我们。我们只能使用 c.A::a; c.B::a; 来明确对类成员的调用,从而避免二义性的产生。
C++ 是支持多继承的语言,但是实际项目开发中非必要请避免使用多继承以降低代码逻辑的复杂性。 当然 C++ 多继承的特性带来一些问题即菱形继承。 ? Byte 、Expert、Frog 三个类,但是 Frog 类不是以虚继承的方式继承 Base 的。 同时,在虚继承机制当中,虚基类是由最终的派生类进行初始化的,本身达成了一种 “间接继承” 的关系。 也就意味着最终的派生类在构造函数初始化中,要在初始化表中调用虚基类的构造函数进行初始化。 :【Example】C++ 虚基类与虚继承 (菱形继承问题) 虚继承时,子类的内存结构当中不包含父类。 【Example】C++ 接口(抽象类)概念讲解及例子演示 【Example】C++ 虚基类与虚继承 (菱形继承问题) 【Example】C++ Template (模板)概念讲解及编译避坑 【Example
一、菱形继承 在介绍虚继承之前介绍一下菱形继承 概念:A作为基类,B和C都继承与A。 虚继承也可以解决这个问题 ? ) 共享的基类对象成为“虚基类” 说明:虚继承不会影响派生类本身,只是对虚基类进行的说明 通过在继承列表中使用virtual关键字来说明,virtual与继承说明符(public、protected、private {}; 三、虚继承中的类型转换 虚继承中也可以将派生类抓换为基类,用基类的指针/引用指向于派生类 菱形继承中的类型转换 菱形继承中会发生错误,不能将派生类转换为基类 原理是差不多的,就是因为派生类中拥有多份基类的实体 解决二义性最好的办法就是在派生类为成员自定义新的实例 五、虚继承的构造函数 虚继承中的构造函数与普通继承的构造函数不一样: 普通继承:派生类可以不为间接基类(基类的基类)进行构造函数的调用 虚继承:不论派生类属于哪一层
此种菱形继承多存储了两倍的A的内存段,下面将介绍虚基类 ? 二、虚基类(virtual) 1.概念:也称虚继承、菱形继承。 用于多级混合继承时,保留一个虚基类 2.构造顺序 先构造虚基类,如果有多个虚基类,按声明(从左至右)依次构造 再构造基类,如果有多个基类,按声明(从左至右)依次构造 如果有子对象,再构造子对像,如果有多个子对象 B,于是去构造B,构造B的时候,发现继承于虚基类A,于是构造虚基类A,接着构造B。 再接着构造C,发现C继承于虚基类A,但发现虚基类A已经被B构造过了,所以不再构造A,直接构造C。 地址解析: B和C中都保存了A的值,但是在D继承B和C的时候,只保存了一份A,且放在最后 在D继承的B和C内存段中分别有一个函数指针放在最前方 二、虚函数表 1.概念:是一块连续的内存,所有虚函数的首地址都存放在虚函数表中
在 C++ 面向对象编程中,多重继承(Multiple Inheritance)允许一个类继承多个基类的特性,这在设计复杂系统(如 “可序列化”+“可绘制” 的图形组件)时非常有用。 2.1 虚继承的声明方式 在 C++ 中,通过 virtual 关键字声明虚继承,确保公共基类在派生类中仅存一份实例。 六、虚继承的常见误区与最佳实践 6.1 误区一:虚继承可以解决所有多重继承问题 虚继承仅解决菱形继承的公共基类二义性,无法解决非菱形结构的成员冲突(如两个无关基类的同名成员)。 七、总结 虚继承是 C++ 为解决菱形继承问题设计的关键机制,通过 virtual 关键字声明,确保公共基类在最终派生类中仅存一份实例,消除二义性并减少数据冗余。 尽管虚继承在复杂系统中不可替代,现代 C++ 设计更倾向于通过 组合模式(Composition)和接口继承(纯虚类)减少多重继承的使用。
只能通过 sofaBed.Bed::weight_ = 10; 访问,但实际上一个sofaBed理应只有一个weight_,下面通过虚基类和虚继承可以解决这个问题。 二、虚继承与虚基类 当派生类从多个基类派生,而这些基类又从同一个基类派生,则在访问此共同基类中的成员时,将产生二义性,可以采用虚基类来解决。 虚基类的引入 用于有共同基类的场合 声明 以virtual修饰说明基类 例:class B1:virtual public BB 作用 主要用来解决多继承时可能发生的对同一基类继承多次而产生的二义性问题 2、在整个继承结构中,直接或间接继承虚基类的所有派生类,都必须在构造函数的成员初始化表中给出对虚基类的构造函数的调用。如果未列出,则表示调用该虚基类的默认构造函数。 参考: C++ primer 第四版 Effective C++ 3rd C++编程规范
菱形继承 菱形继承的概念 两个派生类继承同一个基类,又有某个类同时继承着这两个派生类 菱形继承典型案例 这种继承带来的问题主要有两方面: 羊和驼都继承了动物的类成员,当羊驼想要使用时,会产生二义性 二是通过虚继承的方式,使羊驼仅继承一份数据。 m_Age,通过限定作用域的方式无法彻底解决这个问题,这个时候就要使用虚继承 虚继承与虚基类 具体实现为在羊类和驼类的继承前加上virtual关键词,Animal类称为虚基类 代码如下: #include Animal{}; //虚继承 class Tuo :virtual public Animal{}; //虚继承 class SheepTuo :public Sheep, public Tuo{} 可以看出羊类和驼类中的数据只是一个虚基类指针,并未继承具体的数据,这个虚基类指针指向各自的虚基类表,而虚基类表中存在一个偏移量,通过这个偏移量再加上首地址可以找到基类中的数据,所以实际上羊驼只继承了一份数据
缺点的解决: 数据冗余:通过下面“虚继承”技术来解决(见下) 访问不明确(二义性):通过作用域访问符::来明确调用。 虚继承也可以解决这个问题 ? std::cout << getMa();*/ //正确,通过B访问getMa() std::cout << B::getMa(); } private: int m_d; }; 八、虚继承 虚继承的作用:为了保证公共继承对象在创建时只保存一分实例 虚继承解决了菱形继承的两个问题: 数据冗余:顶级基类在整个体系中只保存了一份实例 访问不明确(二义性):可以不通过作用域访问符::来调用(原理就是因为顶级基类在整个体系中只保存了一份实例 ) 虚继承不常用,也不建议使用 class A { public: A(int a) :m_a(a) {} int getMa() { return m_a; } private: int m_a; }
40; // 控制台暂停 , 按任意键继续向后执行 system("pause"); return 0; } 执行结果 : 二、virtual 虚继承 1、虚继承引入 在多继承中 , 如果一个类继承了多个含有相同基类的派生类 , 就会产生菱形继承结构 ; 这种情况下 , 可能会出现多个不同的基类实例 , 导致重复定义和二义性 ; 为了应对上述 继承的二义性 问题 , C++ 语言 使用 " 虚继承 " 解决 继承中的 二义性问题 ; C++ 中的 " 虚继承 " 是一种解决 多继承 带来的 菱形问题(diamond problem)的技术 ; 虚继承的目的是 确保每个基类只被继承一次 , 从而避免 重复定义 和 二义性等问题 ; 虚继承 通过在 派生类 中使用关键字 virtual 来指示基类应该被虚继承 , 虚继承确保了每个基类只被继承一次 , 从而避免了重复定义和二义性 ; 在 C++ 中,使用虚继承的语法是在基类列表中使用 virtual 关键字 ; 2、虚继承语法 虚继承语法 : 在 继承的 访问限定符 之前 , 添加 virtual 关键字 , 将该继承行为定义为 " 虚继承 " ; class 子类类名 : virtual
总结一下:c++继承时的多态一般指的运行时多态,使用基类指针或者引用指向一个派生类对象,在非虚继承的情况下,派生类直接继承基类的虚表指针,然后使用派生类的虚函数去覆盖基类的虚函数,这样派生类对象通过虚表指针访问到的虚函数就是派生类的虚函数了 三、虚继承 如果仔细看的话,可以发现我先前多次强调了非虚继承,这是因为在没有虚函数的时候是不是虚继承影响不大,但存在虚函数的时候虚继承和非虚继承是不一样的,如下: #include <iostream> 这说明虚继承不只是实现了派生类自己的虚表指针,还重新生成了属于它自己的虚函数表,但这样一来,等于虚继承就比非虚继承多了很多开销,所以大多数情况还是不要使用虚继承吧。 有人会说,上面不是说虚继承会重新生成虚表指针吗,但这里是类B虚继承类A,但是类D继承的时候是非虚继承,所以类D并不会重新生成虚表指针,但此处类B和类C应该重新生产虚表指针,gdb查看却没有,我一开始也很疑惑 ,基类的虚表指针和成员变量在后; 多重继承时最好使用虚继承,否则不只是会产生令人头疼的二义性问题,还会多一份虚基类的拷贝,使用虚继承以后,大家共享虚基类,既节约了空间,又避免了二义性问题。
1.多重继承带来的问题 C++虚拟继承一般发生在多重继承的情况下。C++允许一个类有多个父类,这样就形成多重继承。 (2)被虚拟继承的基类,叫做虚基类。虚基类实际指的是继承的方式,而非一个基类,是动词,而非名词。 (3)为了实现虚拟继承,派生类对象的大小会增加4。 这个增加的4个字节,是因为当虚拟继承时,无论是单虚继承还是多虚继承,派生类需要有一个虚基类表来记录虚继承关系,所以此时子类需要多一个虚基类表指针,而且只需要一个即可。 (4)虚拟继承中,虚基类对象是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的,派生类的构造函数的成员初始化列表中必须列出对虚基类构造函数的调用;如果未列出,则表示使用该虚基类的缺省构造函数。 ---- 参考文献 [1]陈刚.C++高级进阶教程[M].武汉:武汉大学出版社,2008[8.3(P276-P280)]
本文参考深度探索C++对象模型 ---- 我们常常使用基类指针指向派生类对象,那么,为什么基类指针能够如此轻松的调用派生类的方法呢?在多继承的情况下,this指针必须经过调整,才能正确地找到虚表。 下文为你介绍多继承模型下的指针偏移机制 ---- 指针偏移存在机制: 设一个多继承的类内存布局如下,单词代表对象首地址。 调用时:基->派生 指向第二个基类的指针,调用派生类的虚函数。 clone,Derived重写了clone,那么需要向后调整Base1长度以正确指向Derived object ---- 使用派生类指针指向派生类 调用时:派生->基 指向派生类的指针,调用第二个基类继承来的虚函数 根据调用的指针类别判断是否需要调用有调整的函数 函数较大时,产生多重进入点,函数体分为(1)调整this (2)执行自定义函数码,根据是否需要调整,通过thunks跳转至对应的进入点 address points: 虚函数期待获得的是引入虚函数的类对象的地址
一、虚继承原理 1、虚继承解决继承二义性问题 继承的二义性 : 如果 一个 子类 ( 派生类 ) 继承多个 父类 ( 基类 ) , 这些父类 都继承了 相同的父类 , 那么 子类 访问 父类的父类 中的成员 , 就会产生 二义性 ; 报错 : error C2385: 对“x”的访问不明确 ; 使用 " 虚继承 " 可以解决上述问题 , 子类 继承父类时 , 在 访问限定符 之前使用 virtual 关键字 , 即可将 普通继承 改为 虚继承 ; 下面的代码中 A 是父类 ; B 类 和 C 类 虚继承 A 类 , 这样当 某个类 同时 多继承 B 类 和 C 类时 , 访问 A 类中的成员时 , 不会出现 二义性 ; 由于 B 和 C 虚继承 A , D 类访问 A 中的成员 , 不会产生二义性 ; class A { public: int x; }; // 子类 B 继承了父类 A 的 x 成员 对象调用 , 一次由 C 对象调用 ; 此时 D 对象中就包含了 两个 A 类的 子对象 ; 当 访问 A 类的成员时 , 不知道访问哪个 A 类的成员 , 就出现了二义性 ; 3、虚继承原理 使用 虚继承
虚函数与虚继承寻踪 封装、继承、多态是面向对象语言的三大特性,熟悉C++的人对此应该不会有太多异议。 继承机制解决了对象复用的问题,然而多重继承又会产生成员冲突的问题,虚继承在我看来更像是一种“不得已”的解决方案。 virtual在C++中最大的功能就是声明虚函数和虚基类,有了这种机制,C++对象的机制究竟发生了怎样的变化,让我们一起探寻之。 为了查看对象的结构模型,我们需要在编译器配置时做一些初始化。 图4 MyClassC对象模型 虚基类表每项记录了被继承的虚基类子对象相对于虚基类表指针的偏移量。 尤其是在多重、虚拟继承下的复杂结构。通过这些真实的例子,使得我们认清C++内class的本质,以此指导我们更好的书写我们的程序。
参考链接: C++继承 继承 类和类的关系有组合、继承和代理。继承的本质就是代码复用。子类继承父类中的一些东西,父类也称为基类,子类也称为派生类。派生类继承了基类除构造函数以外的所有成员。 继承的方式 继承方式有public(公有继承)、private(私有继承)和protected(保护继承)。 基类中不同访问限定符下(public、protected、private)的成员以不同的继承方式继承,在派生类中的访问限定也不同,具体如下: 基类的布局优先于派生类 #include<iostream 基类中含有虚函数,那么基类布局中存在一个虚函数指针,指向虚函数表;且其派生类中与其同名同参的函数不需要加virtual也是虚函数。 vfptr指针指向的vftable(虚函数表)中,&Base_meta中存放了RTTI信息(运行时类型信息),也就是class Base,0表示偏移,&Base::Show表示虚函数的入口地址。
继承 前面讲到c++的继承是子类在继承时声明继承的权限,之前描述有点不够准确。以下时书中提及的能继承的成员。 ? 当使用protected继承时,父类的所有public成员在当前子类中会变为protected。==。 虚函数 c++中,被定义为虚函数的成员,能被子类重写,虚函数是用virtual修饰的函数。 因为引用类型是父类型,在调用普通方法时,仍是父类方法,只有调用虚方法时,使用了真正的子类方法。而指针类型也是与引用类型类似。 析构函数与继承 c++中子类析构函数结束会自动调用父类析构函数。 在c++中有对应的纯虚函数,具备纯虚函数的类不能进行实例化,纯虚函数指将虚函数赋值为0的函数,如 class A{ virtual pureVirtualFunction() = 0; } 类的提前声明 代码示例 try{ throw "I am string"; }catch(const char * msg){ //code } final 与java类似,c++也有final,通过在类名后面或者虚函数后面加上
Person* p2 = new Student; delete p1; delete p2; return 0; } 2.4 C++11 override 和 final 从上面可以看出,C+ 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数 4. 重写是语法的叫法,覆盖是原理层的叫法 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个 单继承和多继承关系中的虚函数表 5.1 单继承中的虚函数表 class Base { public: virtual void func1() { cout << "Base::func1" << 所以菱形继承、菱形虚拟继承我们的虚表我们就不看了,一般我们也不需要研究清楚,因为实际中很少用 C++ 虚函数表解析 | 酷 壳 - CoolShell C++ 对象的内存布局 | 酷 壳 - CoolShell
C++的三大特性之一的多态是基于虚函数实现的,而大部分编译器是采用虚函数表来实现虚函数,虚函数表(VTAB)存在于可执行文件的只读数据段中,指向VTAB的虚表指针(VPTR)是包含在类的每一个实例当中。 《深度探索C++对象模型》 一、单继承 1 #include<iostream> 2 #include <stdio.h> 3 using namespace std; 4 class A { vptr2[1](); 34 35 return 0; 36 } 运行结果: B1::bar D::foo D::bar \\\\\\ D::bar B2::foo 结论: 多重继承会有多个虚函数表 ,几重继承,就会有几个虚函数表。 再简单总结一下 覆盖 隐藏 重载 的区别: 覆盖 是C++虚函数的实现原理,基类的虚函数被子类重写,要求函数参数列表相同; 隐藏 是C++的名字解析过程,分两种情况,基类函数有virtual,参数列表不同
解决冗余的办法:虚继承! 1.3虚继承 有了多继承就可能有菱形继承,而菱形继承又存在二义性和数据冗余等问题。所以C++就引入了虚继承来解决数据的冗余问题。 1.3.1为什么通过虚继承可以将Person部分成员提取出来? 菱形虚拟继承的原理: 虚继承通过共享基类实例实现成员提取。 当使用虚继承时,派生类会包含一个指向共享基类实例的指针(虚基类指针),而非直接嵌入基类成员。这使得不同路径继承的虚基类在最终派生类中指向同一内存地址。 每个包含虚基类的派生类都会生成虚基类表,记录虚基类相对于该派生类起始地址的偏移量。这使得无论通过哪条继承路径访问,都能定位到同一个基类实例。 虚基类指针或引用 编译器会为虚继承的类生成额外的信息(如虚基类表或偏移量),用于在运行时定位共享基类子对象。这通常通过虚基类指针(vptr)或间接寻址实现。