一.继承的概念
在类的基础上进行扩展,扩展出方法(成员函数)和属性(成员变量), 进而形成新的类,被称为子类。(父类(子类公共的属性)单独封装一个类,让子类继承这个类,同时子类与子类间又做出细分(即每个子类里包含独属于它的成员变量或函数))。 这么干就实现了共有成员变量的复用,告别了冗余。
小试牛刀一下:


#include<iostream>
using namespace std;
class Person
{//老师,学生两个类共有的成员变量以及成员函数,单独写成一个类,用于复用,告别冗余。
public:
void identity()
{
cout << "void identity()" << _name << endl;
}
protected:
string _name = "张三";
string _adderss;
string _tel;
int _year = 18;
};
class Student : public Person//复用
{
public:
void study()
{
cout << "void study()" << endl;
}
protected:
int _stuid;
};
class Teacher : public Person
{
void teach()
{
cout << "void teach()" << endl;
}
protected:
string _title;//职称
};
int main()
{
Student s1;
Teacher t1;
s1.identity();
t1.identity();
return 0;
}
二.继承复用的格式

继承方式的细究
注:子类...成员指的是父类成员在子类中的访问方式。

继承方式,父类成员的限定符:public,protected,private
子类从父类继承来的成员,实际的访问方式并不由自己想要的继承方式(继承方式)单方面决定的,是由父类成员限定符和继承方式同时来决定:



父类其他成员(除private外)在子类访问方式:
public: 子类从父类中继承,子类里外都能对继承来的成员进行直接访问。
private,protected:能继承,子类里可直接访问,子类外无法直接访问。
父类private限定的成员在子类的访问方式:
能在子类里继承,但子类内外均无法直接访问。
但对于成员变量无法直接访问的情况,假如父类里某个成员函数在子类里或子类外能直接访问,则可以借助这个函数完成间接访问。
三.模板的继承
在实现栈时,可不用适配器那个模式,可以通过继承vector来复用相关成员。
注意:



要体现继承来的成员函数已实例化,需添加vevtor<T>。
四.父类和子类对象赋值兼容转换
注意:讨论下面内容的前提是继承方式为public!!!!!!!!

当子类实例化成对象时,能将子类对象(继承父类的相关成员)赋值给父类对象,就相当于一个切片:

代码实现:

当然,是不能将实例化父类对象赋值给子类对象的,因为父类缺乏子类拥有的成员,此时若赋值则无值可赋给子类对象:

但父类指针在一定程度上是允许赋值给子类指针的:

如上图,最终子类指针指向子类空间,在逻辑上也是合理的。因此会使用dynamic_cast(多态章会讲)来判断一下父类指针是指向子类还是父类的空间。若指向子类空间,会返回地址;若指向父类,则返回0,就无法赋值给子类指针。
五.继承的作用域
1.子类和父类各有各的作用域。
2.隐藏

此时若通过打印的方式来访问_num,究竟访问的是哪一个类里的呢?
答案是子类里的_num:

原因在于当子类和父类具有同名成员时,子类成员会对父类同名成员的直接访问做出屏蔽,这种情况叫隐藏。若要访问父类的成员,需使用父类::父类成员,指定作用域来访问。
访问父类同名成员:


父类和子类间成员函数满足隐藏的条件是函数名相同即可。
小试牛刀,but大坑:


一般看到函数名相同,参数类型不同,直接一个重载就上去了,但这是大错特错的。因为函数重载要求在同一作用域,但A类,B类在不同作用域,且B类继承了A类,而且两个成员函数的函数名又是相同的,那么就满足以上关于成员函数隐藏的定义,因此是隐藏关系。
注意:在继承关系的类之间,不到万不得已,不要定义同名成员,不然容易被隐藏坑到!!
六.子类默认成员函数
针对父类继承给子类的成员变量,系统就会去调用父类的默认成员函数
1.默认构造


父类不提供默认构造(无缺省值)的情况:

初始化列表显示调用:


2.拷贝构造



调用父类拷贝构造遇到的问题:

假如在子类拷贝构造函数的初始化列表里不写调用父类拷贝构造,且父类默认构造存在缺省值,那么就会自动去调用默认构造的缺省。

小结,关于构造函数和析构函数中子类&父类的执行顺序
构造函数:先调父类的构造函数,再调子类的。
析构函数:先析构子类再析构父类。
3.operator=



但上面的代码还是有问题的,会栈溢出。因为父类里跟子类一样有同名的operator=,与子类构成了隐藏,就会屏蔽父类的operator=,反复执行子类的该函数,造成了死循环,最终栈溢出。所以要调父类该函数,需要声明父类的类域:

那么这个问题是怎么发现的呢,就得提到一个功能------监视堆栈:

就积累到了经验,当出现栈溢出时,一般原因是递归深度过深:
可看到系统在不断调用子类的operator=,构成了一个死循环的递归,就栈溢出了

4.析构

wtf,父类析构调不动?


子类父类的析构函数名全是destructor(),既然构成隐藏就需要指定作用域,具体解释我们多态章再见吧。

但子类析构是不需要显示调用父类析构的:

七.实现不能被继承的类

八.友元继承

测试友元函数能否被子类继承:

即Display是Person这个父类的友元函数,但不是Student这个子类的友元函数。
想要父类友元函数访问的到子类的成员,只需在子类里也做该函数的友元声明即可:

插曲:
在测试友元是否能被继承时,会出现这样一个报错:

明明友元函数声明的形参处是不缺乏逗号的,那为什么还会这样报错?
编译器遇到函数,变量,类型的时候,为了增加效率,只会向上去寻找它们定义的代码。这时由于子类Student是定义在友元声明的下面的,那么向上去找固然找不到子类,就报错了。
此时需要在友元声明前再加一个子类的前置声明。

九.static成员的继承

十.多继承
1.三种继承方式
单继承:一个子类只有一个直接父类的继承关系。

多继承:一个子类有两个及其以上的直接父类的继承关系
在子类名称后写继承方式时,用多个逗号连接不同继承方式的父类 就行了

菱形继承:一个子类有两个直接父类,同时两个直接父类又同时有一个相同的直接父类。

数据冗余与二义性

由于Student类与Teacher类继承了同一父类Person,那么当Assistant类的对象访问Person类的成员时,究竟是通过Student类,还是Teacher类,这说不清楚,就产生了二义。同时两个类分别继承同一父类还造成了空间的浪费。
解决二义性的方式
1.访问时包含类域:

菱形继承实用的地方:io库


为了使iostream同时继承istream和ostream的成员,只能这么干。
为了同时解决二义性和冗余的问题,我们引入虚继承。
2.虚继承
在拥有冗余父类的两个子类前加virtual ,就能使它俩共用同一个空间的父类,使得重复的两份合二为一。

插曲
下面这个图,算是菱形继承吗?

答案是当然算,因为类E访问类A的时候,依旧存在着A类的成员,由哪个子类继承的二义性 。还有B,C在不同空间同时继承A的冗余性。
那么用虚继承解决这两个问题时,virtual加在哪呢?
答案是B和C

原因在于,哪个类产生了数据二义性和冗余,就该在继承它的两个子类处添加virtual。
尽量不要去设计菱形继承,底层和逻辑上水太深,把握不住的!!!!!!!!
十一.继承和组合


如果父类子类间是继承关系,父类会将所有public或protected的成员开放给子类,那么它俩耦合度过高,父类稍微改动,就会对子类影响很大。
而组合关系里,父类仅开放部分接口给子类,那么它俩耦合度就很低。父类改动,子类影响就很小。