第15章 面向对象程序设计
15.1 OOP:概述
- 面向对象程序设计的核心思想是数据抽象、继承和动态绑定。数据抽象可将类的接口与实现分离,继承可定义相似的类型并对相似关系建模,动态绑定可在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象。
- 继承时分基类和派生类,基类定义所有类共同拥有的成员,派生类定义各自特有的成员。派生类必须在其内部对所有重新定义的虚函数进行声明。
- 动态绑定又称运行时绑定,即在运行时选择函数版本。当使用基类的引用或指针调用虚函数时发生动态绑定。
15.2 定义基类和派生类
1. 定义基类
- 基类通常应该定义一个虚析构函数,即使该函数不执行任何操作。
- 基类将成员函数分为两种:一种是基类希望其派生类进行覆盖的函数;另一种是基类希望派生类直接继承而不要改变的函数。
- 除构造函数之外的非静态函数都可以是虚函数。
-
virtual
只能出现在类内声明语句之前,而不能用于类外函数定义。
2. 定义派生类
- 派生类通常覆盖其继承的虚函数,若未覆盖,则派生类会直接继承其在基类中的版本。
- 派生类包含一个派生类自定义的子对象、一个或多个基类子对象。派生类到基类的类型转换即基类引用或指针可以绑定派生类对象的基类部分。
- 每个类负责定义各自的接口。派生类不能直接初始化基类的成员,而应该通过基类的构造函数来进行初始化。
- 类必须在定义后才能作为基类,仅声明而已不行。类不能派生它本身。派生类声明时应包含类名,而不能包含派生列表。
- 基类可分为直接基类和间接基类,直接基类出现在派生列表中,间接基类通过其直接基类继承而来。
3. 类型转换与继承
- 存在派生类向基类的类型转换,但不存在基类向派生类的类型转换(基类指针或引用可以绑定到派生类对象,但派生类指针或引用不能绑定到基类对象)。
- 静态类型是变量声明时的类型或表达式生成的类型,编译时已知。动态类型是变量或表达式表示的内存中的对象的类型,运行时才可知。
- 若表达式不是指针或引用,则其静态类型与动态类型保持一致。基类指针或引用的静态类型可能与动态类型不一致。
- 对象之间不存在类型转换(基类对象由派生类对象转换而成则只处理派生类的基类部分,派生类不能由基类转换而成)。
Quote base; // 基类
Bulk_quote bulk; // 派生类
Quote* baseP = &bulk;
Quote& baseR = bulk;
Bulk_quote *bulkP = &base; // 错误
Bulk_quote &bulkR = base; // 错误
base = bulk; // 只处理bulk的基类部分
bulk = base; // 错误
15.3 虚函数
- 普通函数不使用时可以只声明不定义,但虚函数无论是否使用都必须提供定义。
- 动态绑定只有当我们通过指针或引用调用虚函数时才发生。
- C++支持多态性的根本在于引用或指针的静态类型与动态类型不同。
- 基类中的虚函数在派生类中隐含是一个虚函数。
- 若派生类覆盖虚函数,则该虚函数在基类和派生类中的形参和返回类型必须完全一致。例外:若虚函数返回类型是类本身的指针或引用,且派生类向基类的类型转换是可访问的,则基类返回基类的引用或指针,派生类返回派生类的引用或指针。
-
override
显式注明要覆盖虚函数,当某个函数被标记为override
时要求该函数是虚函数,且已覆盖基类中的虚函数。final
可以防止继承发生。override
和final
可出现在形参列表之后(包括任何const
和引用修饰符)以及尾置返回类型。
- 若调用虚函数时使用默认实参,则不论是派生类或基类,都使用基类定义的默认实参,故基类和派生类中定义的默认实参最好一致。
- 通过作用域运算符
::
可指定执行虚函数的特定版本,而避免虚函数调用时进行动态绑定。
15.4 抽象基类
- 在类内的虚函数声明语句处
=0
可定义纯虚函数,表示该函数无实际意义。纯虚函数一般不用定义,若要定义则只能在类外定义函数体。
- 含有纯虚函数或者未经覆盖直接继承纯虚函数的类是抽象基类。不能创建抽象基类的对象,但可以在派生类覆盖纯虚函数时定义派生类的对象。
- 重构负责重新设计类的体系以便将操作或数据从一个类移动到另一个类。
class Quote; // 间接基类
class Disc_quote; // 直接基类,抽象基类
class Bulk_quote; // 派生类
Disc_quote disc; // 错误,不能创建抽象基类的对象
Bulk_quote bulk;
15.5 访问控制与继承
- 每个类分别控制其成员的初始化过程和对派生类的访问权限。
public
成员在整个程序内可见。private
成员只对该类成员函数和友元函数可见。protected
成员只对该类成员函数和友元函数、派生类成员函数和友元函数可见。
- 若基类的
public
成员是可访问的,则派生类向基类的类型转换也是可访问的;反之不行。
- 友元关系不能传递和继承。
- 派生类只能为可访问成员提供
using 类名::成员
以修改直接或间接基类成员的访问权限,该权限由using 类名::成员
前的访问说明符决定。
-
class
和struct
唯一的区别:class
的默认成员访问说明符和默认派生访问说明符是private
,而struct
的是public
。
15.6 继承中的类作用域
- 派生类的作用域位于基类作用域之内。查找派生类成员名字时,若在派生类中未找到则会前往基类查找。
- 若派生类成员与基类成员同名,则派生类成员会隐藏基类成员。虽然可通过作用域运算符
::
来使用隐藏成员,但最好不要在派生类中重用除虚函数之外的定义在基类中的名字。
- 调用
p->mem()
执行的步骤:确定p
的静态类型;在静态类型对应的类中查找mem
;找到后,对mem
进行类型检查确保调用合法;合法后,若mem
是虚函数且通过引用或指针进行调用则会在运行时依据动态类型确定虚函数版本,若mem
不是虚函数或通过对象进行调用则会产生一个常规函数调用。
15.7 构造函数与拷贝控制
- 若基类的析构函数不是虚函数,则
delete
一个指向派生类对象的基类指针将产生未定义的行为。
- 若一个类已自定义拷贝构造函数、拷贝赋值运算符或析构函数,则编译器不会为该类合成移动构造函数或移动赋值运算符。
- 基类通常不会合成移动操作。当基类通过
=default
显式合成移动操作时,移动基类对象实际使用合成拷贝操作,故必须同时显式定义拷贝操作。
- 派生类的构造函数和赋值运算符在拷贝和移动自有成员的同时,也要拷贝和移动基类部分的成员。但析构函数只负责销毁派生类自己分配的资源。
- 在默认情况下,基类默认构造函数初始化派生类对象的基类部分。若想拷贝或移动基类部分,则必须在派生类的构造函数初始值列表中显式使用基类的拷贝或移动构造函数。与拷贝和移动构造函数一样,派生类的赋值运算符也必须显式为其基类部分赋值。
- 在析构函数体执行完成后,对象的成员会被隐式销毁。类似的对象的基类部分也是隐式销毁的。对象销毁的顺序与其创建的顺序相反。
- 若构造函数或析构函数调用某个虚函数,则我们应执行与构造函数或析构函数所属类型相对应的虚函数版本。
- 类不能继承默认、拷贝和移动构造函数,除非使用
using
。若派生类未定义构造函数,则编译器将为派生类合成构造函数。
- 和普通成员的
using
声明不同,构造函数的using
声明不会改变构造函数的访问级别。
- 若基类的构造函数是
explicit
或constexpr
,则继承的构造函数也是explicit
或constexpr
。若基类构造函数含有默认实参,派生类不会继承默认实参,而会继承多个构造函数,其中每个构造函数分别省略掉一个含有默认实参的形参。
Derived(const Derived &d):Base(d) {....} // 拷贝基类成员
Derived(const Derived &d): {....} // 基类部分被默认初始化,而非拷贝
Derived(Derived &&d):Base(std::move(d)) {....} // 拷贝基类成员
Derived& Derived::operator=(const Derived &rhs)
{
Base::operator=(rhs);// 为基类部分赋值
....
return *this;
}
15.8 容器与继承
- 容器中只能存放同一类型的元素。基类与派生类虽然存在继承关系,但不是同一类型。若需要在容器中存放具有继承关系的对象,则应存放指向基类对象的指针。
-
upper_bound
返回一个迭代器,该迭代器指向第一个与当前元素的关键字不相同的元素。
15.9 文本查询程序再探