[C++]多继承和虚继承

目录

1 多继承

2 多继承的缺陷——菱形继承

3 虚继承

4 对多继承的评价


1 多继承

        对于我们C++的继承来说,有单继承的同时也有多继承,单继承很好理解,不就是父亲儿子的关系嘛,那么多继承是什么,一个儿子多个爸爸?这样说感觉不太好,那就理解为一个儿子有一个妈妈和一个爸爸的关系。也就表示一个C对象同时含有A的特征也有B的特征,那么此时我们就可以采用多继承来完成我们对于类的实现

        那么现实当中有什么是可以用多继承来描述呢?其实硬要联系起来很容易,一辆车可不可以,他继承于轮胎类,外壳类,发动机类?小番茄可以不?继承与蔬菜类和番茄类,还有很多东西都是可以这样联系起来的,只要我们想一下总归是能找到某些对象是适合多继承的。所以多继承这个东西好不好呢?目前看起来是很好,我们祖师爷本贾尼最开始设计出来多继承的时候估计在想“我可真是太聪明了”。事实如何呢?咱后面再谈。

        那么多继承应该怎么写呢?如下代码。

class A
{
public:
	A(int a)
		:_A(a)
	{
		cout << "class A " << _A << endl;
	}
protected:
	int _A;
};


class B
{
public:
	B(int b)
		:_B(b)
	{
		cout << "class B " << _B << endl;
	}
protected:
	int _B;
};

//C是A和B的派生类
class C:, public A,public B
{
public:
	C(int a, int b, int c)
		:A(a),B(b),_C(c)
	{
		cout << "class C " << _C << endl;
	}
protected:
	int _C;
};
int main()
{
	C c(1,2,3);
	return 0;
}

        多继承的方式就是在单继承的基础上再添加上其他类的继承,这一点基本上是没有什么好讲的,不过值得注意一点,这个继承的顺序会影响我们利用初始化列表的顺序,谁的继承写在前面,那么在初始化列表当中谁就会先被初始化。输出图:

         如果我更改了继承顺序,那么大家看看会发生什么事情?

class C: public B, public A

        输出图:

         看到了吗,就算我初始化列表当中写的A在前面,但是还是B的构造函数更早使用,记住这个概念,之后有大用。

2 多继承的缺陷——菱形继承

        说到多继承,咱们的视角就不能考虑的那么局限了,视角需要打开一点,假设你的爸爸继承于你的爷爷,你的舅舅也继承与你的爷爷,然后你继承了你舅舅的脑袋,你还继承你爸爸的身体(不考虑伦理问题哈),那么这个时候对于你来说,你也是继承于你的爷爷,但是你的舅舅有你的爷爷,你的爸爸有你的爷爷,这样说来,你岂不是有两个爷爷?但是事实上只有一个哇,这个时候该咋搞?是不是就没法搞了哇。也就成为了下图。

         这是我们希望的吗?肯定不是啊,这岂不是你把描述的爷爷和你舅舅描述的爷爷是两个人了?如下代码:

class GrandFather
{
public:
	string _name;
};

class Father:public GrandFather
{
public:
	string _body;
};

class Uncle :public GrandFather
{
public:
	string _face;
};

class You :public Father, public Uncle
{
public:
	int _age;
};

int main()
{
	You you;
	you.Father::_name = "zhangsan";
	you.Uncle::_name = "lisi";
	you._body = "good";
	you._face = "bad";
	you._age = 18;
	return 0;
}

         可以看到下图,你从你爷爷哪里继承过来的_name变量,也就是也就是说,你理应当只有1个名字,就算是赋值两次,你也只应该只有第二次赋值的那个名字,但是事实是什么?你有zhangsan的名字也有lisi的名字,这岂不是证明了你从两个爷爷哪里分别继承了一个_name变量吗?这合理吗?这很不合理

        并且,还有一个问题,看到我对_name的赋值是如何操作的吗?我通过作用域限定符操作,那我不加上作用域限定符会怎么样?来看:

         直接报错了,错误信息是对于_name的访问不明确,也就是说编辑器无法知道我们到底要访问从哪个类继承下来的_name,就算这是同一个类。

        由上述问题可以得知多继承可能会导致什么样的错误?

1. 对于一个变量却有多个值——二义性

2. 同一份代码会继承多份——数据冗余

         所以对于多继承来说,如果造成了菱形继承那么就倒大霉了,以后代码出现错误都不知道怎么去修改它,恶心死人。

        所以对于这一个点,祖师爷也是被骂惨了,本来祖师爷在设计出多继承的时候是等着我们去夸奖他的,但是等来的确实谩骂,但是没有办法,有坑咱们的祖师爷就得改了呗,估计祖师爷在该代码时内心想的是“我真的服了,早知道我就不设计这个完蛋玩意了,恶心死我”。所以之后java在后序借鉴C++设计时,就直接舍弃了多继承这个功能,只有单继承。

3 虚继承

        祖师爷通宵达旦想出来的解决方式是什么呢?那就是虚继承,先不管虚继承是如何实现的,看看加上虚继承之后,我们的代码变成什么样了。

        虚继承的写法就是在继承方式之前添加virtual关键字就行,以下是上面代码的改动。

class Father:virtual public GrandFather

class Uncle :virtual public GrandFather

         输出结果:

         首先看到我们用了虚继承之后还像之前一样不加作用域限定符就找不到_name了吗?没有,因为我都已经运行起来了,怎么可能找不到嘛,其次,我们的_name最后的结果是什么?在任何一个作用域都是相同的名字lisi,这才符合我们的逻辑嘛,之前的那个是个什么玩意嘛。

        上述结果也表明了祖师爷设计出来的虚继承确实解决了菱形继承的问题,那么它的底层是怎么样的呢?让我们来看看。

虚继承底层实现:

        对于虚继承来说,我们的监视窗口已经不管用了,这是因为监视窗口在这一部分已经被我们亲爱的编辑器设计大佬给改了,所以我们只能通过内存来看到底发生了什么。(为了方便观察,我将所有类的数据都更改为整型变量)。

         由地址可以看到,我们的_name = 2跑到了最下面去了,然后爸爸类和舅舅类分别都有两个不知道是什么的数据,那么,我们进入这个数据看看是个什么?

         我们的编辑器是小端存储,所以读取地址时需要反着读取。

        从上面两个地址中,我们可以看到里面有一个空指针和一个14和0c,这是什么?我们计算以下这个大小是多少字节,分别是20和12对吧。记住这个值。

        我们把这两个这两个0x地址分别加上20和12看是多少?是不是都是指向了对应_name的2哪一个地址了,这表明了什么?这表明这存储的数据就是一个偏移量。所以编辑器以后读取_name只要原地址加上偏移量就能得到真正的_name的位置了,也就是这个地址存的数据是一个指针,也就让所有的_name保持一致,就算是继承再多类都没有关系了

        那么这个时候有人可能要问了,为什么要设计一个指针出来,我们直接存到第一个位置不就好了?以后再去访问都是访问第一个位置的数据不行吗?能行,但是这个偏移量的出现并不仅仅是为了解决这一个点,在后面的多态也有用。

        上面的过程其实我相信大家也能看明白,确实是解决了变量的二义性,但是数据冗余呢?这不是还多了一些数据嘛?原来就只有5个变量,现在直接跑出来6个了,你这不是搞我嘛,更何况指针也指向了一片空间,这篇空间不是也会消耗空间吗

        我一个一个解答问题,首先对于变量的增多,我只回答一句话,如果我们的爷爷类有很多很多变量呢?我们多的数据至始至终都只有两个指针而已,其他变量是本来就应该要有的,这样说有些抽象,看代码:

class GrandFather
{
public:
    int _name;
    int a;
    int b;
    int c;
    int d;
    int e;
};

         如上,如果我们不使用虚继承,那么就会比现在多出(5-2)*4个字节大小的空间,这样看来是不是减少了许多?对于这个偏移量来说,只要我们是继承,那么变量之间的相对位置一定是不会改变的,所以只需要存一份偏移量就行,至于为什么有两个指针?其实只要好好想象就能知道,我们继承了两个类,那就有两个指针,那我继承3个4个,那么就会多出来几个指针,实际上这个指针的意义就目前而言就是帮助我们存偏移量

        第二个问题,这个指针指向的内容不是也会消耗空间嘛?确实他会消耗空间,但是我问你,我们有必要存多份这个空间嘛?既然我们用这个空间的目的是为了得知偏移量,另外一个指针用这个类也是为了偏移量,那它们直接用一份不就好了?难道说我定义一个类就为了写一个对象?这肯定是不可能的,如下输出:

         看到了嘛?两个不同的对象,里面指针指向的地址是一模一样的,也就证明了我所说的是正确的。对于整个类来说,这一点空间实在是太微不足道了。只能说祖师爷设计这一块内容还是花费了很多心思的。(自己走的坑,哭死也要走完)。

4 对多继承的评价

        可能看了我上面的讲述,大伙认为,多继承也就这了?我觉得也没什么嘛,只要我多注意以下就行,感觉什么虚继承啊,菱形继承之类的,统统不在话下,只能说,这是还没有看到真正麻烦的代码,如下:(这个代码都算是简单的)

class baseA
{
public:
	baseA(char* a)
		:_a(a)
	{
		cout << _a << endl;
	}
protected:
	char* _a;
};

class mediumB:virtual public baseA
{
public:
	mediumB(char* a, char* b)
		:_b(b), baseA(a)
	{
		cout << _b << endl;
	}
protected:
	char* _b;
};

class mediumC :virtual public baseA
{
public:
	mediumC(char* a, char* c)
		:_c(c), baseA(a)
	{
		cout << _c << endl;
	}
protected:
	char* _c;
};

class subD : public mediumC, public mediumB
{
public:
	subD(char* a, char* b, char* c, char* d)
		:mediumB(a,b),mediumC(a,c),baseA(a),_d(d)
	{
		cout << _d << endl;
	}
protected:
	char* _d;
};


int main()
{
	char s1[] = "class A";
	char s2[] = "class B";
	char s3[] = "class C";
	char s4[] = "class D";
	subD* d = new subD(s1, s2, s3, s4);
	delete d;
	return 0;
}

        请问上述代码输出什么?有多少人会认为是ABACAD?又有多少人认为是ABCD?又有多少人算不出一个正确结果呢?

        那么我们来看看最终答案是什么吧。

         答案是什么?ACBD,是不是很恶心人?有人算出正确答案了嘛?反正博主第一次没算出来。不管算没算出来,一起来看解析吧。

        首先看到我们的代码new的顺序是ABCD,在subD的初始化列表里面是:

:mediumB(a,b),mediumC(a,c),baseA(a),_d(d)

        这样一个顺序,那么按照常规理解应该是D调B,B调A?然后D调C,C调A?再D调A?答案是不是,还是得我开始说的嘛?初始化列表初始化的顺序是继承的顺序,那么一定是先baseA,再mediumC,然后mediumB,最后是subD。所以A一定在最前面,D一定在最后面,C在B的前面。

        再看,B和C都会去调用A,但是呢由于虚继承的原因,这个A已经自己初始化完成了,所以在进入C和B的构造函数时,它是不会再次进去构造的。要问为什么?这得问祖师爷和编写编辑器的大佬了,他们是这样设计的。

        所以最后的答案才会是ACBD。

        看了上面的题,大家还轻松的认为多继承用起来很爽吗?反正博主是觉得头疼,光是这么一个简单的代码就让我看了挺久,关键是还错了,所以博主也不建议大家使用。但是呢还有大佬写过多继承加虚继承,那就是写iostream的大佬,有兴趣大家可以自行了解。


        以上就是博主对于多继承的所有理解了,希望能够帮助到大家。

猜你喜欢

转载自blog.csdn.net/weixin_52345097/article/details/129825021
今日推荐