OOP 概述
面向对象程序设计的核心思想是数据抽象、继承和动态绑定。
- 数据抽象:将类的接口与实现分类。
- 继承:可以定义相似的类型并对其相似关系建模。
- 动态绑定:可以在一定程序上忽略相似类型的区别,而以统一的形式使用它们的对象。
继承
通过继承联系在一起的类构成一种层次关系,通常在层次关系的根部有一个 基类,其他类则直接或间接的从基类继承而来,这些继承得到的类称为 派生类。基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。
派生类必须通过使用 类派生列表 明确指出从哪个或者哪些基类继承而来,类派生列表的形式是:冒号,后面紧跟以逗号分隔的基类列表,其中每个基类前面都可以有访问说明符。
如果基类希望他的派生类各自定义适合自身版本的函数,此时基类就将这些函数声明成虚函数。C++ 11 新标准允许派生类显示地注明它将使用哪个成员函数改写基类的虚函数,具体措施是在函数的形参列表之后再加一个 override
关键字。
动态绑定
在 C++ 语言中,使用基类的引用或者指针调用一个虚函数时将发生动态绑定。
定义基类和派生类
定义基类
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; //表示普通状态下不打折的价格
};
注意:
基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际的操作也是如此。
成员函数与继承
派生类可以继承其基类的成员,但是针对特定的操作,派生类可以提供自己新的定义,以覆盖从基类继承而来的旧定义。
C++ 中必须将它的两种成员函数区分开来:
- 一种是基类希望其派生类进行覆盖的函数。
- 一种是基类希望派生类直接继承而不要改变的函数。
对于第一种函数,基类通常将其定义为虚函数,当使用指针或者引用调用虚函数时,该调用将动态绑定,根据引用或指针所绑定的对象类型不同,该调用可能执行基类的版本,也可能执行某个派生类的版本。
基类通过在成员函数前面加上 virtual
函数使得该函数执行动态绑定。
- 任何构造函数之外的非静态函数都可以是虚函数。
- 关键字
virtual
只能出现在类内部声明语句之前,不能用于类外部的函数定义。 - 如果基类把一个函数写成虚函数,则该函数在派生类中隐式地也是虚函数。
成员函数如果没有被声明为虚函数,则其解析过程发生在编译时期而非运行时。
访问控制与继承
派生类可以继承定义在基类中的成员,但是派生类的成员函数不一定有权访问从基类继承而来的成员。派生类可以访问公有成员,而不能访问私有成员,protected
成员是基类希望它的派生类能够访问该成员,但是同时禁止其他用户访问。
定义派生类
派生类需要使用类派生列表明确指出它是从哪个或哪些基类继承而来的,类派生列表的形式是:冒号后面紧跟逗号分隔的基类列表,每个基类前面都要指明访问说明符:public
,protected
,private
。
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;
};
继承自一个类的形式称为单继承。
派生类中的虚函数
派生类经常但不总是覆盖它继承的虚函数,如果派生类没有覆盖其基类中的某个虚函数,则该虚函数的行为类似于其他的普通成员,派生类会直接继承其在基类中的版本。
派生类对象及派生类向基类的类型转换
一个派生类对象包含多个组成部分:含有派生类自定义的对象,继承自基类的对象。
C++ 中没有明确规定派生类的对象在内存中如何分布,但是可以 认为 Bulk_quote
对象包含如下两个部分:
因为在派生类对象中含有与其基类对应的组成部分,所以能把派生类的对象当成基类对象使用,而且能将基类的指针或引用绑定到派生类对象中的基类部分:
Quote item; // 基类对象
Bulk_quote bulk; // 派生类对象
Quote *p = &item; // p指向基类 Quote 对象
p = &bulk; // p指向派生类 Bulk_quote 对象的基类部分
Quote &r = bulk; // r绑定到派生类 Bulk_quote 对象的基类部分
上面介绍的转换通常称为 派生类到基类的类型转换。这种类型转换是编译器隐式执行的。
派生类构造函数
尽管派生类包含了从基类继承而来的成员,但是派生类不能直接初始化这些成员,而是需要使用基类的构造函数来初始化它们。也就是说:每个类控制它自己的成员初始化过程。
派生类对象的基类部分与派生类对象自己的数据成员都是在构造函数的初始化阶段执行初始化操作的:
Bulk_quote(const std::string& book,double p,std::size_t qty,double disc) : Quote(book,p),min_qty(qty),discount(disc){}
除非指出,否则派生类对象的基类部分数据成员会执行默认初始化。
另外,首先会初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。
派生类使用基类的成员
派生类可以访问其基类中的公有成员和受保护的成员。
继承与静态成员
如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义,无论从基类中派生出来多少个派生类,对于每个静态成员来说都只存在唯一的实例。
静态成员遵循通用的访问控制规则。如果某个静态成员是可以访问的,那么既可以通过基类也可以通过派生类使用它。
派生类的声明
派生类的声明包含类名但是不包含它的派生列表:
class Bulk_quote : public Quote; // 错误,派生类列表不能出现在这里
class Bulk_quote; // 正确,声明派生类的正确方式
被用作基类的类
如果想使用某个类作为基类,则该类必须是已经定义而非仅仅声明:
class Quote; // 声明,但未定义
class Bulk_quote : public Quote {...}; // 错误,Quote必须先被定义
派生类中包含并且可以使用它从基类继承而来的成员,为了使用这些成员,派生类当然要先知道它们是什么,因此规定还有一层隐含的意思,即一个类不能派生它本身。
一个类是基类,同时也可以是派生类:
class Base{/*...*/};
calss D1 : public Base {/*...*/};
class D2 : public D1 {/*...*/};
Base
是 D1
的直接基类,同时是 D2
的间接基类。直接基类出现在派生列表中,而间接基类由派生类通过其直接基类继承而来。
防止继承的发生
如果想定义一个类并且不希望从它派生出新的类,可以禁止继承的方式,C++ 11 新标准中在类名后面紧跟一个关键字 final
即可实现:
class NoDerived final{/*...*/}; // NoDerived 不能作为基类
类型转换与继承
通常情况下引用或指针绑定到一个对象时,引用或指针的类型应与对象的类型一致,或者对象的类型含有一个可接受的 const
类型转换规则。存在继承关系的类是一个重要的例外:可以将基类的指针或引用绑定到派生类的对象上。
可以将基类的指针或引用绑定到派生类对象上是一层极为重要的含义:当使用基类的引用或指针时,实际上我们并不清楚该引用或指针所绑定对象的真实类型。该对象可能是基类的对象,也可能是派生类的对象。
智能指针也支持派生类向基类的类型转换。
静态类型与动态类型
当使用存在继承关系的类型时,必须将一个变量或其他表达式的静态类型与该表达式表示对象的动态类型区分开来,表达式的静态类型在编译时总是已知的,它是变量声明时的类型或表达式生成的类型。动态类型则是变量或表达式表示的内存中的对象类型,动态类型直到运行时才可以知道。
如果表达式既不是引用也不是指针,则它的动态类型永远与静态一致。
不存在从基类向派生类的隐式转换类型
之所以存在派生类向基类的类型转换是因为每个派生类对象都包含一个基类部分,基类的引用或指针可以绑定到这部分上。
因为一个基类的对象可能是派生类对象的一部分,也可能不是,所以不存在从基类向派生类的自动类型转换:
Quote base;
Bulk_quote bulkP = &base; // 错误,不能将基类转换成派生类
Bulk_quote& bulkRef = base; // 错误,不能将基类转换成派生类
如果上述转换合法,则我们有可能会使用 bulkP
或 bulkRef
来访问 base
中根本不存在的数据成员。
除此之外还有一种情况显得有点特别,即使一个基类指针或引用绑定在一个派生类对象上,也不能执行从基类向派生类的转换:
Bulk_quote bulk;
Quote *itemP = &bulk; //正确,动态类型是 Bulk_quote
Bulk_quote *bulkP = itemP; //错误,不能将基类转换成派生类
如果在基类中含有一个或多个虚函数,可以使用 dynamic_cast
来请求一个类型转换,该转换的安全检查将在运行时执行,同样。如果已知某个基类向派生类转换是安全的,则可以使用 static_cast
来强制覆盖掉编译器的检查工作。
对象之间不存在类型转换
派生类向基类的自动类型转换只针对指针或引用类型有效,在派生类类型和基类类型之间不存在装的。