全网最全的c++继承包含了菱形继承,虚拟继承,虚基表等内容

所谓无底深渊,下去,也是前程万里。

1.继承的概念及定义

1.1继承的概念

继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。列如以下的代码就是一个简单的继承关系。

class A {
public:
	A() {
		cout << "A" << endl;
	}
	int a;
};
class B : public A{
public:
	B() {
		cout << "B" << endl;
	}
	int b;
};

1.2继承的定义

在这里插入图片描述

1.3继承关系和访问限定符

上面呢我们可以看到B是子类也叫派生类,A是父类也叫基类,public是继承方式。
那么有public继承方式肯定也有别的继承方式喽,没错也有别的继承方式,并且是有组合的,组合的结果呢就是下面那张图片。
在这里插入图片描述

1.4继承基类方式访问成员的变化

在这里插入图片描述

1.5总结

  1. . 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它
  2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
  3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public>protected > private。
  4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式
  5. 我们在一般的使用中也一般都是使用的public继承因为我们的主要目的是为了代码复用所以很少使用private以及protect并且也并不推荐使用。

2.基类和派生类的赋值转换

  1. 基类对象可以赋值给基类的对象/基类的指针/基类的引用。这里有个形象的说法寓意是值将父类的那一部分切割过去。

  2. 基类对象不能赋值给派生类对象。

  3. 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类时才是安全的。这里基类如果是多态类型,可以使用RTTI的dynamic cast来进行识别后进行安全转换

     可以看一下下面这个代码。比较全面的说明类赋值转换。
    
#include<iostream>
using namespace std;
class person {
public:
	person()
	{
		cout << "person" << endl;
	}
};
class studen :public person 
{
public:
	studen()
	{
		cout << "studen" << endl;
	}
};
int main()
{
	studen f;
	person son;
//	父类的指针和派生类的指针
	studen*s=&f;
	person* pf = &son;
//	派生类的地址可以赋值给父类的指针
	pf = &f;
//	父类的地址不可以赋值给派生类的指针
	s = &son;//这里在编译器中会报错
	return 0;
}

3.继承中的作用域

  1. 在继承的体系中派生类和基类都有自己独立的作用域。
  2. 子类和父类中有同名成员,子类成语将屏蔽父类对同名成员的直接访问,这种情况叫隐藏也叫重定义。(在子类成员函数中可以使用基类::基类成员显示访问)
  3. 这里要注意如果是成员函数的隐藏的话只需要成员函数同名就可以了。
  4. 注意在实际中在继承体系里面最好不要定义同名的成员。
#include<iostream>
using namespace std;
class person {
public:
	int _num = 111;
	string name = "张三";
	void print()
	{
		cout << _num << ":_num" << endl;
		cout << name << ":name" << endl;
	}
};
class studen :public person 
{
public:
	int _num = 999;
	string name = "李四";
	void print(int val = 10)
	{
		cout << _num << ":_num" << endl;
		cout << name << ":name" << endl;
	}
};
int main()
{
	studen f;
	person son;
	f.print();
	return 0;
}

上面的这个列子大家可以看一下,我们可以看出来即使派生类和基类的成员函数的类型并没有保持一只但是在调用的时候仍然是调用的派生类的print函数。

4.派生类的默认构造函数

6个默认构造函数,默认的意思就是我们不写但是编译器会自动生成一个,这几个函数主要又以下几个。

  1. 派生类的构造函数必须调用基类的构造函数从而初始化基类的成员。如果基类没有默认的构造函数那必须在派生类的显示列表中显示调用。
  2. 派生类的拷贝构造函数必须调用基类的拷贝构造从而完成基类拷贝的初始化
  3. 派生类的operator=必须调用基类的operator=完成基类的复制。
  4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员因为这样才能保证派生类对象先清理派生类对象在清理基类成员的顺序。
  5. 派生类的初始化先调用基类的构造函数再调用基类的构造。
  6. 派生类对象析构清理先调用派生类析构再调基类的析构。
  7. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个大家有兴趣可以接下来看一下我的另一个文章 多态)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加\virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。

5.继承和友元

这里的话其实大家记住一句话就可以了那就是友元关系不可以继承,这里的话其实很好理解,那就是你爸爸的朋友他不一定就是你的朋友。换句话也就是基类的友元函数不能访问派生类的保护和私有成员。

6.继承和静态成员

基类定义了一个static 成员那么无论继承了多少次在这整个继承体系中只有一个这样的成员。无论派生了多少次都只有一个static实列。我们可以通过一下代码进行证明。

#include<iostream>
using namespace std;
class person {
public:
	 static int cot;
	int _num = 111;
	string name = "张三";
	void print()
	{
		cout << _num << ":_num" << endl;
		cout << name << ":name" << endl;
	}
};
int person::cot = 0;
class studen :public person 
{
public:
	int _num = 999;
	string name = "李四";
	void print(int val = 10)
	{
		cout << _num << ":_num" << endl;
		cout << name << ":name" << endl;
	}
};
class studen2 :public person {
public:
	int _num = 999;
	string name = "王五";
};
int main()
{
	studen son;
	person f;
	studen2 stu;
	stu.cot++;
	cout << f.cot << endl;
	son.cot++;
	cout << f.cot << endl;
	return 0;
}

在这里插入图片描述
上面我们可以看到无论是从派生类1或者派生类2对cot变量进行修改最终都会影响到cot本身当然了也可以直接从地址上进行更加直观的观察。

7.复杂的菱形继承及菱形虚拟继承(本节重点)

在开启菱形继承前我们首先要知道两个概念分别是单继承和多继承。

7.1单继承

一个子类只有一个父类时这个继承关系成为单继承如下图

在这里插入图片描述

7.2多继承

多继承指的就是一个子类拥有不止一个父类。如下图

在这里插入图片描述
好的那么我们知道了单继承和多继承之后呢我们可以发现多继承的这种形式就会出现一个特殊的情况,那就是菱形继承。

7.3菱形继承

通过上面的说明我们可以知道菱形继承其实就是多继承的一种特殊形式那么用图的形式表示出来是什么样子呢请看下图。
在这里插入图片描述
就是这样子,可能有人会疑惑这样子有什么危害呢?我们可以试着画一下如果发生这种继承的话那么Assistant这个类对象的内部会保存哪些组成呢?
在这里插入图片描述
请看上图根据我们的菱形继承,Assistant类实列化出来的对象会保存的有这些值,这里面主要有Assistant自己本身的成员,以及student的成员,以及teacher的成员,那么这里我们会发现,student和teacher这两个成员中有一个值相同但是地址不同的地方那就是person。
那么我们可以这样理解假如说Assistant是一个人,他的名字保存在person中我们访问person可以从student访问也可以从teacher访问,也就是说这个人有两个名字,他作为teacher时使用的是一个名字,他作为student时却是一个名字,那么这样子同学们可能感觉很合理啊,我作为老师大家可以叫我陈老师,张老师,作为学生可以叫我陈同学,张同学。可是在这跟取别名不一样的地方再与取别名是我任何时候都可以这样子代表你就好比你小名叫皮蛋那么在抛开礼节的情况下,我可以在家里叫你,在学校叫你,你成为老师后我依然可以叫你,但是这里可以吗?显然不可以因为这里的person是单独存在于student以及teacher中的也就是说就好比,你是学生的时候我叫你陈同学,但是当你变成老师的时候我再叫你陈同学我就不是再叫你了这显然很不合理。

	我给大家写一个代码帮助大家的理解	
class Person
{
public :
 string _name ; // 姓名
};
class Student : public Person
{
protected :
 int _num ; //学号
};
class Teacher : 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";
}

7.3.1菱形继承的缺点

从上面我们的描述中呢我们发现了菱形继承可以说是弊大于利的,那么菱形继承的缺点我们总结起来其实就是,会导致,数据冗余,以及二义性。
而在java中呢就没有菱形继承这个东西因为它就没有多继承,其实这也是java抄c++作业一个很不错的地方为什么c++被很多人吐槽难学其实也正是这个原因因为它有太多的就是站在当时那个角度没有考虑进去的问题了。

7.3.2如何改进菱形继承(虚拟继承)

那么正是因为菱形继承我们如果对c++的迭代有些了解的同学呢就会发现在之后的两个版本中祖师爷可以说想尽了办法去修补菱形继承这个缺点,最终呢解决办法就是引进了虚拟继承。

	那么什么是虚拟继承呢?
	虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和
Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地	
方去使用。

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

为了研究虚拟继承原理,我们给出了一个简化的菱形继承继承体系,再借助内存窗口观察对象成
员的模型。
首先呢下面这张图是没有进行虚拟继承的样子那么虚拟继承其实就是将腰部 的继承关系加一个virtual代码我给大家粘贴一下

//未进行虚拟继承的代码
#include<iostream>
using namespace std;
class A {
public:
	int _a=1;
};
class B :public A {
public:
	int _b=2;
};
class C :public A {
public:
	int _c=3;
};
class D :public B, public C {
public:
	int _d=4;
};
int main()
{
	D d;
	A a;
	B b;
	C c;
	return 0;
}



//虚拟继承的代码如下
#include<iostream>
using namespace std;
class A {
public:
	int _a=1;
};
class B :virtual public A {
public:
	int _b=2;
};
class C :virtual public A {
public:
	int _c=3;
};
class D :public B, public C {
public:
	int _d=4;
};
int main()
{
	D d;
	A a;
	B b;
	C c;
	return 0;
}
未进行虚拟继承的图片如下

在这里插入图片描述

接下来是进行虚拟继承的图如下

在这里插入图片描述

上面两张图片我们可以看一下区别首先呢我们发现原来保存A的地方保存成了一个地址,而A呢则出现了最底部那么我们可以看一下原来保存A的地方保存的东西是什么。
在这里插入图片描述
在这里插入图片描述
采用内存窗口查看的时候我们发现这里面存的是一个整数。
那么这个整数有什么意义呢?其实这个整数就是B和C距离A的地址的偏移量,这里其实就是两个指针当我们需要读取A的时候指针先读取到偏移量,然后呢根据偏移量找到A的地址从而访问A那么这里。而存储偏移量的这个表就被称为虚基表

7.3.4虚基表

首先什么是虚基表

通过我们上面的描述虚基表存储的是腰部派生类到达基类的偏移量,通过偏移量来找到基类的地址从而实现我们可以找到访问基类,从而只保存一个基类的同时又可以让B和C访问到基类(A)。

好的那么还有一个问题就是为什么要这样做

为了搞清楚这个问题我们要先知道虚基表是干什么的,通过上面我们可以知道虚基表是为了存储偏移量的。

为什么要存储偏移量呢? 因为为了找到基类的地址从而可以访问基类。

那么为什么我们不直接存储基类的地址,而是先存储偏移量的地址,再读取偏移量呢? 那么这个问题其实是因为我们太局限了,我们要明白一个问题,这个继承体系中有多少个菱形继承?很明显只有一个那么我们以后还会遇到不止一个菱形继承比如说。下图这个样子
在这里插入图片描述
如果说我们选择了存储基类的地址的话那么图中我们需要存储多少个基类的地址呢?很明显是两个一个是A 一个是E而如果我们采取虚基表的方式的话我们只需要存储一个偏移量的地址,然后通过偏移量依次读取B和C的偏移量直接算出来即可。这样的话不就节省了更多的空间了吗?

虚基表的好处

虚基表有什么好处呢?首先肯定是解决了菱形继承的导致的数据冗余和二义性的问题。那么还有一个好处就是我们提到的切片操作,在切片的时候我们需要偏移量来保证我们切的准确,因此这里的话可以直接访问虚基表就可以了。

关于继承的总结

  1. 很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱
    形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设
    计出菱形继承。否则在复杂度及性能上都有问题。
  2. 多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java。
  3. 我们在使用的时候优先使用组合,其次使用继承。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值