继承机制使得我们声明一个类继承自另一个类,我们可以将继承类对象当做其基类对象来使用。
15.1OOP:概述
面向对象程序设计的核心:
1、数据抽象 —— 接口和实现分离
2、继承 —— 定义相似的类型
3、动态绑定 —— 以统一的方式使用基类和派生类的对象
虚函数
基类希望有些函数能够适应不同派生类的版本,就把这些函数声明成虚函数(virtual function)
虚函数使用时候的必要操作:
1、基类指明哪些函数是虚函数
2、派生类还要对虚函数进行重新声明。
派生类对虚函数重新声明的两种方式:
//1、直接通过virtual关键字
virtual type func(type xxx);
//2、c++11新标准使用override关键字
type func(type xxx) override;
使用override要注意的地方:
1、是派生类才可以用它来表明是虚函数
2、override有自动检查虚函数错误的功能。
动态绑定(dynamic binding)
动态绑定时调用的参数是基类的引用或者指针,所以既可以使用基类对象调用该函数,也可以使用派生类的对象调用它。调用虚函数的运行版本是由虚函数决定,所以是在运行时选择函数的版本,又把动态绑定称运行时绑定。
NOTE:在c++中,使用基类的引用(指针)调用一个虚函数时发生动态绑定。
15.2定义基类和派生类
15.2.1定义基类
NOTE:基类的析构函数应该定义为虚函数
成员函数与继承
派生类必须对基类虚函数重定义,以覆盖(override)从基类继承来的旧定义
基类必须把两种成员函数区分开:
1、希望派生类进行覆盖的定义为虚函数
2、希望直接继承不要改变的函数
关键字virtual只能用于类内部的声明,而不能用于类外部的函数定义
普通成员函数和虚函数的区别:前者解析过程发生在编译时,而后者发生在运行时
访问控制与继承
为什么定义受保护(protected)成员类型:基类希望派生类访问该成员,同时禁止其他用户的访问
15.2.2定义派生类
访问说明符:public,protected,private
派生类必须将其继承而来的成员函数中需要覆盖的重新声明(虚函数)
派生类中的虚函数
派生类经常(但不总是)覆盖它继承的虚函数,如果派生类没有覆盖,则虚函数行为类似于其它普通成员,直接继承基类的版本。
派生类可以在它覆盖的函数前使用virtual关键字(也可以不加);也可以使用c++11的新标准允许派生类显示地著名它使用某个成员函数覆盖它继承的虚函数。
class Quote {
public:
virtual double net_price(size_t n) const { return n * price; }
protected:
double price = 0.0;
};
class Bulk_quote :public Quote {
public:
double net_price(size_t) const override; // ---1 override ---
//virtual double net_price(size_t) const; //---2 virtual ---
};
NOTE:基类的虚函数在派生类中隐含的也是一个虚函数
派生类对象及派生类向基类类型转换
派生类对象包含:派生类自己定义的(非静态)部分,从基类继承的部分。
因为派生类对象包含基类对应的部分,所以可以把派生类对象当成基类对象使用;并且把基类指针或引用绑定到派生类对象的基类部分。
Quote item;
Bulk_quote bulk;
Quote *p = &item;
p = &bulk; //p指向bulk中Quote部分
Quote r = bulk; //r绑定到bulk中Quote部分
这种转换称:派生类到基类(derived-to-base)类型转换。
这种隐式转换意味:
1、派生类对象或派生类对象的引用可以用在基类引用的地方
2、派生类对象的指针用在需要基类指针的地方
NOTE:派生类对象中含有与基类对应的组成部分,这一事实是继承的关键所在。
派生类的构造函数
派生类并不能直接初始化从基类继承的函数,必须使用基类的构造函数来初始化它的基类部分。
NOTE:每个类控制自己的成员初始化过程
派生类构造函数通过构造函数初始化列表将实参传递给基类构造函数。
class Bulk_quote :public Quote {
public:
Bulk_quote(const string &book, double p, size_t qty, double disc) :Quote(book, p), min_qty(qty), discount(disc) {};
private:
size_t min_qty = 0;
double discount = 0.0;
};
NOTE:首先通过基类构造函数初始化基类部分,然后按照声明依次去初始化派生类的成员。
派生类使用基类的成员
派生类可以访问共有成员和受保护的成员
关键概念:遵循基类的接口
每个类负责定义各自的接口,要想与类的对象交互必须使用该类的接口,即使对象是派生类的基类部分也必须如此。因此,派生类对象不能直接初始化基类的成员。派生类应该遵循基类的接口,并且调用基类的构造函数来初始化那么从基类中继承的成员。
继承与静态成员
如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义,不管基类派生出多少派生类,对于每个静态成员都只有唯一的实例。同时静态成员遵守访问控制规则。
派生类的声明
和普通声明没有区别,声明中不用包含它的派生列表
被用作基类的类
如果类被用作是基类,则必须已经定义。
直接基类(direct base):出现在派生列表
间接基类(indirect base):由派生类通过其直接基类继承而来
每个类都会继承直接基类的所有成员,最终的派生类会包含它直接基类的子对象以及每个间接基类的子对象。
防止继承的发生
c++11提供一种防止继承发生的办法,在类名后跟一个关键字final
class NoDerived final{/**/}
15.2.3类型转换与继承
WARNING:理解基类与派生类之间的类型转换是理解c++面向对象编程的关键
可以把基类的指针或引用绑定到派生类对象上有很重要的意义:当使用基类的引用(指针)时,实际上我们并不清楚引用(指针)所绑定的对象的真是类型,可能是基类的对象,也可能是派生类的对象。
NOTE:和内置指针意义,智能指针类也支持派生类向基类的类型转换,意味着可以将一个派生类对象的指针存储在一个基类的智能指针内。
静态类型在编译时总是已知的,它是变量声明时的类型或表达式生成的类型;动态类型是变量或表达式表示的内存中的对象的类型。直到运行的时候才知道。
Quote item1;
Quote &item2 =item;
double ret = item2.net_price(n);
Bulk_quote item3;
Quote &item4 =item3;
double ret2 = item4.net_price(n);
item2,item4的静态类型都是Quote&,它的动态类型则依赖于item绑定的实参。动态类型直到运行到该函数才知道。如果表达式既不是引用也不是指针,则它的动态类型永远和静态类型一致。
NOTE:基类指针或引用的静态类型可能与动态类型不一致,读者一定要理解其中的原因
不存在基类向派生类的隐式类型转换……
之所以存在派生类向基类的类型转换是因为每个派生类对象都包含一个基类部分,而基类的引用或指针可以绑定到该基类部分上。
派生类的指针和引用不能绑定到基类对象
Quote base;
Bulk_quote* bulkp = &base;
//错误,基类不可以向派生类转换
为什么错误?如果允许了这种转换,则我们可能通过bulkp指针访问派生类Bulk_quote当中的成员,然而这个成员可能不存在于基类base之中。
即使基类指针或者引用绑定在派生类对象上,我们也不能再定义一个派生类指针指向该指针。
Bulk_quote bulk;
Quote* itemp = &bulk;
Bulk_quote *bulkp = itemP;
编译器在编译时无法确定某个特定的转换在运行时是否安全,因为编译器只能检查指针或者引用的静态类型来判断该转换是否合法。如果基类中含有一个或者多个虚函数,我们可以使用dynamic_cast请求类型转换,该转换的安全检查将在运行时执行(P730);如果我们一直某个基类向派生类的转换是安全的,则我们可以使用static_cast来强制覆盖掉编译器的检查工作(P144)。
……对象之间不存在类型转换
派生类向基类的自动类型转换只对指针或引用类型有效。
Bulk_quote bulk;
Quote item(bulk);
item = bulk;
上述初始化或者赋值的过程,只有属于基类Quote的部分由bulk拷贝,赋值了,然而Bulk_quote 自己的部分会被忽略,也就可以说bulk自己的部分被切掉了(sliced down)
WARNING:当我们用一个派生类对象为一个基类对象初始化/赋值时,只有该派生类对象中基类部分会被拷贝、移动、赋值,它的派生类部分被忽略掉了。
关键概念:
具有继承关系的类之间发生的类型转换,有三点很重要:
- 从派生类向基类的类型转换只对指针或引用有效;
- 基类向派生类不存在隐式类型转换;
- 派生类向基类的类型转换也可能会由于访问受限而变得不可行;
虽然自动类型转换只对指针或引用类型有效,继承体系中大多数类仍然(显示或者隐式)定义了拷贝控制成员,因此,我们通常能够将一个派生类对象拷贝、移动或赋值给一个基类对象。不过需要注意的是,这种操作只处理派生类对象的基类部分。