 
1 一个简单的基类
从一个类派生出另一个类时,原始类称为基类,继承类称为派生类。为说明继承,首先需要一个基类。   
3 多态公有继承
RatedPlayer继承示例很简单。派生类对象使用基类的方法,而未做任何修改。然而,可能会遇到这样的情况,即希望同一个方法在派生类和基类中的行为是不同的。换句话来说,方法的行为应取决于调用该方法的对象。这种较复杂的行为称为多态——具有多种形态,即同一个方法的行为随上下文而异。有两种重要的机制可用于实现多态公有继承:
现在来看另一个例子。由于Webtown 俱乐部的工作经历,您成了Pontoon银行的首席程序员。银行要求您完成的第一项工作是开发两个类。 一个类用于表示基本支票账户一Brass Account, 另一个类用于表示代表Brass Plus 支票账户,它添加了透支保护特性。也就是说,如果用户签出一张超出其存款余额的支票——但是超过的数额并不是很大,银行将支付这张支票,对超出的部分收取额外的费用,并追加罚款。可以根据要保存的数据以及允许执行的操作来确定这两种账户的特征。
下面是用于Brass Account支票账户的信息:
下面是可以执行的操作:
Pontoon银行希望Brass Plus支票账户包含Brass Account的所有信息及如下信息:
不需要新增操作,但有两种操作的实现不同:
- 对于取款操作,必须考虑透支保护
- 显示操作必须显示Brass Plus账户的其他信息
假设将第一个类命名为Brass,第二个类为BrassPlus。应从Brass公有派生出BrassPlus吗?要回答这个问题,必须先回答另一个问题: BrassPlus 类是否满足is-a条件?当然满足。对于Brass对象是正确的事情,对于BrassPlus对象也是正确的。它们都将保存客户姓名、账号以及结余。使用这两个类都可以存款、取款和显示账户信息。请注意,is-a 关系通常是不可逆的。也就是说,水果不是香蕉:同样,Brass 对象不具备BrassPlus对象的所有功能。
3.1 开发Brass类和BrassPlus类
Brass Account类的信息很简单,但是银行没有告诉您有关透支系统的细节,但是提供了如下信息:
- Brass Plus账户限制了客户的透支款额。默认为500元,但有些客户的限额可能不同
- 银行可以修改客户的透支限额;
- Brass Plus账户对贷款收取利息。默认为11.125%,但有些客户的利率可能不同
- 银行可以修改客户的利率;
- 账户记录客户所欠银行的金额(透支数额加利息)。用户不能通过常规存款或从其他账户转账的方式偿付,而必须以现金的方式交给特定的银行工作人员。如果有必要,工作人员可以找到该客户。欠款偿还后,欠款金额将归零。
最后一种特性是银行出于做生意的考虑而采用的,这种方法有它有利的一面——使编程更简单。
上述列表表明,新的类需要构造函数,而且构造函数应提供账户信息,设置透支上限(默认为500元)和利率(默认为11.125%)。另外,还应有重新设置透支限额、利率和当前欠款的方法。要添加到Brass类中的就是这些,这将在BrassPlus类声明中声明。
有关这两个类的信息声明,类声明应类似于程序清单13.7。
#ifndef BRASS_H_
#define BRASS_H_
#include<string>
class Brass{
private:
std::string fullName;
long acctNum;
double balance;
public:
Brass(const std::string & s = "Nullbody", long an=-1,
double bal=0.0);
void Deposit(double amt);
virtual void Withdraw(double amt);
double Balance() const;
virtual void ViewAcct() const;
virtual ~Brass(){}
};
class BrassPlus:public Brass{
private:
double maxLoan;
double rate;
double owesBank;
public:
BrassPlus(const std::string & s = "Nullbody", long an = -1,
double bal = 0.0, double ml=500,
double r = 0.11125);
BrassPlus(const Brass & ba, double ml=500, double r = 0.11125);
virtual void ViewAcct() const;
virtual void Withdraw(double amt);
void ResetMax(double m) { maxLoan = m; }
void ResetRate(double r) { rate = r; }
void ResetOwes() { owesBank = 0; }
};
# endif
对于程序清单 13.7 需要说明的有下面几点:
- BrassPlus类在Brass类的基础上添加了3个私有数据成员和3个公有成员函数;
- Brass类和BrassPlus类都声明了
ViewAcct() 和Withdraw() 方法,但BrassPlus对象和Brass对象的这些方法的行为是不同的; - Brass类在声明
ViewAcct() 和Withdraw() 时使用了新关键字virtual 。这些方法被称为虚方法; - Brass类还声明了一个虚析构函数,虽然该析构函数不执行任何操作
第二点介绍了声明如何指出方法在派生类的行为的不同。两个ViewAcct() 原型表明将有2个独立的方法定义。基类版本的限定名为Brass::ViewAcct() ,派生类版本的限定名为BrassPlus::ViewAcct() 。程序将使用对象类型来确定使用哪个版本:
Brass dom("Dominic Banker",11224,4183.45)
BrassPlus dot("Dorothy Banker",12118,2592.00);
dom.ViewAcct();
dot.ViewAcct();
同样,WidthDraw() 也有两个版本,一个供Brass对象使用,另一个供BrassPlus对象使用。对于在两个类中行为相同的方法(如Deposit() 和Balance() ),则只在基类中声明。
第三点(虚函数)比较复杂。如果方法是通过引用或指针而不是对象调用的,它将确定使用哪一种方法。如果没有使用关键字virtual,程序将根据引用类型或指针类型选择方法;如果使用了virtual,程序将根据引用或指针指向的对象的类型来选择方法。如果ViewAcct() 不是虚的,则程序的行为如下:
Brass dom("Dominic Banker",11224,4183.45);
BrassPlus dot("Dorothy Banker",12118,2592.00);
Brass & b1_ref = dom;
Brass & b2_ref = dot;
b1_ref.ViewAcct();
b2_ref.ViewAcct();
引用变量的类型为Brass,所以选择了Brass::ViewAccount() 。使用Brass指针代替引用时,行为将与此类似。
如果ViewAcct() 是虚的,则行为如下:
Brass dom("Dominic Banker",11224,4183.45);
BrassPlus dot("Dorothy Banker",12118,2592.00);
Brass & b1_ref = dom;
Brass & b2_ref = dot;
b1_ref.ViewAcct();
b2_ref.ViewAcct();
这里两个引用的类型都是Brass,但b2_ref 引用的是一个BrassPlus对象,所以使用的是BrassPlus::ViewAcct() 。使用Brass指针代替引用时,行为将类似。
虚函数的这种行为非常方便。因此,经常在基类中将派生类会重新定义的方法声明为虚方法。方法在基类中被声明为虚的后,它在派生类中将自动成为虚方法。然而,在派生类声明中使用关键字virtual来指出哪些函数是虚函数也不失为一个好方法。
第四点是,基类声明了一个虚析构函数。这样做是为了确保释放派生对象时,按正确的顺序调用析构函数。本文后面将详细介绍这个问题。为基类声明一个虚析构函数也是一种惯例。
510 


          

   
   
 
 
4 静态联编和动态联编
程序调用函数时,将使用哪个可执行代码块呢?编译器负责回答这个问题。将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编(binding)。 在C语言中,这非常简单,因为每个函数名都对应一个不同的函数。在C++中,由于函数重载的缘故,这项任务更复杂。编译器必须查看函数参数以及函数名才能确定使用哪个函数。然而,C/C++编译器可以在编译过程完成这种联编。在编译过程中进行联编被称为静态联编(static binding),又称为早期联编(early binding)。然而,虚函数使这项工作变得更困难。正如在程序清单13.10所示的那样,使用哪一个函数是不能在编译时确定的,因为编译器不知道用户将选择哪种类型的对象。所以,编译器必须生成能够在程序运行时选择正确的虚方法的代码,这被称为动态联编(dynamic binding), 又称为晚期联编(late binding)。
知道虚方法的行为后,下面深入地探讨这一过程, 首先介绍C++如何处理指针和引用类型的兼容性。
待补充 518
|