1 继承的定义
继承的概念:就是把某些类中共有的成员变量或者是成员方法抽取出来,形成一个类,为了更好的实现代码的复用!
继承定义的格式如下:
那么联想到之前所学的访问限定符,在结合今天所学的继承方式。那么在子类中对于父类成员访问方式的变化可以总结如下:
举个例子来说明一下吧!如果我们采用public继承方式,父类中的成员变量是private修饰,那么此时子类就不能对父类成员变量进行访问,也就是在子类中不可见,不可见是指父类的私有成员还是被继承到了子类对象中,但是语法上限制子类对象不管在类里面还是类外面都不能去访问它!
总结一下:
1️⃣除了父类成员变量是用private修饰的,在子类中不可见!其他情况下,子类对于父类成员变量的访问方式取决于父类成员变量访问限定符和继承方式中更小的那个权限
2️⃣class与struct区别就是:class默认的访问权限是private,在继承时,也是默认private方式继承。而struct恰恰相反,默认的访问权限是public,继承方式也是默认public,但是我们更加提倡更直观的写出继承方式!
3️⃣一般我们都是用public继承方式更多!
2 子类和父类对象赋值转换
我们先来回顾一下之前学过的一个代码片段:
为什么这里就会报错呢?原因很简单,就是类型不一样,赋值的过程中是会产生一个临时变量!如下图所示:
而我们知道临时变量是具有常性的,只能读不可以写的,而默认的引用是可读可写的,所以我们就必须在double前面加上const!
那我们来看下面这段代码:
那么为什么这里就没有报错呢?而且这里做的原理是什么呢?首先我来回答第一个问题,因为这里就是C++中的一个规定,这里就不会产生临时变量!上述赋值过程可以用下图来进行表示!
所以就是子类对象可以赋值给父类的对象/父类的指针/父类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。还要注意的就是父类的对象是不可以赋值给子类对象的!
3 继承中的作用域
子类与父类是独立的作用域!如果子类与父类中有相同的名字的成员变量,那么想要访问父类中的那个成员就必须指定对应的作用域!所以一般不建议取有重名的成员变量!
3.1 重定义(隐藏)
定义:子类和父类中存在相同名字的函数就构成了重定义(隐藏),不管参数类型以及返回值是什么,包括我们上面将的有同名的成员变量,也叫做重定义(隐藏)
例子如下:
在这里,即使子类与父类的参数不同,返回值不同,也是调用不到父类中的print的!所以我们想要调用父类的,就必须指定作用域!还需要注意的是,重载是在同一个作用域下面,参数不同,函数名一样就称之为重载!
4 默认的成员函数
我们采用以下代码来理解一下,在继承中,默认的成员函数是怎么做的。
class Person { public : Person(const char* name = "peter") : _name(name ) { cout<<"Person()" <<endl; } Person(const Person& p) : _name(p._name) { cout<<"Person(const Person& p)" <<endl; } Person& operator=(const Person& p ) { cout<<"Person operator=(const Person& p)"<< endl; if (this != &p) _name = p ._name; return *this ; } ~Person() { cout<<"~Person()" <<endl; } protected : string _name ; // 姓名 }; class Student : public Person { public : Student(const char* name, int num) : Person(name ) , _num(num ) { cout<<"Student()" <<endl; } Student(const Student& s) : Person(s) , _num(s ._num) { cout<<"Student(const Student& s)" <<endl ; } Student& operator = (const Student& s ) { cout<<"Student& operator= (const Student& s)"<< endl; if (this != &s) { Person::operator =(s); _num = s ._num; } return *this ; } ~Student() { cout<<"~Student()" <<endl; } protected : int _num ; //学号 };
可以自己先创建一个Student类的对象,调用一下就可以了解
1️⃣子类的构造函数必须调用父类的构造函数初始化基类的那一部分成员。如果父类没有默认的构造函数(或者你要改变父类中的成员),则必须在子类构造函数的初始化列表阶段显式调用。并且是先调用完父类的构造函数,在去调用子类的构造函数
2️⃣子类的拷贝构造函数必须调用父类的拷贝构造完成父类的拷贝初始化。
3️⃣ 子类的operator=必须要调用父类的operator=完成父类的复制。并且必须指定类域,因为函数名相同已经构成了隐藏
4️⃣在析构函数中,不用去调用父类的析构函数的,会先清理完子类中的资源,然后自动调用父类中的析构函数,从而清理父类中的资源
5️⃣子类与父类之间的析构函数构成了隐藏关系,因为编译器会对析构函数名进行特殊处理,处理成destrutor()
5 多继承
单继承:顾名思义,就是只继承了一个父类
多继承:就是继承了多个父类
5.1 菱形继承
在C++中,正是因为有多继承的出现,所以就会出现菱形继承!如下图所示:
D类继承了B,C两个类,然后B,C两个类都继承了同一个类,这种情况就叫做菱形继承!
菱形继承就会带来以下两个问题
1 成员变量的二义性(就是不确定性)
2 代码冗余
例如下图所示
这就是一个典型的二义性问题,这是为什么呢?让我来画一个内存图来理解一下:
因为D类继承了B,C两个类,而B,C两个类又继承了A类,所以在D对象中a这个成员变量会存储两遍!如果访问成员变量a,那么到底是B类中的a呢,还是C类中的a呢?当然我们可以指定类域来进行访问!但是如果A中的成员变量不止是一个a呢?那代码的冗余程度是不是就大了!
5.2 解决办法
那么如何解决菱形继承问题呢?在C++中采用了虚继承的方式来解决!如下图所示:
那么此时对应的内存图又是张啥样的呢?如下图所示:
此时成员变量a就只是存储了一份,并且B,C类中的成员中存储的是地址,加上偏移量,就可以指向成员变量a所在的地址,从而就找到了a了!
6 组合与继承
什么是组合呢?如下图所示就是一种组合
就是在一个类中,用到了另一个类中的方法或者成员函数!本质上其实也是一种代码的复用!像我们前面所学过的反向迭代器就是一种组合!
组合和继承的区别在哪里?
1️⃣ 组合的耦合性更低,继承的耦合性更高!举个例子说明一下,如果一个项目需要改动代码,如果是在继承关系里面改动的,那么成员函数或者变量的改变就会影响其他模块也必须跟着改,但是如果是组合,只有公共的成员函数或者函数改变,才会影响着对应的模块改变!
2️⃣组合本质上就是一种黑箱复用,你只需要知道组合使用的那个类,可以实现那些个功能就可以了,不用关心实现的细节!而继承也是一种白箱复用,我们需要了解父类中是如何实现的,而不是仅仅关注功能!要求会更高
3️⃣组合就是一种 has-a的关系,继承则就是一种is-a的关系!
对于继承与组合的使用,在这里建议尽量使用组合,降低代码的耦合性!