第七课 继承

- 继承了抽象类型,保证代码不会因为子类的修改而变动,有利于今后的维护
- 增量开发:可以直接在
List类下直接增加LinkedList,而不影响类库 - 继承:父类中的所有部分都必须在子类中有
- 继承主要是为了继承类型,而不是继承父类的代码
**class**的默认访问权限是**private**,**struct**的默认访问权限是**public**- 类中的函数编译时和其他普通函数一样
classname::function(classname *const this) - 派生类调用基类的函数,向基类传入自己的指针 – 允许派生类隐式转换为基类
- 可以理解为派生类中内嵌了一个基类对象:基类中的
private成员代表派生类中存在基类的私有变量,但逻辑上不可见,只能通过基类的公开方法访问。 - 派生类中可以访问基类中的
protected成员:如果额外创建了一个对象student,此时不可以访问,只有自己继承父类student,才可以访问其中的变量。因为额外创建的对象不是自己的,继承的是自己的。如果额外创建了一个父类对象,则不可以访问其成员1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37//
// Created by 13467 on 2022/11/15.
//
using namespace std;
class B {
int x = 0;
protected:
int pro = 0;
};
class D : public B {
public:
// using B::x;
int x = 0;
B *outFather = new B;
void proInFather(){
cout << pro << endl;
// 此处不会发生编译错误,因为基类嵌入在派生类的内存块中
// 继承时,不管是public继承还是private/protected继承,基类对于派生类来说,其public成员和
// protected成员都是可见的
};
void proOutFather(){
cout << outFather->pro << endl;
// 此处会发生编译错误,因为是额外创建了一个基类,而pro是保护成员,无法被
// 实例化的对象所访问,因此会发生编译错误
}
};
int main() {
return 0;
}
基类和继承类的方法关系
- 根据实际对象执行 – 在编译时期无法确定实际的对象
- 利用名空间匹配函数名,匹配函数名成功后,继续匹配参数:但是匹配名称的过程已经结,不会在退回到此步骤(性能上的考虑),所以会直接报错
- 不重新定义基类中的非虚函数
- 方法的继承:名空间的可见
- 属性的继承:都有拷贝,但不一定可见
- 访问的时候会出现名空间隐藏的问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58//
// Created by 13467 on 2022/11/15.
//
using namespace std;
class Student {
int id = 10;//id在Undergraduated_Student中仍然是私有的,默认权限位private
void SetNickName(char *s) { strcpy(nickname, s); }
public:
char nickname[16];
void set_ID(int x) { id = x; }
void showInfo() { cout << nickname << ":" << id << endl; }
void showInfo(int x) { cout << x << endl; }
void getId() {
cout << "getId" << id << endl;
} // 通过继承基类的公有方法访问到基类的私有变量
protected:
int y = 7;
};
class Undergraduated_Student : public Student {
int dept_no{};//学院编号
public:
void setDeptNo(int x) { dept_no = x; }
// void showInfo(){cout << dept_no << ":" << nickname << endl;}
void set_ID(int x) {}
void SetNickName() {
cout << "changed" << endl;
} // 对基类的函数进行覆盖,并修改了访问权限
private:
using Student::nickname;//这样在才能修改可见性
void showInfo() {
cout << y << endl;
cout << dept_no << ":" << nickname << endl;
}
//新定义了一个private方法,父类对应方法被隐藏
};
int main() {
Undergraduated_Student *us = new Undergraduated_Student;
// cout << us->y;
us->SetNickName();
us->getId();
// us->showInfo(); 无法访问,通过隐藏父类的方法,修改了访问权限
us->Student::showInfo(10); // 被隐藏了,但是可以显式的使用对应的名空间进行访问
// us->showInfo(10); //error ,因为被子类的同名方法所隐藏了
}
继承方式
继承访问权限:
默认:private
public
private:原来的
私有继承不能用在多态中
私有继承不是类型继承,只是代码复用,所以私有继承使用较少
前向声明:只是知道有这个东西,不在乎内存大小,所以前向声明的正确方式为第二种,完整声明的正确方式为第一种
1)基类成员对派生类都是:共有和保护的成员是可见的,私有的的成员是不可见的。
2)基类成员对派生类的对象来说:要看基类的成员在派生类中变成了什么类型的成员。如:私有继承时,基类的共有成员和私有成员都变成了派生类中的私有成员,因此对于派生类中的对象来说基类的共有成员和私有成员就是不可见的。
对于公有继承方式
(1) 基类成员对其对象的可见性:
公有成员可见,其他不可见。这里保护成员同于私有成员。
(2) 基类成员对派生类的可见性:
公有成员和保护成员可见,而私有成员不可见。这里保护成员同于公有成员。
(3) 基类成员对派生类对象的可见性:
公有成员可见,其他成员不可见。
所以,在公有继承时,派生类的对象可以访问基类中的公有成员;派生类的成员函数可以访问基类中的公有成员和保护成员。这里,一定要区分清楚派生类的对象和派生类中的成员函数对基类的访问是不同的。
有元和protected

只有基类直接的派生类可以访问到基类的protected,不可以传入基类对象或者创建基类对象进行访问。保护 — 只能访问内嵌的基类,否则就可以如上述案例,绕过保护作用
友元不具有传递性:不能通过成为派生类的友元来成为基类的友元
继承时构造函数的调用顺序
1、子类对象在创建时会首先调用父类的构造函数
2、父类构造函数执行完毕后,才会调用子类的构造函数
3、当父类构造函数有参数时,需要在子类初始化列表(参数列表)中显示调用父类构造函数
4、析构函数调用顺序和构造函数相反
派生类对象的初始化由基类和派生类共同完成
构造函数的执行次序
- 基类的构造函数
- 派生类对象成员类的构造函数
- 派生类的构造函数
**析构函数的执行次序 **与构造函数相反
执行顺序:
1.构造基类(因为首先要通过继承,确认成员变量)
2.成员变量初始化
3.初始化列表初始化
4.该类的构造函数
1 |
|

如果自定义拷贝构造函数
1 | B b1(10,100) |
- 如果自定义了B的拷贝构造函数,则先调用A的默认构造函数和B的拷贝构造函数,不会进行默认拷贝构造函数。需要显示的声明调用A的拷贝构造函数
- 如果没有自定义B的拷贝构造函数,会调用A B的默认拷贝构造函数
- 程序员介入了资源管理,编译器会决定什么都不干
语法糖
虚函数
类型相容

类型相容:A a; B b; class B: public Aa=b都是栈上的两个对象,会将一个内存单元的内容赋值到另一个内存单元中。但a的内存块通常比b小。赋值相容不代表精度仍然保持一致,直接丢弃超出的内容,因为赋值后身份已经发送了变化,所以不会再使用独属于派生类的属性了。b所丢失的部分就是对象切片
引用和指针都不涉及到对象的赋值,对象身份没有改变,可以使用多态
C++会灵活使用栈和堆,而Java大部分都在堆上创建对象,C++效率更高
举例1

func1调用A::f func2调用A::f
编译顺序很关键:
- 先编译
,a的地址已经定义好了,**fun1**不知道实际对象是什么,只看声明的类型是什么,编译时刻已经确定地址是什么。当出现调用时,再传递参数。
绑定顺序

- 前期绑定是通过声明的类型确定地址,效率更高😀
- 前期绑定也叫静态绑定,动态绑定也叫后期绑定
- 多态只能通过动态绑定实现,需要不断去寻找地址。
定义

基类中被定义为虚成员函数,则派生类中对其重定义的成员函数均为虚函数
在子类中重写函数,但是没显示写virtual,仍然会默认为虚函数
限制
- 类的成员函数才可以是虚函数
- 静态成员函数不能是虚函数:在全局初始化时,静态编译时就已经确定,只和类有关,和对象无关,类是通过静态绑定的。
- 内联成员函数不能是虚函数:在编译时进行代码替换,需要决定用哪一段代码进行替换,已经消除了函数调用,相当于复制了函数内容
- 构造函数不能是虚函数

不存在声明类型和实际类型不一致的情况
- 析构函数可以(往往)是虚函数
当基类指针指向派生类的时候,若基类析构函数不声明为虚函数,在析构时,只会调用基类而不会调用派生类的析构函数,从而导致内存泄露。
动态绑定的实现

- 调用
p->f因为是虚函数,所以指向a时,调用的是A中的f,指向b时,调用的是B中的f。每一次调用,都要寻找地址 - 调用
p->h,调用的都是A中的h,因为是静态绑定的,所以只和声明的类型有关,是为了提高效率
_**(**((char *)p-4))(p)**_
- 通过两次解引用找到函数,所以效率较低
举例2

举例3

非虚函数部分和当前对象保持一致,虚函数部分和实际对象保持一致
- 非虚接口:非虚函数中调用虚函数

override:参数一致,返回值也要一致
f2中override去掉是可以的,是新定义的静态函数。和虚函数同名的静态函数,也会名空间覆盖
const:不能去掉,有无const不能不能类型转换
纯虚函数和抽象类

抽象工厂模式


虚析构函数

- **析构函数是用来换额外的内存的,不是用来还自身对象的内存的,因为申请内存时是会记录内存大小的 **所以没有申请额外内存时,就不需要自定义析构函数
因为D中不会自动delete name,所以需要在D中自定义析构函数,所以需要确保调用该析构函数,所以需要定义为虚函数。
- 派生类析构函数完成后,自动调用基类的析构函数
绝对不要重新定义继承而来的缺省参数值

- 默认参数值:编译时确定的
f()将默认参数绑定在上面f(x)将参数x绑定在函数上
- 根据声明类型,找到默认参数值
动态调用默认参数值,需
对象中只记录虚函数的入口地址,不记录相关的参数,为了提高效率,除了虚函数其他都是静态编译完成的
1 |
|
好的公开继承
- 确定public inheritance,是真正意义的“is_a”关系
- 不要定义与继承而来的非虚成员函数同名的成员函数
- 最弱前置条件
- 最强后置条件
- 契约式设计
- 派生类不是代表“特殊”,因为特殊代表性质会改变

**assert(s.width() == s.height());**不变式


同一个对象,呈现了不同行为 – 名空间不同,出现错误调用
明智的私有继承
1 | class CHumanBeing { … }; |
- 私有继承,派生类无法隐式转换为基类。不能使用基类指向派生类的场景
- 转换由编译器完成,要求派生类能够公开访问基类的构造函数。
Implemented-in-term-of
- 需要使用
Base Class中的protected成员,或重载virtual function - 不希望一个
Base Class被client使用 - 实际上是Has-A关系
- 如果两个类的继承是私有的,则不能在派生类外将派生类转换成基类对象。
总结

1 | class Shape { |