C++篇——继承
目录
引言
1.继承的概念及定义
1_1,继承的概念
1_2, 继承定义
1_2_1,继承关系和访问限定符
1_2_2,继承基类成员访问方式的变化
2.基类和派生类对象赋值转换
3.继承中的作用域
4.派生类的默认成员函数
构造函数
拷贝构造函数
赋值运算符重载
析构函数
总结
5.继承与友元
6. 继承与静态成员
7.复杂的菱形继承及其问题
7_1,菱形继承
7_2,菱形继承的问题:
8,菱形虚拟继承
8_1,虚拟继承的使用
8_2,虚拟继承解决数据冗余和二义性的原理
引言
前面咱们手撕了 string,vector 和 list 这一期咱们来学习C++里面一个比较复杂的语法——继承
1.继承的概念及定义
1_1,继承的概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
#include <iostream>
#include <string>
using namespace std;class Person
{
public:void Print(){cout << "_name : " << _name << endl;cout << "_age : " << _age << endl << endl;}protected:string _name = "xxx";int _age = 18;
};class Student : public Person
{protected:int _stuid; //学号
};class Teacher : public Person
{
protected:int _jobid; // 工号
};int main()
{Person p1;p1.Print();Student st1;st1.Print();return 0;
}
Student 公有继承了 Person,Student 里面也包含了Person 的成员变量和成员函数,在Sudent里面可直接使用,就像自己的成员变量和成员函数一样
1_2, 继承定义
定义格式
1_2_1,继承关系和访问限定符
1_2_2,继承基类成员访问方式的变化
不可见:
在类和对象篇中,我们知道在类外面我们是不能访问类的private 和 protected的成员,只能访问类的公有成员,在类和对象篇中private 和 protected 的区别并不是特别明显
而在继承中,不论派生类(子类)采用什么继承方式,对于基类(父类)的private成员,无论在子类内还是在子类外,都是不能访问的,这种状况叫做不可见。
总结:
1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私 有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面 都不能去访问它。
2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在 派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他 成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。
4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过 最好显示的写出继承方式。
5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡 使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里 面使用,实际中扩展维护性不强。
2.基类和派生类对象赋值转换
~派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片 或者切割。寓意把派生类中父类那部分切来赋值过去。
~基类对象不能赋值给派生类对象。
~基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。
代码正常运行,这里咱们可以看见,子类的对象是可以赋值给父类的对象,父类的指针,父类的引用。
而当用父类的对象给子类赋值的时候就直接报错了,这里就可以看出,基类的对象是不能赋值给派生类对象的。
这里我们可以看见基类的指针(引用也是可以的,这里没有演示)可以通过强制类型转换给派生类的指针(或者引用)。但是必须是基类的指针是指向派生类对象时才是安全的。
3.继承中的作用域
1. 在继承体系中基类和派生类都有独立的作用域。
2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏, 也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
4. 注意在实际中在继承体系里面最好不要定义同名的成员。
这里咱们可以看见,派生类和基类存在一个成员变量_num重名,这个时候形成了隐藏(或者叫重定义),子类成员将屏蔽父类对同名成员的直接访问,在语句 cout << _num << endl 中, _num为派生类的_num,所有打印出来的是999。
当然也是有方法访问基类的_num的,在子类成员函数中,可以使用 基类::基类成员 显示访问。
4.派生类的默认成员函数
6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类 中,这几个成员函数是如何生成的呢?
构造函数
1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
这里我们可以看出来,我们是不能在派生类的初始化列表对基类的成员变量进行初始化的。
我们需要在这里去调用基类的构造函数。
拷贝构造函数
2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
赋值运算符重载
3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
析构函数
4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
所以析构函数是比较特殊的一个,为了保证先先子后父的析构顺序,编译器会在子类析构结束后自动父类的析构函数
总结
派生类对象初始化先调用基类构造再调派生类构造。
派生类对象析构清理先调用派生类析构再调基类的析构。
因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲 解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加 virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。
派生类的默认成员函数的核心就是:各司其职,各干各的。
5.继承与友元
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
6. 继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子 类,都只有一个static成员实例
可以形象的理解为,派生类仅仅是继承了基类静态成员的使用权。
7.复杂的菱形继承及其问题
7_1,菱形继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承:菱形继承是多继承的一种特殊情况。
这里我们定义了4个类,Student,Teacher 继承了 Person,Assistant继承了Student 和 Tearcher
7_2,菱形继承的问题:
从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。 在Assistant的对象中Person成员会有两份。
这里就很好的演示了二义性的问题,Tearcher 和 Student 都有 _name 成员变量,无法明确知道访问的是哪一个
二义性的解决方法非常的简单,明确类域就可以了,但是数据冗余的问题还是无法解决。
8,菱形虚拟继承
8_1,虚拟继承的使用
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。
如上面的继承关系,在Student和 Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用
虚拟继承的使用格式是在继承方式前面加上 关键字 ” virtual “
这里我们可以看见二义性的问题已经被解决了。
8_2,虚拟继承解决数据冗余和二义性的原理
为了研究虚拟继承原理,我们给出了一个简化的菱形继承继承体系,再借助内存窗口观察对象成 员的模型。
菱形继承
很明显的看出 B中的 _a 和 C 中的_a是相互独立的,数据冗余
菱形虚拟继承
这里可以分析出D对象中将A放到的了对象组成的最下面
这个A同时属于B和C,那么B和C如何去找到公共的A呢?这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A。
根据窗口显示,可以推测偏移量是用 int 存储的,虚基表的第一个位置是不存储有效数据的,从下一个位置存储偏移量。