课件
10.1 引言
引言
notes
引子 | 为什么需要继承和派生
类真不错!类描述了群体的共性,通过创建类的不同对象,我们实现了代码重用。但是这种重用是不充分的……
假设我们有汽车类
#include <iostream>
using namespace std;
class Car {
public:
int seats;
void accelerate() { cout << "加速到50迈" << endl; } // 油门
void brake() {
// 刹车
}
};
int main() {
Car c;
return 0;
}
当 Car 类的功能需要进行扩展为变形金刚类 Transformer,增加“变身”的功能,怎么做?
解决方案一:创建一个新类 Transformers,在其中粘贴 Car 类的代码,再添加新的方法 voidbianshen()
#include <iostream>
using namespace std;
class Car {
public:
int seats;
void accelerate() { cout << "加速到50迈" << endl; } // 油门
void brake() {
// 刹车
}
void bianshen() {
// 变身
}
};
int main() {
Car c;
return 0;
}
Car 类增加一个功能:停车 voidstop(),那么我们要同时更改 Car 类和 Transformer 类的代码。这种做法太不优雅啦,会让代码难以维护。
解决方案二:继承与派生
Transofomer 类“使用”了 Car 类的特性,那么不需要复制粘贴,Transformer 类就自动具有 Car 类所有的特性。更好的一点是,任何时候 Car 类进行修改,Transformer类都能应用这种修改。
继承与派生就是本章节要介绍的重点内容。
10.2 派生类的引入与特性
- 一旦指定了某种事物父代的本质特征,那么它的子代将会自动具有那些性质。这就是一种朴素的 可重用的概念
- 继承就是在一个已经存在的类的基础上建立另一个新的类
- 已存在的类称为“基类”或“父类”,新建立的类称为“派生类”或“子类”
派生:
- 子代可以拥有父代没有的特性,这是 可扩充的概念
- 派生类的功能主要通过以下方式来体现:
- 吸收基类成员
- 添加新成员
- 改造基类成员
从编码的角度来看,派生类从基类中以较低的代价换来了较大的灵活性:
- 派生类可以对继承的属性进行扩展、限制或改变。
- 一旦产生了可靠的基类,只需要调试派生类中所作的修改即可。
10.3 单继承
单继承的继承方式
同名成员的访问方式
赋值兼容原则
单继承方式下的构造与析构函数的调用顺序
notes
单继承、多继承
- 单继承:派生类只有一个直接基类
- 多继承:派生类有多个直接基类
单继承派生类的定义语法、访问权限
class 派生类名:<继承方式>基类名{
// …… 派生类修改基类的成员
// …… 派生类新添加的成员
};
// 继承方式:public、private、protected
虽然继承了基类的所有成员,但是派生类并非都能访问基类的所有成员,继承方式 会影响派生类对基类中各种成员的使用。
public、private、protected:
- 回顾类成员的访问方式也有 public、private、protected 分别是什么意思?
- 和我们这里继承方式的 public、private、protected 有区别吗?
类成员的 public、private、protected
public
类内外都可以访问,是类的对外接口private
类内可以访问,内外不能访问protected
同 private,类内可以访问,内外不能访问
继承方式的 public、private、protected
public
公有派生类private
私有派生类protected
保护派生类
基类 | 公有成员 | 私有成员 | 保护成员 |
---|---|---|---|
公有派生类 | 公有成员 | 不可访问成员 | 保护成员 |
私有派生类 | 私有成员 | 不可访问成员 | 私有成员 |
保护派生类 | 保护成员 | 不可访问成员 | 保护成员 |
虽然派生类继承了基类所有的成员,但是对于派生类来说,基类的私有成员是不可见的,因此不能被派生类所访问。
不可访问成员:
- 在类外不能被直接访问
- 在派生类的类内不能被直接访问
class 派生类名: public 基类名 {
// …… 派生类新添加的成员
}
#include <iostream>
using namespace std;
class Base {
int v1; // 没有使用访问修饰符,则默认使用 private 访问修饰符
public:
int v2;
Base(int a = 0, int b = 0) {
v1 = a;
v2 = b;
}
};
class Derived : public Base {
int v3;
public:
int v4;
Derived(int a = 0, int b = 0) {
v3 = a;
v4 = b;
}
void func() {
// cout << v1 << endl; // 编译器报错“ v1 是 Base 的 private 成员,不能访问”,private 权限 + 任何继承 = 不能访问
cout << v2 << endl; // 成功访问到基类成员v2 ,public 权限 + public 继承 = public 权限,此时 v2 的访问权限是 public
cout << v3 << endl; // 成功访问到自身成员 v3,private 权限,允许自身成员函数访问,不允许自身对象访问。
cout << v4 << endl; // 成功访问到自身成员 v4,public 权限,允许自身成员函数,自身对象访问。
}
};
int main() {
Derived obj = Derived(5, 6);
// obj.v1 = 8; // 编译器报错:“Derived 没有成员 v1”
// obj.v3 = 4; // 编译器报错:“v3 是 Derived 的私有成员”
obj.v2 = 8; // 成功,通过对象访问公有变量 public 权限 + public 继承 = public 权限
obj.v4 = 7; // 成功,通过对象访问公有变量。
obj.func();
return 0;
}
/* 运行结果:
8
5
7
*/
class 派生类名: private 基类名 {
// …… 派生类新添加的成员
}
#include <iostream>
using namespace std;
class Base {
int v1;
public:
int v2;
Base(int a = 0, int b = 0) {
v1 = a;
v2 = b;
}
};
class Derived : private Base {
int v3;
public:
int v4;
Derived(int a = 0, int b = 0) {
v3 = a;
v4 = b;
}
void func() {
// cout << v1 << endl;
cout << v2 << endl;
cout << v3 << endl;
cout << v4 << endl;
}
};
int main() {
Derived obj = Derived(5, 6);
// obj.v1 = 8;
// obj.v3 = 4;
// obj.v2 = 8; // v2 是派生类的私有成员,不能被类外访问
obj.v4 = 7;
obj.func();
return 0;
}
/* 运行结果:
0
5
7
*/
class 派生类名: protected 基类名 {
// …… 派生类新添加的成员
}
#include <iostream>
using namespace std;
class Base {
protected:
int v1;
public:
int v2;
Base(int a = 0, int b = 0) {
v1 = a;
v2 = b;
}
};
class Derived : protected Base {
int v3;
public:
int v4;
Derived(int a = 0, int b = 0) {
v3 = a;
v4 = b;
}
void func() {
cout << v1 << endl;
cout << v2 << endl;
cout << v3 << endl;
cout << v4 << endl;
}
};
int main() {
Derived obj = Derived(5, 6);
// obj.v1 = 8; // error => 'v3' is a private member of 'Derived'
// obj.v3 = 4; // error => 'v3' is a private member of 'Derived'
// obj.v2 = 8; // error => 'v2' is a protected member of 'Base'
obj.v4 = 7;
obj.func();
return 0;
}
/* 运行结果(注意:和课件描述不一致):
0
0
5
7
*/
派生类与基类同名成员的访问方式
- C++ 允许派生类可以重新定义基类的成员,此时称派生类的成员 覆盖了 基类的同名成员。
- 如果在派生类中,想使用基类的同名成员,则可以显式地使用 类名 + 限定符 的方式:
基类名::成员
#include <iostream>
using namespace std;
class Base {
protected:
int v1;
public:
int v2;
Base(int a = 0, int b = 0) {
v1 = a;
v2 = b;
}
};
class Derived : public Base {
int v2;
public:
int v3;
Derived(int a = 0, int b = 0) {
v2 = a;
v3 = b;
}
void func() {
int sum1 = v1 + v2 + v3;
int sum2 = v1 + Base::v2 + v3;
cout << "v1 = " << v1 << endl;
cout << "v2 = " << v2 << endl;
cout << "v3 = " << v3 << endl;
cout << "Base::v2 = " << Base::v2 << endl;
cout << "sum1 = " << sum1 << endl;
cout << "sum2 = " << sum2 << endl;
}
};
int main() {
Derived obj(5, 6);
// obj.v2 = 8; // 错误 这么写使用的是 Derived 中的 v2,它是私有成员
obj.Base::v2 = 9; // 正确 使用的是 Base 中的 v2
obj.func();
return 0;
}
/* 运行结果:
v1 = 0
v2 = 5
v3 = 6
Base::v2 = 9
sum1 = 11
sum2 = 15
*/
赋值兼容规则
必须满足赋值兼容规则才能够正常完成赋值。
#include <iostream>
using namespace std;
class animal {
public:
void eat() { cout << "animal is eating" << endl; }
};
class dog : public animal {
public:
void eat() { cout << "wang wang" << endl; }
};
int main() {
animal a;
dog d;
d.eat(); // => wang wang
a = d; // 成功赋值,因为"所有的动物都是狗"
a.eat(); // => animal is eating
animal& ref_a = d;
ref_a.eat(); // => animal is eating
animal* pointer_a = &d;
pointer_a->eat(); // => animal is eating
return 0;
}
#include <iostream>
using namespace std;
class animal {
public:
void eat() { cout << "animal is eating" << endl; }
};
class dog : public animal {
public:
void eat() { cout << "wang wang" << endl; }
};
int main() {
animal a;
dog d;
d.eat(); // => wang wang
// d = a; // 编译错误,不能将 animal 对象 a 转换为 dog 对象 d,因为“不是所有的动物都是狗”
return 0;
}
#include <iostream>
using namespace std;
class animal {
public:
void eat() { cout << "animal is eating" << endl; }
};
class dog : public animal {
public:
void eat() { cout << "wang wang" << endl; }
};
class lion : public animal {
public:
void eat() { cout << "lion lion" << endl; }
};
int main() {
animal a;
dog d;
lion l;
a = l; // 正确,因为狮子是动物
a.eat(); // => animal is eating
// l = a; // 错误,动物不一定是狮子
l.eat(); // => lion lion
// l = (lion)a; // 错误,动物不一定是狮子
// d = (dog)a; // 错误,动物不一定是狗
l.eat(); // => lion lion
d.eat(); // => wang wang
return 0;
}
在 公有派生 方式下,派生类对象可以作为基类对象来使用,具体方式如下:
- 派生类的对象可以直接赋值给基类的对象
- 基类对象的引用可以引用一个派生类对象
- 基类对象的指针可以指向一个派生类对象
这个规则可以简单的总结为:所有的子类对象都是基类的对象。
例:所有的狗都是动物,但不是所有的动物都是狗。
单继承的构造与析构
问:派生类继承了基类成员,对象中既包含派生类的成员又包含基类的成员。当我们生成派生类对象的时候,是怎么进行初始化的呢?
答:通过构造函数!
就像我们自己要建造变形金刚汽车,我们首先要抽象汽车的共性,包括发动机,轮胎,车门等信息,然后在派生类当中加上我们自己的特色,比如我们的汽车具有变形功能等。
所以派生类在构造对象的时候,先要生成自己基类部分,然后生成自己的成员对象,最后才是自己独有的部分。
派生类构造函数(参数表):基类构造函数(参数表),对象成员1(参数表), …… 对象成员n(参数表), {
// …… 初始化自定义数据成员
}
- 如果没有对象成员,那么在初始化列表中可以省略
对象成员(参数表)
这一项。 - 如果基类使用的是缺省的构造函数或不带参数的构造函数,那么在初始化列表中可以省略
基类构造函数(参数表)
这一项。
#include <iostream>
using namespace std;
class Point {
protected:
float x, y;
public:
Point(float xx = 0, float yy = 0) : x(xx), y(yy) {}
float GetX() { return x; }
float GetY() { return y; }
void moveto(float xx, float yy) {
x = xx;
y = yy;
}
};
class Circle {
Point center;
float radius;
public:
Circle(float x, float y, float r) : center(x, y) { radius = r; }
Point GetCenter() const { return center; }
int GetRadius() { return radius; }
};
class ColorCircle : public Circle {
int color;
public:
ColorCircle(float x, float y, float r, int color) : Circle(x, y, r) {
this->color = color;
}
int GetColor() { return color; }
};
int main() {
ColorCircle cc(1, 2, 3, 4);
Point center = cc.GetCenter();
int r = cc.GetRadius();
cout << "圆心: (" << center.GetX() << ", " << center.GetY() << ")" << endl;
cout << "半径: " << r << endl;
cout << "颜色: " << cc.GetColor() << endl;
return 0;
}
/* 运行结果:
圆心: (1, 2)
半径: 3
颜色: 4 */
Circle(x,y,r)
初始化基类数据this->color=color;
初始化自定义的基本数据类型数据
#include <iostream>
using namespace std;
class Base {
public:
Base() { cout << "Base obj created" << endl; }
~Base() { cout << "Base obj deleted" << endl; }
};
class Derived : public Base {
public:
Derived() { cout << "Derived obj created" << endl; }
~Derived() { cout << "Derived obj deleted" << endl; }
};
int main() {
Derived d;
return 0;
}
/* 运行结果:
Base obj created
Derived obj created
Derived obj deleted
Base obj deleted
*/
构造函数和析构函数的调用顺序:
- 构造函数的调用顺序如下:
- 先调用基类构造函数;
- 再调用对象成员所属类的构造函数;
- 最后调用派生类构造函数;
- 析构函数的调用顺序如下:
- 再调用对象成员所属类的析构函数;
- 先调用派生类的析构函数;
- 最后调用基类的析构函数;