第 15 章 面向对象程序设计
15.1 OOP:概述
面向对象程序设计(Object-Oriented Programming)的核心思想是:
- 数据抽象(第 7 章)
- 继承
- 动态绑定
(1)继承
基类:Quote,表示按原价销售的书籍。
派生类:Bulk_quote,表示可以打折销售的书籍。
这些类将包含下面的两个成员函数∶
- isbn(),返回书籍的 ISBN 编号
- net_price(size_t),返回书籍的实际销售价格
在 C++ 中,基类将类型相关的函数与派生类不做改变直接继承的函数区分对待。对于某些函数,基类希望它的派生类各自定义适合自身的版本,此时基类就将这些函数声明成虚函数。因此将 Quote 类编写成:
class Quote
{
public:
std::string isbn() const;
virtual double net_price(std::size_t n) const; // virtual 表示此为虚函数
};
派生类必须通过使用类派生列表明确指出它是从哪个(哪些)基类继承而来的。
// Bulk_quote 继承了Quote
class Bulk_quote : public Quote
{
public:
double net_price(std::size_t) const override;
};
派生类必须在其内部对所有重新定义的虚函数进行声明。派生类可以在这样的函数之前加上 virtual 关键字,但是并不是非得这么做。
C++11 新标准允许派生类显式地注明它将使用哪个成员函数改写基类的虚函数,具体措施是在该函数的形参列表之后增加一个 override 关键字。
(2)动态绑定
通过使用动态绑定,我们能用同一段代码分别处理 Quote 和 Bulk_quote 的对象。
下面的函数负责打印总的费用:
// 计算并打印销售给定数量的某种书籍所得的费用
double print_total(ostream &os, const Quote &item, size_t n)
{
// 根据传入 item 形参的对象类型调用 Quote::net_price
// 或者 Bulk_quote::net_price
double ret = item.net_price(n);
//调用 Quote::isbn
os << "ISBN: " << item.isbn() << " # sold:" << n << " total due: " << ret << endl;
return ret;
}
-
因为函数 print_total 的 item 形参是基类 Quote 的一个引用,所以我们既能使用基类 Quote 的对象调用该函数,也能使用派生类 Bulk_quote的对象调用它
-
因为 print_total 是使用引用类型调用 net_price 函数的,所以实际传入 print_total 的对象类型将决定到底执行 net_price 的哪个版本:
// basic 的类型是 Quote; bulk 的类型是 Bulk_quote
print_total(cout, basic, 20); // 调用 Quote 的 net_price
print_total(cout, bulk, 20); // 调用 Bulk_quote 的 net_price
因为在上述过程中函数的运行版本由实参决定,即在运行时选择函数的版本,所以动态绑定有时又被称为运行时绑定。
15.2 定义基类和派生类
15.2.1 定义基类
class Quote
{
public:
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; // 代表普通状态下不打折的价格
};
基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此。
(1)成员函数与继承
在 C++ 中,基类必须将它的两种成员函数区分开来:
- 一种是基类希望其派生类进行覆盖的函数
- 一种是基类希望派生类直接继承而不要改变的函数。
对于前者,基类通常将其定义为虚函数。当我们使用指针或引用调用虚函数时,该调用将被动态绑定。根据引用或指针所绑定的对象类型不同,该调用可能执行基类的版本,也可能执行某个派生类的版本。
任何构造函数之外的非静态函数都可以是虚函数。关键字 virtual 只能出现在类内部的声明语句之前而不能用于类外部的函数定义。如果基类把一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数。成员函数如果没有被声明为虚函数,其解析过程发生在编译时而非运行时。
(2)访问控制与继承
protected:派生类有权访问基类的 protected 成员,同时禁止其他用户访问。
15.2.2 定义派生类
// 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; // 以小数表示的折扣额
};
大多数类都只继承自一个类,派生类经常(但不总是)覆盖它继承的虚函数。
(1)派生类对象及派生类向基类的类型转换
C++ 标准并没有明确规定派生类的对象在内存中如何分布,但是我们可以认为 Bulk_quote 的对象包含如图所示的两部分。
因为在派生类对象中含有与其基类对应的组成部分,所以我们能把派生类的对象当成基类对象来使用,而且我们也能将基类的指针或引用绑定到派生类对象中的基类部分上。
Quote item; // 基类对象
Bulk_quote bulk; // 派生类对象
Quote *p = &item; // p 指向 Quote 对象
p = &bulk; // p 指向 bulk 的 Quote 部分
Quote &r = bulk; // r 绑定到 bulk 的 Quote 部分
这种转换通常称为派生类到基类的类型转换,编译器会隐式地执行这种转换。
(2)派生类构造函数
我们要让每个类控制它自己的成员初始化过程。因此派生类必须使用基类的构造函数来初始化它的基类部分。
//与之前一致
Bulk_quote(const std::string& book, double p, std::size_t qty, double disc) : Quote(book, p), min_qty(qty), discount(disc) { }
否则派生类对象的基类部分会像数据成员一样执行默认初始化。
(3)继承与静态成员
如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。不论从基类中派生出来多少个派生类,对于每个静态成员来说都只存在唯一的实例。
静态成员遵循通用的访问控制规则,如果基类中的成员是 private 的,则派生类无权访问它。
(4)派生类的声明
class Bulk_quote : public Quote; // 错误; 派生列表不能出现在这里
class Bulk_quote; // 正确; 声明派生类的正确方式
如果我们想将某个类用作基类,则该类必须已经定义而非仅仅声明。
(5)防止继承的发生
C++11 新标准提供了一种防止继承发生的方法,即在类名后跟一个关键字 final:
class NoDerived final { /* */ }; // NoDerived 不能作为基类
class Base { /* */ };
// Last 是 final 的;我们不能继承 Last
class Last final : Base { /* */ }; // Last 不能作为基类
class Bad : NoDerived { /* */ }; // 错误∶NoDerived 是 final的
class Bad2 : Last { /**/ }; // 错误∶ Last 是 final 的
15.2.3 类型转换与继承
我们可以将基类的指针或引用绑定到派生类对象上。
当使用基类的引用(或指针)时,实际上我们并不清楚该引用(或指针)所绑定对象的真实类型。该对象可能是基类的对象,也可能是派生类的对象。
(1)静态类型与动态类型
-
表达式的静态类型在编译时总是已知的,它是变量声明时的类型或表达式生成的类型。
-
动态类型则是变量或表达式表示的内存中的对象的类型。动态类型直到运行时才可知。
(2)不存在从基类向派生类的隐式类型转换
之所以存在派生类向基类的类型转换是因为每个派生类对象都包含一个基类部分,而基类的引用或指针可以绑定到该基类部分上。一个基类的对象既可以以独立的形式存在,也可以作为派生类对象的一部分存在。如果基类对象不是派生类对象的一部分,则它只含有基类定义的成员,而不含有派生类定义的成员。
因为一个基类的对象可能是派生类对象的一部分,也可能不是,所以不存在从基类向派生类的自动类型转换。
如果在基类中含有一个或多个虚函数,我们可以使用 dynamic_cast 请求一个类型转换,该转换的安全检查将在运行时执行。如果我们已知某个基类向派生类的转换是安全的,则我们可以使用 static_cast 来强制覆盖掉编译器的检查工作。
(3)对象之间不存在类型转换
将派生类赋给基类时,实际上并不是发生了类型转换,而是调用了相应的函数:
Bulk_quote bulk; // 派生类对象
Quote item(bulk); // 使用 Quote::Quote(const Quote&) 构造函数
item = bulk; // 调用 Quote::operator=(const Quote&)
当构造 item 时,运行 Quote 的拷贝构造函数。该函数只能处理 bookNo 和 price 两个成员,它负责拷贝 bulk 中 Quote 部分的成员,同时忽略掉 bulk 中 Bulk_quote 部分的成员。类似的,对于将 bulk 赋值给 item 的操作来说,只有 bulk 中 Quote 部分的成员被赋值给 item。
因为在上述过程中会忽略 Bulk_quote 部分,所以我们可以说 bulk 的 Bulk_quote部分被切掉了。