C++进阶—继承(下)菱形(虚拟)继承分析&&虚拟继承存储对象模型

目录

0. 前言

1. 普通多继承下,基类和派生类复制转换底层细节(切片)

2. 多继承下的复杂菱形继承

3. 菱形虚拟继承(虚基类)重点

3.1 菱形非虚拟继承对象存储模型

3.2 菱形虚拟继承对象存储模型

3.3 虚拟继承对象存储模型

3.4 多对象继承关系分析其虚基类&虚拟化继承位置

5. 继承的总结和反思


0. 前言

这篇文章主要接上篇文章,从更深层次理解普通继承切片切割以及虚拟继承切片切割,从底部虚拟内存分析,以及分析C++多继承带来的一些问题,和C++解决多继承带来问题采取的方式,并从底层内存观察其逐步实现及原理,最终更深层次感受多继承!并从软件工程分析继承和组合两个概念!!!

1. 普通多继承下,基类和派生类复制转换底层细节(切片)

派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。

那么编译器在普通继承下,处理时如何切片,由上面一题目,看内存分析如下:

可知在编译阶段,对象实例化时,实例化对象只存储成员变量,而成员函数会根据其模板参数、所属类域存储在公共代码段,以便进行调用!

在对象实例化时,通过调试观察其虚拟内存得出实例化对象会提前在栈区或者堆区开辟好空间,其成员变量在栈区先使用低地址在使用高地址,(如结构体,便于通过偏移量计算成员位置),因此可以绘制出对象d实例化时,内存存储数据模型:

        而通过上篇文章可知,默认构造,先构造其基类,在构造子类,而对于多继承其根据继承顺序依次构造,因此先实例化_b1,在实例化_b2,其次实例化_d,因此可以看出由低地址到高地址使用实例化!!!

使用调试,观察其切割切片方式:

  1.  将&d派生类Derive地址赋值给Base* p1基类指针,此时便会进行切片,切割使用_b1,所以此时p1指向的地址便是原类Derive实例化对象d的地址,但是由于其进行切片,向后只能访问其基类大小个字节,只能访问_b1
  2. 将&d派生类Derive地址赋值给Base* p2基类指针,此时便会进行切片,切割使用_b2,由于Base2实例化在中间,因此切片时从_b2地址进行切片赋值,向后只能访问其基类大小个字节,只能访问_b2
  3. 将&d派生类Derive地址赋值给其所属类型的指针变量,此时未发生切片,p3所指向的地址便是整个实例化对象的地址,所以p3的地址便是最开始的地址!!!

最终结果:

 p3和p1虽然向后访问数据的偏移量不同,但是所指向同一空间的起始地址&d,_d1,而p2指向同一空间基于Base2实例化的地址,即_b2地址,再根据派生类成员变量内存分布,即可以得出上图结果!!!

总结:

  1. 对于派生类引用赋值给基类,底层是对指针和解引用的封装,含义不同,内存操作相同!!!
  2. 对于派生类直接赋值给基类,会直接进行切割赋值

2. 多继承下的复杂菱形继承

单继承:一个子类只有一个直接父类时称这个继承关系为单继承

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承

 菱形继承:菱形继承是多继承的一种特殊情况。

什么是二义性:(多继承和菱形继承都会导致二义性)

 如上图:在多继承中class A和class B若是有多个相同数据成员,此时对于class C而言同名的数据成员会产生二义性的问题,需要通过类域对其进行区分,如下代码:

	class A {
	public:
		A() :_a(1), _same(10) {

		}

		int _a;
		int _same;
	};
	class B {
	public:
		B() :_b(1), _same(1) {

		}
		int _b;
		int _same;
	};
	class C : public A,public B{
	public:
		void Print() {
			//cout << _same << endl;//err _same无法确定是属于哪个类,二义性
		}
		int _c;
	};
	void test() {
		C c;
		//cout << c._same << endl;//err _same无法确定是属于哪个类,二义性
	}

菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。 在Assistant的对象中Person成员会有两份,除了二义性,当间接相同基类成员变量如果占用空间过大,也会浪费内存空间。

	class Person
	{
	public:
		string _name; // 姓名
	};

	class Student : public Person
	{
	public:
		int _num; //学号
	};

	class Teacher : public Person
	{
	public:
		int _id; // 职工编号
	};

	class Assistant : public Student, public Teacher
	{
	public:
		string _majorCourse; // 主修课程
	};

	void Test()
	{
		// 这样会有二义性无法明确知道访问的是哪一个
		Assistant a;
		//a._name = "peter";
		// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
		a.Student::_name = "xxx";
		a.Teacher::_name = "yyy";
	}

我们发现在类Assistant中存在两份的基类Person,分别Assistant存在类Student和类Teacher中,如果数据多则严重浪费空间,也不利于维护, 我们引用基类Person中的数据还需要通过域运算符进行区分。

为了解决以上问题,C++提供了虚基类,也叫做虚拟继承的概念

3. 菱形虚拟继承(虚基类)重点

为了解决上述菱形继承带来的问题,C++中引入了虚基类,其作用是在间接继承共同基类时只保留一份基类成员,虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。

	class Person
	{
	public:
		string _name; // 姓名
	};
	class Student : virtual public Person
	{
	protected:
		int _num; //学号
	};
	class Teacher : virtual public Person
	{
	protected:
		int _id; // 职工编号
	};
	class Assistant : public Student, public Teacher
	{
	protected:
		string _majorCourse; // 主修课程
	};
	void Test()
	{
		Assistant a;
		a._name = "peter";
	}

虚拟继承解决数据冗余和二义性的原理分析

3.1 菱形非虚拟继承对象存储模型

为了研究虚拟继承原理,先给出一个简化的菱形非虚拟继承体系,再借助内存窗口观察对象成员的模型:

class A {
	public:
		int _a;
	};

	class B : public A{
	public:
		int _b;
	};
	class C : public A{
	public:
		int _c;
	};

	class D :public B, public C {
	public:
		int _d;
	};

	void Test() {
		D d;
		d.B::_a = 1;
		d.C::_a = 2;
		d._b = 3;
		d._c = 4;
		d._d = 5;
	}

分析上述菱形非虚拟继承对象存储模型,如下:

实例化对象d进行了多继承,其基类B、C为非虚拟继承:

  1. 首先根据其继承的第一个类调用基类的构造,即调用B的构造,B继承了A,因此B再次调用基类A的构造
  2. 其实根据其继承的第二个类调用基类的构造,即调用C的构造,C继承了A,因此C再次调用基类A的构造
  3. 再根据vs内存监视窗口,其虚拟内存,观察其更改顺序,即可的到其非虚拟菱形继承对象存储模型
  4. 从对象存储模型可观察到非虚拟菱形继承中类A,分别在B类C类各有一份,因此造成二义性和数据冗余

3.2 菱形虚拟继承对象存储模型

为了研究虚拟继承原理,再给出一个简化的菱形虚拟继承体系,再借助内存窗口观察对象成员的模型:

	class A {
	public:
		int _a;
	};

	class B : virtual public A{
	public:
		int _b;
	};
	class C : virtual public A{
	public:
		int _c;
	};

	class D :public B, public C {
	public:
		int _d;
	};

	void Test() {
		D d;
		d.B::_a = 1;
		d.C::_a = 2;
		d._a = 0;
		d._b = 3;
		d._c = 4;
		d._d = 5;
	}

分析上述菱形虚拟继承对象存储模型,如下:

 实例化对象d进行了多继承,其基类B、C为虚拟继承,类A为虚基类:

1. 首先根据其继承的第一个类调用其基类的构造,即调用B的构造,B虚拟继承了A,B中指向A的数据就变成了虚基类表的指针,该指针指向一个虚基类表,虚基类表中存储了该指针到公共数据所在内存的偏移量,然后构造B的成员

2. 其次根据其继承的第二个类调用其基类的构造,即调用C的构造,C虚拟继承了A,C中指向A的数据就变成了虚基类表的指针,该指针指向一个虚基类表,虚基类表中存储了该指针到公共数据所在内存的偏移量,然后构造C的成员

3. 最后构造D类的成员变量,此时,实例化对象d,d对象中只有一份数据A,以及其两个基类的虚基类表指针

简单来说:

  • 如果使用非虚拟继承,那么D会从B、C那里继承两份相同的数据
  • 如果使用虚拟继承,那么那两份相同的数据在D类对象中只会存在一份。而D从B、C那里继承的是它们独有的数据以及B和C的虚基类表指针。通过它们各自的虚基类表指针,就可以获取该指针与那份公共数据存储位置的偏移量,进而可以访问它。

3.3 虚拟继承对象存储模型

当不是菱形状态传递时,有关继承virtual,其虚拟继承的对象存储模型:

	class A {
	public:
		int _a;
	};

	class B : virtual public A{
	public:
		int _b;
	};
	class C : virtual public A{
	public:
		int _c;
	};

	class D :public B, public C {
	public:
		int _d;
	};

	void func(B* bb) {
		cout << bb->_a << endl;
	}
	void Test() {
		D d;
		d.B::_a = 1;
		d.C::_a = 2;
		d._a = 0;
		d._b = 3;
		d._c = 4;
		d._d = 5;
		B b;
		func(&d);
		func(&b);
	}

只要是虚拟继承,编译器都会按照虚基类表指针进行编译,才能保证其子类的传递正确性。

3.4 多对象继承关系分析其虚基类&虚拟化继承位置

当存在多边形对象继承时,其虚基类,一般为最终继承关系中会重复的成员变量所属的类!!!

 从上图继承关系可知,构成菱形继承,E类中会造成A类成员变量二义性,因此,需要将类B、C定义虚拟继承解决其二义性和数据冗余。

5. 继承的总结和反思

  1. 很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱 形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设 计出菱形继承。否则在复杂度及性能上都有问题。
  2. 多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java。
  3. 继承和组合
  •         public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
  •         组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
  •         优先使用对象组合,而不是类继承 。
  •         继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称 为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
  •         对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象 来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复 用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。

        实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有 些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用 继承,可以用组合,就用组合。

	// Car和BMW Car和Benz构成is-a的关系
	class Car {
	protected:
		string _colour = "白色"; // 颜色
		string _num = "陕ABIT00"; // 车牌号
	};

	class BMW : public Car {
	public:
		void Drive() { cout << "好开-操控" << endl; }
	};

	class Benz : public Car {
	public:
		void Drive() { cout << "好坐-舒适" << endl; }
	};

	// Tire和Car构成has-a的关系

	class Tire {
	protected:
		string _brand = "Michelin";  // 品牌
		size_t _size = 17;         // 尺寸

	};

	class Car {
	protected:
		string _colour = "白色"; // 颜色
		string _num = "陕ABIT00"; // 车牌号
		Tire _t; // 轮胎
	};

猜你喜欢

转载自blog.csdn.net/IfYouHave/article/details/131333938
今日推荐