15.1概述
1.继承
2.动态绑定
在C++中通过基类的引用或指针调用虚函数时,发生动态绑定
15.2 定义基类和派生类
基类通常应将派生类需要重定义的任意函数定义为虚函数。
public,private访问控制标号,用户代码(类的对象)可以访问类的public成员,而不能访问类的private成员, private成员只能由基类的成员和友元来访。
派生类对基类的public和private成员的访问权限与程序中的其他任意部分一样, 派生类可以访问protected成员,而类的普通用户则不能访问类的protected对象。
派生类只能通过派生类对象访问基类的protected成员,派生类对其基类类型对象的protected成员没有特殊访问权限。
关键概念:类设计与受保护成员
通过继承,类有三种用户: 类本身的成员、类对象(类的用户)和类的派生类,派生类通常需要访问基类的实现,为了允许这种访问而仍然禁止对实现的一般访问,提供了protected的访问标号。
定义类为基类时,接口函数为public,数据一般为private。 提供给派生类的接口应是protected成员和public成员的组合。
15.2.3派生类
派生类对象包含两个部分:从基类继承的成员和自己定义的成员。
15.2.4 virtual与其他成员函数
C++中函数调用触发动态绑定,两个条件:第一,只有定义为虚函数的成员函数才能进行动态绑定,成员函数默认为非虚函数,非虚函数不进行动态绑定。第二,必须通过基类类型的引用或指针进行函数调用。
关键概念:C++的多态性
引用和指针的静态类型与动态类型可以不同,是C++用以支持多态性的基石。
即通过基类的引用或指针调用基类中定义的函数时,我们不知道执行函数的对象的确切类型,执行函数的对象可以是基类类型的,也可以是派生类类型的。
非虚函数总是在编译时根据调用该函数的对象、引用或指针的类型而确定。
派生类虚函数调用基类版本时,必须显式使用作用域操作符。如果忽略了这样做,则函数调用会在运行时确定并且将是一个自身调用,从而导致无穷递归。
在同一虚函数的基类版本和派生类版本中使用不同的默认实参几乎一定会引起麻烦。
15.2.5 公用、私有和受保护的继承
每个类控制它所定义的成员的访问权限,派生类可以进一步限制但不能放松对所继承的成员的访问。
公用继承(public inheritance),基类成员保持自己的访问级别,基类的public成员为派生类的public成员,基类的protected成员为派生类的protected成员。
受保护继承(protected inheritance)基类的public和protected成员在派生类中为protected成员。
私有继承(private inheritance)基类的所有成员在派生类中为private成员。
public派生类继承基类的接口,它具有与基类相同的接口。设计良好的类层次中,public派生类的对象可以被用在任何需要基类对象的地方。
使用private和protected派生的类不继承基类的接口,相反,这些派生通常被称为实现继承。
最常见的继承形式是public继承。
class保留字 默认具有private继承,struct 默认为public继承。
15.2.6 友元关系与继承
友元可以访问类的private和protected数据。 友元关系不能继承,基类的友元对派生类的成员没有特殊访问权限。
15.2.7 继承与静态成员
如果基类定义了static成员,则整个继承层次中只有一个这样的成员。 无论从基类派生出多少个派生类,每个static成员只有一个实例。
15.3 转换与继承
一般可以使用派生类型对象对基类对象进行赋值或初始化。
将派生类型对象传给希望接受基类对象引用的函数时,引用直接绑定到该对象,实际上实参是该对象的引用,对象本身未被复制。 如果将派生类对象传递给希望接受基类对象(而不是引用)的函数时,形参的类型是固定的----在编译和运行时都是基类的对象。 如果用派生类对象调用这样的函数,则派生类的基类部分被复制到形参。
一个是用派生类的对象转换为基类类型引用,一个是用派生类对象对基类对象进行初始化或赋值。
2.用派生类对象对基类对象进行初始化或赋值
对基类对象进行初始化或赋值,实际上是在调用函数:初始化时调用构造函数,赋值时调用赋值操作符。
即用派生类的基类部分进行初始化或赋值, 即派生类部分在进行初始化或赋值时被切掉了。
15.3.2 基类到派生类的转换
从基类到派生类的自动转换是不存在的。
15.4 构造函数和复制控制
每个派生类对象由派生类中定义的(非static)成员加上一个或多个基类子对象构成。 这一事实影响着派生类对象的构造、复制、赋值和撤销。 构造函数和复制控制成员不能继承,每个类定义自己的构造函数和复制控制成员。
某些类需要只希望派生类使用的特殊构造函数,这样的构造函数应定义为protected。
派生类构造函数的初始化列表只能初始化派生类成员,不能直接初始化继承成员。 派生类构造函数通过将基类包含在构造函数初始化列表中来间接初始化继承成员。
一个类只能初始化自己的直接基类,直接基类就是在派生列表中指定的类。
关键概念:尊重基类接口
构造函数只能初始化其直接基类的原因是每个类都定义了自己的接口,一单类定义了自己的接口,与该类对象的所有交互都应该通过该接口,即使对象是派生类对象的一部分也不例外。
派生类构造函数不能初始化基类的成员且不应该对基类成员赋值。派生类应该通过基类的构造函数尊重基类的初始化意图,而不应该在派生类构造函数体中对这些成员赋值。
15.4.3 复制控制和继承
只包含类类型或内置类型数据成员、不含指针的类一般可以使用合成操作,复制、赋值或撤销这样的成员不需要特殊控制。具有指针成员的类一般需要定义自己的复制控制来管理这些成员。
如果派生类显式定义自己的复制构造函数或赋值操作符,则该定义将完全覆盖默认定义。被继承类的复制构造函数和赋值操作符负责对基类成分以及类自己的成员进行复制和赋值。
15.4.4 虚析构函数
删除指向动态分配对象的指针时,需要运行析构函数在释放对象内存之前清除对象。 指针的静态类型可能与被删除对象的动态类型不同,即可能删除实际上指向派生类对象的基类类型指针,删除基类指针,则需要运行基类的析构函数并清除基类的成员,如果对象实际是派生类型的,则该行为没有定义。要保证运行适当的析构函数,基类中得析构函数必须为虚函数。
三法则(rule of three):
如果类需要析构函数,则它也需要赋值操作符和复制构造函数。
如果析构函数为虚函数,那么通过指针调用时,运行哪个析构函数将因指针所指对象类型的不同而不同。
即使析构函数没有工作要做,继承层次的根类也应该定义一个虚析构函数。
只有析构函数应定义为虚函数,构造函数不能定义为虚函数。
将类的赋值操作符设为虚函数很可能会令人混淆,而且不会有什么用处。
15.4.5 构造函数和析构函数中的虚函数
构造派生类对象时,首先运行基类的构造函数初始化对象的基类部分。 撤销派生类对象时,首先撤销派生类部分,然后按照与构造顺序的逆序撤销它的基类部分。
在以上两种情况下,运行构造函数或析构函数的时候,对象都是不完整的。 在基类构造函数或析构函数中,将派生类对象当做基类类型对象对待。
如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本。???
15.5 继承情况下得类作用域
每个类保持自己的作用域,派生类的作用域嵌套在基类作用域中。 如果不能在派生类作用域中确定名字,就在外围基类作用域中查找该名字的定义。
15.5.1 名字查找在编译时发生
对象、引用或指针的静态类型决定了对象能够完成的行为,甚至当静态类型和动态类型可能不同的时候,即基类类型的引用或指针时,静态类型仍然决定着可以使用什么成员。
15.5.2 名字冲突与继承
与基类成员同名的派生类成员将屏蔽对基类成员的直接访问。
设计派生类时,只要可能,最好避免与基类成员的名字冲突。
15.5.3 作用域与成员函数
基类和派生类中使用同一名字的成员函数,其行为与数据成员一样:派生类作用域中的派生类成员将屏蔽基类成员。 即使函数原型不同,基类成员也会被屏蔽。
局部作用域中声明的函数不会重载全局作用域中定义的函数,即派生类中定义的函数也不会重载基类中定义的成员。通过派生类对象调用函数时,实参必须与派生类中定义的版本相匹配,只有派生类根本没有该函数时,才考虑基类函数。
派生类不用重定义所继承的每一个基类版本,可以为重载成员提供using声明。派生类只需要重定义本类型确实必须定义的那些函数,对其他版本可以使用继承的定义。
15.5.4 虚函数与作用域
关键概念:名字查找与继承
C++中继承层次中函数调用遵循以下四个步骤:
1.首先确定函数调用对象、引用或指针的静态类型。
2.在该类中查找函数,如果找不到,就在直接基类中查找,如此循着类的继承链往上找,直到找到函数或者查找玩最后一个类。如果找不到则调用非法。
3.一旦找到了该名字,就进行常规类型检查
4. 假定函数调用合法,编译器就生成代码。如果函数时虚函数且通过引用或指针调用,则编译器生成代码以确定根据对象的动态类型运行哪个函数版本。
15.6 纯虚函数
在形参表后面写上=0以指定纯虚函数:
class Disc_item: public Item_base{
public:
double net_price(std::size_t) const=0;
};
含有(或继承)一个或多个纯虚函数的类是抽象基类(abstract base class).抽象基类可以作为其派生类的组成部分,不能创建抽象基类的对象。
15.7 容器与继承
15.8 句柄类与继承
C++中通过定义包装类(cover)或句柄类(handle)来支持面向对象编程。句柄类存储和管理基类指针
包装了继承层次的句柄两个重要的设计考虑因素:
- 像对任何保存指针的类一样,必须确定对复制控制做些什么。包装了继承层次的句柄通常表现得像一个智能指针或像一个值。
- 句柄类决定句柄接口屏蔽还是不屏蔽继承层次,如果不屏蔽继承层次,用户必须了解和使用基本层次中的对象。
15.8.1 指针型句柄
定义一个名为Sales_item的指针型句柄类,表示Item_base层次。 Sales_item类有三个构造函数,默认构造函数,复制构造函数和接受Item_base对象的构造函数。 第三个构造函数将复制Item_base对象,并保证:只要Sales_item对象存在副本就存在。当复制Sales_item对象或给Sales_item对象赋值时,将复制指针而不是复制对象。
Sales_item类有两个数据成员,都是指针,一个指向Item_base对象,另一个指向使用计数。
class Sales_item{
public:
Sales_item():p(0),use(new std::size_t(1) ){}
Sales_item(const Item_base &);
Sales_item(const Sales_item &i):p(i.p),use(i.use){ ++*use; }
~Sales_item() {decr_use();}
private:
Item_base *p;
std:: size_t *use;
}
要实现接受Item_base对象的构造函数,必须首先解决,不知道它是一个Item_base对象或者是一个Item_base派生类的对象。
句柄类经常需要在不知道对象确切类型时分配已知对象的新副本。
解决这个问题的通用办法是定义虚操作进行复制,即clone。
为了支持句柄类,需要从基类开始,在继承层次的每个类型中增加clone,基类必须将该函数定义为虚函数:
class Item_base{
public:
virtual Item_base* clone() const
{ return new Item_base(*this); }
};
每个类必须重定义该虚函数。 因为函数的存在是为了生成类对象的新副本,所以定义返回类型为类本身:
class Bulk_item: public Item_base{
public:
Bulk_item* clone() const
{ return new Bulk_item(*this); }
};
如果虚函数的基类实例返回类类型的引用或指针,则该虚函数的派生类实例可以返回基类实例返回的类型的派生类(或者是类类型的指针或引用)。
一旦有了clone函数,就可以这样编写Sales_item构造函数:
Sales_item::Sales_item(const Item_base &item):
p(item.clone() ), use( new std:: size_t(1) ) { }