继承与多态

面向对象的核心思想是数据抽象,继承和动态绑定。

定义基类与派生类

通过继承(inheritance)联系在一起的类构成了一种层次关系。通常在层次的根部有一个基类(base class),其他类则直接或间接地从基类继承而来, 这些继承而来的类称为派生类

派生类必须通过使用派生类列表明确指出它是从哪个基类继承而来的。

类派生列表的形式是:首先一个冒号,后面紧跟以基类列表,其中每个基类前面可以有访问说明符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>

// 基类
class Entity {
public:
float X, Y;

void Move(float xa, float ya) {
X += xa;
Y += ya;
}
};

// 派生类
// Player 继承了 Entity
class Player : public Entity {
public:
const char* Name;

void printName() {
std::cout << Name << std::endl;
}
};

定义基类

我们定义一个图书的Quote类:

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
// book.cpp

#include <iostream>

class Quote {
public:
// C++11新特性,默认构造函数
Quote() = default;

Quote(const std::string &book, double sales_price) :
bookNo(book), price(sales_price) {}

std::string isbn() const {
return bookNo;
}

// 返回给定数量的书籍的销售总额
// 派生类负责改写并使用不同的折扣计算算法

virtual double net_price(std::size_t n) const {
return n * price;
}
// 析构函数
virtual ~Quote() = default;
private:
std::string bookNo; // 书籍的ISBN编号
protected:
double price = 0.0; // 代表普通状态下不打折的价格
};

虚函数我们后面再讲,现在只需要记住,作为继承关系中根节点的类通常都定义一个虚析构函数。

Note > 基类通常都定义一个虚析构函数,即使该函数不执行任何实际操作。

成员函数与继承

派生类需要对一些操作提供自己的新定义以覆盖(override)从基类继承而来的定义。

基类必须将它的两种成员函数区分开:

  • 基类希望其派生类进行覆盖的函数

  • 基类希望派生类直接继承而不要改变的函数

对于前者,基类通常将其定义为虚函数(virtual)。当我们使用指针或引用调用虚函数时,该调用将被动态绑定。

成员函数如果没被声明为虚函数,则其解析过程发生在编译时而非运行时。

访问控制与继承

派生类可以继承定义在基类中的成员,但是派生类的成员函数不一定有权访问从基类继承而来的成员。 派生类能访问公有成员,而不能访问私有成员。

如果我们希望它的派生类有权访问该成员,同时禁止其他用户访问,我们可以使用protected访问运算符说明这样的成员。

定义派生类

派生类必须将其继承而来的成员函数需要覆盖的那些重新声明,因此我们的Bulk_quote类必须包含一个net_price成员:

1
2
3
4
5
6
7
8
9
10
11
12
// Bulk_quote 继承自 Quote
class Bulk_quote : public Quote{
public:
Bulk_quote() = default;
Bulk_quote(const std::string&, double, std::size_t, double);

// 覆盖基类的函数版本以实现基于大量购买的折扣政策
double net_price(std::size_t) const override;
private:
std::size_t min_qty = 0; // 适用折扣政策的最低购买量
double discount = 0.0; // 以小数表示的折扣额
};

我们的Bulk_quote类从它的基类Quote那里继承了isbn函数和bookNo,price等数据成员,此外,它还定义了net_price的新版本, 同时新增了两个新增加的数据成员min_qtydiscount。这两个成员分别用于说明享受折扣所需购买的最低数量以及一旦该数量达到 之后具体的折扣信息。

派生类中的虚函数

派生类经常覆盖它继承的虚函数。如果派生类没有覆盖,则会直接继承其在基类中的版本。

派生类可以在它覆盖的函数前使用virtual关键字,C++11新标准允许派生类显式地注明它使用某个成员函数覆盖了它继承的虚函数,具体做法是 在形参列表后面,或者在const成员函数的const关键字后面,或者在引用成员函数的引用限定符后面添加一个关键字override

派生类对象及派生类向基类的类型转换

一个派生类对象包含多个组成部分:

  • 派生类自己定义的(非静态)成员的子对象

  • 该派生类继承的基类对应的子对象

因为在派生类对象中含有与其基类对应的组成部分,所以我们能把派生类的对象当成基类对象来使用,而且我们也能将基类的指针或引用绑定 到派生类对象中的基类部分上:

1
2
3
4
5
Quote item;             // 基类对象
Bulk_quote bulk; // 派生类对象
Quote *p = &item; // p 指向Quote 对象
p = &bulk; // p 指向bulk的Quote部分
Quote &r = bulk; // r 绑定到bulk的Quote部分

这种转换称为派生类到基类的类型转换,编译器会隐式地执行派生类到基类的转换。

派生类构造函数

尽管在派生类对象中含有基类继承而来的成员,但是派生类并不能直接初始化这些成员。必须通过基类的构造函数来初始化它的基类部分。

例如接受4个参数的Bulk_quote构造函数:

1
2
Bulk_quote(const std::string& book, double p, std::size_t qty, double disc) :
Quote(book, p), min_qty(qty), discount(disc) {}

该函数将它的两个参数(分别表示ISBN 和价格)传递给Quote的构造函数,由Quote的构造函数负责初始化Bulk_quote的基类部分(即bookNo,price)。 接下来由派生类直接定义的min_qty成员和discount成员,最后运行Bulk_quote构造函数的(空的)函数体。

派生类使用基类的成员

派生类可以访问基类的公有成员和受保护成员:

1
2
3
4
5
6
7
// 如果达到了购买书籍的某个最低限量值,就可以享受折扣价格了
double Bulk_quote::net_price(size_t cnt) const {
if (cnt >= min_qty)
return cnt * (1 - discount) * price;
else
return cnt * price;
}

继承与静态函数

如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。不论从基类中派生出来多少个派生类,对于每个静态成员来说都只存在唯一的实例。

防止继承的发生

我们不希望其他类继承它,可以通过C++11新标准提供的一种防止继承发生的方法,即在类名后跟一个关键字final:

1
class NoDerived final {/* */};              / NoDerived不能作为基类

不存在从基类向派生类的隐式类型转换:

1
2
3
Quote base;
Bulk_quote* bulkP = &base; // error: 不能将基类转换成派生类
Bulk_quote& bulkRef = base; // error

如果上述合法,则我们有可能会使用bulkPbulkRed访问base中本不存在的成员。

当我们用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝,移动或赋值,它的派生类部分将会被忽略掉。