C++中的继承(二)

这篇具有很好参考价值的文章主要介绍了C++中的继承(二)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

上一篇文章我们C++的正常继承其实已经讲完了,但是后面还有一个大坑。
实际当中继承有单继承和多继承。

单继承就是直接继承一个类。
只有一个直接父类的就叫做单继承。
C++中的继承(二),c++,c++,java,数据库
如果是单继承那就比较简单。
现实世界除了有单继承还有多继承。

多继承

多继承就是我一个类我具备另外两个类的特征。
单继承就是一个类只具备另外一个类的特征。
C++中的继承(二),c++,c++,java,数据库

现实世界当中有什么东西需要具备两个特征都继承一下呢?
比如:有没有一种物种既具有水果的特征,也具有蔬菜的特征?
番茄。

多继承很重要,它能够更好的描绘这个世界。
所以多继承看起是很合理的,但是它有一个大坑。

多继承就可能会导致这种菱形继承。
C++中的继承(二),c++,c++,java,数据库

菱形继承会有什么样的问题呢?
他会导致对象里面有两份人的信息。

C++中的继承(二),c++,c++,java,数据库
C++中的继承(二),c++,c++,java,数据库
这样你就不知道要访问从学生那里继承来的呢,还是从老师那里继承来的呢?
C++中的继承(二),c++,c++,java,数据库

指定访问是能解决二义性的
C++中的继承(二),c++,c++,java,数据库
C++中的继承(二),c++,c++,java,数据库
但是这样很不合理的。
首先名字肯定有一个正式的名字比较合理,另外如果有其他的一些信息,
你肯定会觉得很冗余,比如
C++中的继承(二),c++,c++,java,数据库

数据冗余的本质是空间浪费

所以不要搞出菱形继承,非常非常坑。

虚继承

菱形继承怎样解决数据冗余和二义性呢?
这里引入了一个新的东西,虚继承。
C++中的继承(二),c++,c++,java,数据库

C++中的继承(二),c++,c++,java,数据库
它们都变成了同一个。所以也得出一个结论,监视窗口看到的不一定是真实的,
它们都是被处理过的。

从实际的角度,可以用多继承,但是不要用菱形继承。
但是从学习的角度,我们还得学一下这个菱形继承。

这样是不是菱形继承?
C++中的继承(二),c++,c++,java,数据库

是的,它都有数据冗余和二义性。

虚继承的底层

虚继承是如何解决数据冗余和二义性的?
虚继承虽然解决了数据冗余和二义性,但是这个过程是很难看的。
监视窗口已经看不出它最真实的面目。它的底层不是这样的。

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
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";
	// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
	a.Student::_name = "xxx";
	a.Teacher::_name = "yyy";
}

int main()
{
	Test();
	return 0;
}

C++中的继承(二),c++,c++,java,数据库
这里虽然有三个_name,但是每个_name都是一样的。

它的底层到底是怎么样呢?
物理上它的底层到底是怎么样呢?这里要换一个角度去看,
监视窗口已经看不到真实的东西,我们这里看一下内存窗口,内存窗口是真实的,不加修饰的。

我们这里用一个简化的类模型,方便我们看。

class A
{
public:
	int _a;
};
// class B : public A
class B : virtual public A
{
public:
	int _b;
};
// class C : public A
class C : virtual public A
{
public:
	int _c;
};
class D : public B, public C
{
public:
	int _d;
};

ABCD依次构成继承。
C++中的继承(二),c++,c++,java,数据库

看它的底层是什么样的,我们先看不看虚继承,就看菱形继承。
C++中的继承(二),c++,c++,java,数据库

接下来我们看虚继承。
C++中的继承(二),c++,c++,java,数据库

对象模型相比刚才已经发生了本质的变化。

注意看这里面还有我们不认识的两个东西,这些东西是什么?
00 aa cd 8c
00 aa cb ac
这两个有点像指针,而且距离也不远

它们具体是什么呢,我们再用一个内存窗口观察一下。
注意这是小端,然后把它调成4,因为编译器是32位的,指针也是32位的,并且存的是整型,正好对齐方便观察。

C++中的继承(二),c++,c++,java,数据库

这是怎么回事,这个地址指向的空间是个0, 并且下面有一个值。
这个值到底是什么?
C++中的继承(二),c++,c++,java,数据库
所以这两个值是偏移量,也是相对距离。

之前是B里有一个a,C里也有一个a,那这就造成数据冗余和二义性。
现在把一个a放到公共的空间去,然后通过偏移量去找。

那现在问题来了,为什么不直接存a的地址呢?
直接存a的地址,这样不是更好吗?

大家注意,这个偏移量它没有存到第一个位置,这个位置是空出来的,为以后的
多态做准备。
C++中的继承(二),c++,c++,java,数据库

如果这里存a地址,只解决了一个问题。而如果这个地方存放一个指针,指向一个表,这张表
可以存很多其他信息。
C++中的继承(二),c++,c++,java,数据库

什么情况会涉及刚才这样一块问题呢?
C++中的继承(二),c++,c++,java,数据库
以前的赋值兼容转换直接切割就可以了,现在直接切父类没毛病,但是不完整,还有一个a.

这个a在哪呢,在切的时候就涉及一个问题,找到对应的a。
怎么找呢?拿到对应的偏移量,然后计算才能找到a.

第二种就更复杂了。(这里还是比较难的)
ptrc指向哪里?
不是指向最开始的地方,多继承指针会发生偏移。
C++中的继承(二),c++,c++,java,数据库

虚继承还有更复杂的问题。b对象的对象模型是什么样的?
C++中的继承(二),c++,c++,java,数据库

按照我们以前的理解,b里面有一个_a,有个一个_b,现在实际并不是这样。
虚继承影响了这块。它要保持一致。
C++中的继承(二),c++,c++,java,数据库
也就意味着这里面有两种情况,这两个代码看起来一样,实际上跑起来天差地别。
C++中的继承(二),c++,c++,java,数据库
一个指向d对象,一个指向b对象。它们的偏移量也是不一样的。

但是它们的汇编指令是一样的。
它们去访问a是一样的,都是找第一个位置的地址。拿这个地址找到指向的表,
找到偏移量,然后计算找到a.

验证一下上面说的
一个是普通赋值,一个是切片
C++中的继承(二),c++,c++,java,数据库
这样它的模型就对上了,它不需要区分是子类对象还是父类对象,它的动作是一样的。
C++中的继承(二),c++,c++,java,数据库

汇编指令是一样的。

虚继承不是要解决数据冗余的问题吗?怎么还变大了?
这块变大了,是因为a太小了。它要解决数据冗余是有成本的,增加两个指针。
但是这个成本是固定的,而这个数据的大小是不确定的。

为什么不需要考虑指向的空间?
一个类可能定义很多很多对象,每个对象空间都要多8个字节,
但是这个空间不是每个对象独立的,是共同分担的,因为它们的偏移量是不变的。
真正的消耗并不在这里。

自己可以单独去验证一下。

小问题
1.如果A有多个成员,需不需要增加指针?
不需要,首先偏移量不会变,并且它是按照声明顺序去访问的。
内存对齐并不会影响这个。

举个例子。ptr是如何访问这些成员的?
这里也有内存对齐。编译器也是根据内存对齐的规则去算的。

C++中的继承(二),c++,c++,java,数据库

写编译器的人真的是高手中的高手

虚继承是有一定的效率损失的。

在实际当中我们不要去玩菱形继承,效率上有损失而且出问题了很难分析。

看一下下面这道题,结果是什么,看一下你还想不想玩菱形继承。
C++中的继承(二),c++,c++,java,数据库
C++中的继承(二),c++,c++,java,数据库

这道题也没什么,就是在虚继承的基础上加入了构造函数。
这里打印顺序是什么?
这里面调用三次A的构造函数,难道打印了三次A吗?
打印三次A就意味着A被初始化三次。看起来好像这样。

编译器肯定做了很多特殊处理。
A的构造函数实际应该是调用一次,因为只有一份A.
C++中的继承(二),c++,c++,java,数据库
现在还有一个问题,调的这个A,是B里的A,还是C里的A,还是单独的A?
肯定是D里面单独调用这个A最好。外面单独去搞更好。

这里面的运行顺序是怎样的呢?
C++中的继承(二),c++,c++,java,数据库
为什么先调A?
因为初始化列表初始化的顺序跟出现的顺序无关,跟声明的顺序有关,
谁先声明谁先初始化。
注意,谁先被继承谁就先声明
C++中的继承(二),c++,c++,java,数据库

这样出题难度更大。
实际结果没有变。
C++中的继承(二),c++,c++,java,数据库

组合

什么是组合?举个例子。
C++中的继承(二),c++,c++,java,数据库
D想复用C,可以像上面这样复用。

单从关系来说AB的继承关系更紧密一些,还是CD的组合关系更紧密一些?
也就是耦合度,继承的耦合度更高一些,为什么?
C++中的继承(二),c++,c++,java,数据库
实际当中组合更好
我们之前的适配器就用了组合。

为什么还要用继承?
有些关系适合继承那就用继承。另外多态的基础必须是继承。

C++中的继承(二),c++,c++,java,数据库
两个都可以那就用组合,适合用继承就用继承,
适合用组合就用组合。

容器里面有没有迭代器?
容器里面是没有迭代器的,除了vector.
容器只是通过begin()去获取那个位置的迭代器,并不是里面有。文章来源地址https://www.toymoban.com/news/detail-765108.html

到了这里,关于C++中的继承(二)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包