第十五章 面向对象程序设计
2020年11月10日大约 9 分钟
第十五章 面向对象程序设计
OOP:概述
- 面向对象程序设计(object-oriented programming)的核心思想是数据抽象、继承和动态绑定。
- 继承(inheritance):
- 通过继承联系在一起的类构成一种层次关系。
- 通常在层次关系的根部有一个基类(base class)。
- 其他类直接或者简介从基类继承而来,这些继承得到的类成为派生类(derived class)。
- 基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。
- 对于某些函数,基类希望它的派生类个自定义适合自己的版本,此时基类就将这些函数声明成虚函数(virtual function)。
- 派生类必须通过使用类派生列表(class derivation list)明确指出它是从哪个基类继承而来。形式:一个冒号,后面紧跟以逗号分隔的基类列表,每个基类前都可以有访问说明符。
class Bulk_quote : public Quote{};
- 派生类必须在其内部对所有重新定义的虚函数进行声明。可以在函数之前加上
virtual
关键字,也可以不加。C++11新标准允许派生类显式地注明它将使用哪个成员函数改写基类的虚函数,即在函数的形参列表之后加一个override
关键字。
- 动态绑定(dynamic binding,又称运行时绑定):
- 使用同一段代码可以分别处理基类和派生类的对象。
- 函数的运行版本由实参决定,即在运行时选择函数的版本。
定义基类和派生类
定义基类
- 基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此。
- 基类通过在其成员函数的声明语句前加上关键字
virtual
使得该函数执行动态绑定。 - 如果成员函数没有被声明为虚函数,则解析过程发生在编译时而非运行时。
- 访问控制:
protected
: 基类和和其派生类还有友元可以访问。private
: 只有基类本身和友元可以访问。
定义派生类
- 派生类必须通过类派生列表(class derivation list)明确指出它是从哪个基类继承而来。形式:冒号,后面紧跟以逗号分隔的基类列表,每个基类前面可以有一下三种访问说明符的一个:
public
、protected
、private
。 - C++11新标准允许派生类显式地注明它将使用哪个成员函数改写基类的虚函数,即在函数的形参列表之后加一个
override
关键字。 - 派生类构造函数:派生类必须使用基类的构造函数去初始化它的基类部分。
- 静态成员:如果基类定义了一个基类成员,则在整个继承体系中只存在该成员的唯一定义。
- 派生类的声明:声明中不包含它的派生列表。
- C++11新标准提供了一种防止继承的方法,在类名后面跟一个关键字
final
。
类型转换与继承
- 理解基类和派生类之间的类型抓换是理解C++语言面向对象编程的关键所在。
- 可以将基类的指针或引用绑定到派生类对象上。
- 不存在从基类向派生类的隐式类型转换。
- 派生类向基类的自动类型转换只对指针或引用类型有效,对象之间不存在类型转换。
虚函数
- 使用虚函数可以执行动态绑定。
- OOP的核心思想是多态性(polymorphism)。
- 当且仅当对通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型才有可能与静态类型不同。
- 派生类必须在其内部对所有重新定义的虚函数进行声明。可以在函数之前加上
virtual
关键字,也可以不加。 - C++11新标准允许派生类显式地注明它将使用哪个成员函数改写基类的虚函数,即在函数的形参列表之后加一个
override
关键字。 - 如果我们想覆盖某个虚函数,但不小心把形参列表弄错了,这个时候就不会覆盖基类中的虚函数。加上
override
可以明确程序员的意图,让编译器帮忙确认参数列表是否出错。 - 如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致。
- 通常,只有成员函数(或友元)中的代码才需要使用作用域运算符(
::
)来回避虚函数的机制。
抽象基类
- 纯虚函数(pure virtual):清晰地告诉用户当前的函数是没有实际意义的。纯虚函数无需定义,只用在函数体的位置前书写
=0
就可以将一个虚函数说明为纯虚函数。 - 含有纯虚函数的类是抽象基类(abstract base class)。不能创建抽象基类的对象。
访问控制与继承
- 受保护的成员:
protected
说明符可以看做是public
和private
中的产物。- 类似于私有成员,受保护的成员对类的用户来说是不可访问的。
- 类似于公有成员,受保护的成员对于派生类的成员和友元来说是可访问的。
- 派生类的成员或友元只能通过派生类对象来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问特权。
- 派生访问说明符:
- 对于派生类的成员(及友元)能否访问其直接积累的成员没什么影响。
- 派生访问说明符的目的是:控制派生类用户对于基类成员的访问权限。比如
struct Priv_Drev: private Base{}
意味着在派生类Priv_Drev
中,从Base
继承而来的部分都是private
的。
- 友元关系不能继承。
- 改变个别成员的可访问性:使用
using
。 - 默认情况下,使用
class
关键字定义的派生类是私有继承的;使用struct
关键字定义的派生类是公有继承的。
继承中的类作用域
- 每个类定义自己的作用域,在这个作用域内我们定义类的成员。当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内。
- 派生类的成员将隐藏同名的基类成员。
- 除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字。
构造函数与拷贝控制
虚析构函数
- 基类通常应该定义一个虚析构函数,这样我们就能动态分配继承体系中的对象了。
- 如果基类的析构函数不是虚函数,则
delete
一个指向派生类对象的基类指针将产生未定义的行为。 - 虚析构函数将阻止合成移动操作。
合成拷贝控制与继承
- 基类或派生类的合成拷贝控制成员的行为和其他合成的构造函数、赋值运算符或析构函数类似:他们对类本身的成员依次进行初始化、赋值或销毁的操作。
派生类的拷贝控制成员
- 当派生类定义了拷贝或移动操作时,该操作负责拷贝或移动包括基类部分成员在内的整个对象。
- 派生类析构函数:派生类析构函数先执行,然后执行基类的析构函数。
继承的构造函数
- C++11新标准中,派生类可以重用其直接基类定义的构造函数。
- 如
using Disc_quote::Disc_quote;
,注明了要继承Disc_quote
的构造函数。
容器与继承
- 当我们使用容器存放继承体系中的对象时,通常必须采用间接存储的方式。
- 派生类对象直接赋值给积累对象,其中的派生类部分会被切掉。
- 在容器中放置(智能)指针而非对象。
- 对于C++面向对象的编程来说,一个悖论是我们无法直接使用对象进行面向对象编程。相反,我们必须使用指针和引用。因为指针会增加程序的复杂性,所以经常定义一些辅助的类来处理这些复杂的情况。
文本查询程序再探
- 使系统支持:单词查询、逻辑非查询、逻辑或查询、逻辑与查询。
面向对象的解决方案
- 将几种不同的查询建模成相互独立的类,这些类共享一个公共基类:
WordQuery
NotQuery
OrQuery
AndQuery
- 这些类包含两个操作:
eval
:接受一个TextQuery
对象并返回一个QueryResult
。rep
:返回基础查询的string
表示形式。
- 继承和组合:
- 当我们令一个类公有地继承另一个类时,派生类应当反映与基类的“是一种(Is A)”的关系。
- 类型之间另一种常见的关系是“有一个(Has A)”的关系。
- 对于面向对象编程的新手来说,想要理解一个程序,最困难的部分旺旺是理解程序的设计思路。一旦掌握了设计思路,接下来的实现也就水到渠成了。
Query程序设计:
操作 | 解释 |
---|---|
Query 程序接口类和操作 | |
TextQuery | 该类读入给定的文件并构建一个查找图。包含一个query 操作,它接受一个string 实参,返回一个QueryResult 对象;该QueryResult 对象表示string 出现的行。 |
QueryResult | 该类保存一个query 操作的结果。 |
Query | 是一个接口类,指向Query_base 派生类的对象。 |
Query q(s) | 将Query 对象q 绑定到一个存放着string s 的新WordQuery 对象上。 |
q1 & q2 | 返回一个Query 对象,该Query 绑定到一个存放q1 和q2 的新AndQuery 对象上。 |
`q1 | q2` |
~q | 返回一个Query 对象,该Query 绑定到一个存放q 的新NotQuery 对象上。 |
Query 程序实现类 | |
Query_base | 查询类的抽象基类 |
WordQuery | Query_base 的派生类,用于查找一个给定的单词 |
NotQuery | Query_base 的派生类,用于查找一个给定的单词 |
BinaryQuery | Query_base 的派生类,查询结果是Query 运算对象没有出现的行的集合 |
OrQuery | Query_base 的派生类,返回它的两个运算对象分别出现的行的并集 |
AndQuery | Query_base 的派生类,返回它的两个运算对象分别出现的行的交集 |