1.派生类与基类之间有一些特殊关系。其中之一是派生类对象可以使用基类的方法,条件是方法不是私有的。
2.基类指针/引用可以在不进行显式类型转换的情况下指向/引用派生类对象。(即派生类可以转化为基类,但基类指针或引用只能用于调用基类方法。)
有时我们希望同一个方法在派生类和基类中的行为是不同的,即派生类对基类的方法进行修改,这种行为就是多态。有两种重要的机制可用于实现多态公有继承:
1.在派生类中重新定义基类的方法。
2.使用虚方法。
如果要在派生类中重新定义基类的方法,通常应将基类方法声明为虚的。这样,程序将根据对象类型而不是引用或指针的类型来选择方法版本。为基类声明一个虚析构函数也是一种惯例。
通常应给基类提供一个虚析构函数,即使它并不需要析构函数。
程序调用函数时,将使用哪个可执行代码块呢?编译器负责回答这个问题。将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编(binding)。在C语言中,这非常简单,因为每个函数名都对应一个不同的函数。在C++中,由于函数重载的缘故,这项任务更复杂。编译器必须查看函数参数以及函数名才能确定使用哪个函数。然而,C/C++编译器可以在编译过程完成这种联编。在编译过程中进行联编被称为静态联编(static binding),又称为早期联编(early binding)。然而,虚函数使这项工作变得更困难。正如在程序清单13.10所示的那样,使用哪一个函数是不能在编译时确定的,因为编译器不知道用户将选择哪种类型的对象。所以,编译器必须生成能够在程序运行时选择正确的虚方法的代码,这被称为动态联编(dynamic binding),又称为晚期联编(late binding)。
如果动态联编让您能够重新定义类方法,而静态联编在这方面很差,为何不摒弃静态联编呢?原因有两个——效率和概念模型。
首先来看效率。为使程序能够在运行阶段进行决策,必须采取一些方法来跟踪基类指针或引用指向的对象类型,这增加了额外的处理开销(稍后将介绍一种动态联编方法)。例如,如果类不会用作基类,则不需要动态联编。同样,如果派生类(如RatedPlayer)不重新定义基类的任何方法,也不需要使用动态联编。在这些情况下,使用静态联编更合理,效率也更高。由于静态联编的效率更高,因此被设置为C++的默认选择。Strousstrup说,C++的指导原则之一是,不要为不使用的特性付出代价(内存或者处理时间)。仅当程序设计确实需要虚函数时,才使用它们。
接下来看概念模型。在设计类时,可能包含一些不在派生类重新定义的成员函数。例如,Brass::Balance( )函数返回账户结余,不应该重新定义。不将该函数设置为虚函数,有两方面的好处:首先效率更高;其次,指出不要重新定义该函数。这表明,仅将那些预期将被重新定义的方法声明为虚的。
关键字protected与private相似,在类外只能用公有类成员来访问protected部分中的类成员。private和protected之间的区别只有在基类派生的类中才会表现出来。派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。因此,对于外部世界来说,保护成员的行为与私有成员相似;但对于派生类来说,保护成员的行为与公有成员相似。
最好对类数据成员采用私有访问控制,不要使用保护访问控制;同时通过基类方法使派生类能够访问基类数据。
抽象基类抽象基类(abstract base class,ABC)。
ABC描述的是至少使用一个纯虚函数的接口,从ABC派生出的类将根据派生类的具体特征,使用常规虚函数来实现这种接口。
要成为真正的ABC,必须至少包含一个纯虚函数。原型中的=0使虚函数成为纯虚函数。
可以将ABC看作是一种必须实施的接口。ABC要求具体派生类覆盖其纯虚函数——迫使派生类遵循ABC设置的接口规则。这种模型在基于组件的编程模式中很常见,在这种情况下,使用ABC使得组件设计人员能够制定“接口约定”,这样确保了从ABC派生的所有组件都至少支持ABC指定的功能。
构造函数初始化列表构造函数初始化列表以一个冒号开始,接着是以逗号分隔的数据成员列表,每个数据成员后面跟一个放在括号中的初始化式。例如:
//@1 class A { public: int a; float b; A(): a(0),b(9.9) {} //构造函数初始化列表 }; //@2 class A { public: int a; float b; A() //构造函数内部赋值 { a = 0; b = 9.9; } };