派生类和基类之间的特殊关系是基于 C++ 继承的底层模型的。实际上,C++有3种继承方式:公有继承、保护继承和私有继承。公有继承是最常用的方式,它建立一种is-a关系,即派生类对象也是一个基类对象,可以对基类对象执行的任何操作也可以对派生类对象执行。例如,假设有一个 Fruit 类,可以保存水果的重量和热量。因为香蕉是一种特殊的水果,所以可以从 Fruit 类派生出 Banana 类。新类将继承原始类的所有数据成员,因此,Banana对象将包含表示香蕉重量和热量的成员。新的Banana类还添加了专门用于香蕉的成员,这些成员通常不用于水果,例如 Banana Institute Peel Index(香蕉机构果皮索引)。因为派生类可以添加特性,所以,将这种关系成为 is-a-kind-of(是一种) 关系可能更准确,但是通常使用属于 is-a。
为阐明 is-a 关系,来看一些与该模型不符的例子。公有继承不建立 has-a 关系。例如,午餐可能包括水果,但通常午餐不是水果。所以,不能通过从 Fruit 类派生出 Lunch 类来在午餐中添加水果。在午餐中加入水果的正确方法是将其作为一种 has-a 关系:午餐有水果。正如将在之后介绍的,最容易的建模方式是,将 Fruit 对象作为 Lunch 类的数据成员。
公有继承不能建立 is-like-a 关系,也就是说,它不采用明喻。人们通常说律师就像鲨鱼,但律师并不是鲨鱼。例如,鲨鱼可以在水下生活。所以,不应从 Shark 类派生出 Lawyer 类。继承可以在基类的基础上添加属性,但不能删除基类的属性。在有些情况下,可以设计一个包含共有特征的类,然后以 is-a 或 has-a 关系,在这个类的基础上定义相关的类。
公有继承不建立 is-implemented-as-a(作为…来实现)关系。例如,可以使用数组来实现栈,但从 Array 类派生出 Stack 类是不合适的,因为栈不是数组。例如,数组索引不是栈的属性。另外,可以以其它方式实现栈,如链表。正确的方法是,通过让栈包含一个私有 Array 对象成员来隐藏数组实现。
公有继承不建立 use-a 关系。例如,计算机可以使用激光打印机,但从 Computer 来派生出 Printer 类(或反过来)是没有意义的。然而,可以使用友元函数或类来处理 Printer 对象和 Computer 对象之间的通信。
在 C++ 中,完全可以使用公有继承来建立 has-a、is-implemented-as-a 或 use-a 关系;然而,这样做通常会导致编程方面的问题。因此,还是坚持使用 is-a 关系吧。
RatedPlayer 继承示例很简单。派生类对象使用基类的方法,而未作任何修改。然而,可能会遇到这样的情况,即希望同一个方法在派生类和基类中的行为是不同的。换句话来说,方法的行为应取决于调用该方法的对象。这种较复杂的行为称为多态——具有多种形态,即同一个方法的行为随上下文而异,有两种重要的机制可用于实现多态公有继承;
现在来看另一个例子。由于 Webtown 俱乐部的工作经历,您成了 Ponton 银行的首席程序员。银行要求您完成的第一项工作是开发两个类。一个类用于表示基本支票账户——Brass Account,另一个类用于表示代表 Brass Plus 支票账户,它添加了透支保护特性。也就是说,如果用户签出一张超出其存款余额的支票——但是超出的数额并不是很大,银行将支付这张支票,对超出的部分收取额外的费用,并追加罚款。可以根据要保存的数据以及允许执行的操作来确定这两种账户的特征。
下面是用于 Brass Account 支票账户的信息:
下面是可执行的操作:
Pontoon 银行希望 Brass Plus 支票账户包含 Brass Account 的所有信息及如下信息:
不需要新增操作,但有两种操作的实现不同:
假设将第一个类命名为 Brass,第二个类为 BrassPlus。应从 Brass 公有派生出 BrassPlus 吗?要回答这个问题,必须先回答另一个问题:BrassPlus 类是否满足 is-a 条件?当然满足。对于 Brass 对象是正确的事情,对于 BrassPlus 对象也是正确的。它们都将保存客户姓名、账号以及结余。使用这两个类都可以存款、取款和显示账户信息。请注意,is-a 关系通常是不可逆的。也就是说,水果不是香蕉;同样,Brass 对象不具备 BrassPlus 对象的所有功能。
Brass Account 类的信息很简单,但是银行没有告诉您有关透支系统的细节。当您向友好的Pontoon 银行代表询问时,他提供了如下信息:
最后一种特性是银行出于做生意的考虑而采用的,这种方法有它有利的一面——使编程更简单。
上述列表表明,新的类需要构造函数,而且构造函数应提供账户信息,设置透支上限(默认为 500 元)和利率(默认为 11.125%)。另外,还应有重新设置透支限额、利率和当前欠款的方法。要添加到 Brass 类中的就是这些,这将在 BrassPlus 类声明中声明。
// brass.h -- bank account classes
#ifndef BRASS_H_
#define BRASS_H_#includeclass 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() {}
};// Brass Plus Account Class
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
对上面的程序,需要说明的有下面几点:
BrassPlus 类在 Brass 类的基础上添加了 3 个私有数据成员和 3 个公有成员函数;
Brass 类和 BrassPlus 类都声明了 ViewAcct() 和 Withdraw() 方法,但 BrassPlus 对象和 Brass 对象的这些方法的行为是不同的;
Brass 类在声明 ViewAcct() 和 Withdraw() 时使用了新关键字 virtual。这些方法被称为虚方法( virtual method);
Brass 类还声明了一个虚析构函数,虽然该析构函数不执行任何操作。
第一点没有什么新鲜的。RatedPlayer 类在 TableTennisPlayer 类的基础上添加新数据成员和2个新方法的方式与此类似。
第二点介绍了声明如何指出方法在派生类的行为的不同。两个 ViewAcct() 原型表明将有2个独立的方法定义。基类版本的限定名为 Brass::ViewAcct(),派生类版本的限定名为 BrassPlus::ViewAcct()。程序将使用对象类型来确定使用哪个版本:
Brass dom("Dominic Banker", 11224, 4183.45);
BrassPlus dot("Dorothy Banker", 12118, 2592.00);
dom.viewAcct(); // use Brass::ViewAcct()
dot.viewAcct(); // use BrassPlus::ViewAcct()
同样,Withdraw() 也有 2 个版本,一个供 Brass 对象调用,另一个供 BrassPlus 对象使用。对于在两个类中行为相同的方法( 如Deposit() 和 Balance() ),则只在基类中声明。
第三点(使用 virtual )比前两点要复杂。如果方法是通过引用或指针而不是对象调用的,它将确定使用哪一种方法。如果没有使用关键字 virtual,程序将根据引用类型或指针类型选择方法;如果使用了 virtual,程序将根据引用或指针指向的对象的类型来选择方法。如果 ViewAcct() 不是虚的,则程序的行为如下:
// behaviour with non-virtual ViewAcct()
// method chosen according to reference type
Brass dom("Dominic Banker", 11224, 4183.45);
BrassPlus dot("Dorothy Banker", 12118, 2592.00);
Brase & b1_ref = dom;
Brase & b2_ref = dot;
b1_ref.ViewAcct(); // use Brass::ViewAcct()
b2_ref.ViewAcct(); // use Brass::ViewAcct()
引用变量的类型为 Brass, 所以选择了 Brass::ViewAccount()。使用 Brass 指针代替引用时,行为将与此类似。
如果 ViewAcct() 是虚的,则行为如下:
// behavior with virtual ViewAcct()
// method chosen according to object type
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(); // use Brass::ViewAcct()
b2_ref.ViewAcct(); // use BrassPlus::ViewAcct()
这里两个引用的类型都是 Brass,但 b2_ref 引用的是一个 BrassPlus 对象,所以使用的是 BrassPlus::ViewAcct()。使用 Brass 指针代替引用时,行为将类似。
稍后您将看到,虚函数的这种行为非常方便。因此,经常在基类中将派生类会重新定义的方法声明为虚方法。方法在基类中被声明为虚的后,它在派生类中将自动成为虚方法。然而,在派生类声明中使用关键字 virtual 来指出哪些函数是虚函数也不失为一个好办法。
第四点是,基类声明了一个虚析构函数。这样做是为了确保释放派生对象时,按正确的顺序调用析构函数。本章后面将详细介绍这个问题。
注意:如果要在派生类中重新定义基类的方法,通常应将基类方法声明为虚的。这样,程序将根据对象类型而不是引用或指针的类型来选择方法版本。为基类声明一个虚析构函数也是一种惯例。
// brass.cpp -- bank account class methods
#include
#include #include "13.7_brass.h"using std::cout;
using std::endl;
using std::string;// formatting stuff
typedef std::ios_base::fmtflags format;
typedef std::streamsize precis;
format setFormat();
void restore(format f, precis p);// Brass methodsBrass::Brass(const string & s, long an, double bal){fullName = s;acctNum = an;balance = bal;
}void Brass::Deposit(double amt){if(amt<0){cout << "Negative deposit not allowed; "<< "deposit is cancelled.\n";}else{balance += amt;}
}void Brass::Withdraw(double amt){// set up ###.## formatformat initialState = setFormat();precis prec = cout.precision(2);if (amt < 0){cout << "Withdrawal amount must be positive; "<< "withdrawal canceled.\n";}else if(amt <= balance)balance -= amt;else{cout << "Withdrawal amount of $" << amt<< " exceeds your balance.\n"<< "Withdrawal canceled.\n";}restore(initialState, prec);
}double Brass::Balance() const{return balance;
}void Brass::ViewAcct() const{// set up ###.## formatformat initialState = setFormat();precis prec = cout.precision(2);cout << "Client: " << fullName << endl;cout << "Account Number: " << acctNum << endl;cout << "Balance: $" << balance << endl;restore(initialState, prec); // restore original format
}// BrassPlus Methods
BrassPlus::BrassPlus(const string & s, long an, double bal,double ml, double r) : Brass(s, an, bal)
{maxLoan = ml;owesBank = 0.0;rate = r;
}// redefine how ViewAcct() works
void BrassPlus::ViewAcct() const{// set up ###.## formatformat initialState = setFormat();precis prec = cout.precision(2);Brass::ViewAcct(); // display base portioncout << "Maximum loan: $" << maxLoan << endl;cout << "Owed to bank: $" << owesBank << endl;cout.precision(3); // ###.### formatcout << "Loan Rate: " << 100 * rate << "%\n";restore(initialState, prec);
}// redefine how Withdraw() works
void BrassPlus::Withdraw(double amt){// set up ###.## formatformat initialState = setFormat();precis prec = cout.precision(2);double bal = Balance();if (amt <= bal){Brass::Withdraw(amt);}else if( amt <= bal + maxLoan - owesBank){double advance = amt - bal;owesBank += advance *(1.0 + rate);cout << "Bank advance: $" << advance << endl;cout << "Finance charge: $" << advance * rate << endl;Deposit(advance);Brass::Withdraw(amt);}else {cout << "Credit limit exceeded. Transaction cancelled.\n";}restore(initialState, prec);
}format setFormat(){// set up ###.## formatreturn cout.setf(std::ios_base::fixed, std::ios_base::floatfield);
}void restore(format f, precis p){cout.setf(f, std::ios_base::floatfield);cout.precision(p);
}
介绍上述程序的具体细节(比如一些方法的格式化处理)之前,先来看一下与继承直接相关的方面。记住,派生类不能直接访问基类的私有数据,而必须使用基类的公有方法才能访问这些数据。访问的方式取决于方法。构造函数使用一种技术,而其它成员函数使用另一种技术。
派生类构造函数在初始化基类私有数据时,采用的是成员初始化列表语法。RatedPlayer 类构造函数和 BrassPlus 构造函数都使用这种技术:
BrassPlus::BrassPlus(const string & s, long an, double bal, double ml, double r): Brass(s, an, bal){maxLoan = ml;owesBank = 0.0;rate = r;
}
BrassPlus::BrassPlus(const Brass & ba, double ml, double r) : Brass(ba)// use implicit copy constructor
{maxLoan = ml;owesBank = 0.0;rate = r;
}
这几个构造函数都使用成员初始化列表语法,将基类信息传递给基类构造函数,然后使用构造函数体初始化 BrassPlus 类新增的数据项。
非构造函数不能使用成员初始化列表语法,但派生类方法可以调用公有的基类方法。例如,BrassPlus 版本的 ViewAcct() 核心内容如下(忽略了格式方面):
// redefine how ViewAcct() works
void BrassPlus::ViewAcct() const{...Brass::ViewAcct(); // display base portioncout << "Maximum loan: $ " << maxLoan << endl;cout << "Owed to bank: $ " << owesBank << endl;cout << "Loan Rate: " << 100 * rate << "%\n";
}
换句话说,BrassPlus::ViewAcct() 显示新增的 BrassPlus 数据成员,并调用基类方法 Brass::ViewAcct() 来显示基类数据成员。在派生类方法中,标准技术是使用作用域解析运算符来调用基类方法。
代码必须使用作用域解析运算符。假如这样编写代码:
// redefine erroneously how ViewAcct() works
void BrassPlus::ViewAccct() const{...ViewAcct(); // oops! recursive call...
}
编译器将认为 ViewAcct() 是 BrassPlus::ViewAcct(),这将创建一个不会终止的递归函数——这可不好。
接下来看 BrassPlus::Withdraw() 方法。如果客户提取的金额超过了结余,该方法将安排贷款。它可以使用 Brass::Withdraw() 来访问 balance 成员,但如果取款金额超过了结余,Brass::Withdraw() 将发出一个错误消息。这种实现使用 Deposit() 方法进行放贷,然后在得到了足够的结余后调用 Brass::Withdraw,从而避免了错误消息:
// redefine how Withdraw() work
void BrassPlus::Withdraw(double amt){...double bal = Balance();if ( amt <= bal ){Brass::Withdraw(amt);}else if ( amt <= bal + maxLoan - owesBank ){double advance = amt - bal;owesBank += advance * (1.0 + rate);cout << "Bank advance: $" << advance << endl;cout << "Finance charge: $" << advance * rate << endl;Deposit(advance);Brass::Withdraw(amt);}else{cout << "Credit limit exceeded. Transaction cancelled.\n";}...
}
该方法使用基类的Balance()函数来确定结余。因为派生类没有重新定义该方法,代码不必对 Balance() 使用作用域解析运算符。
方法 ViewAcct() 和 Withdraw() 使用格式化方法 setf() 和 precision() 将浮点值的输出模式设置为定点,即包含两位小数。设置模式后,输出的模式将保持不变,因此该方法将格式模式重置为调用前的状态。未避免代码重复,该程序将设置格式的代码放在辅助函数中:
// formatting stufftypedef std::ios_base::fmtflags format;typedef std::streamsize precis;format setFormat();void restore(format f, precis p);```函数 setFormat() 设置定点表示法并返回以前的标记设置:```format setFormat(){return cout.setf(std::ios_base::fixed, std::ios_base::floatfield);}```而函数 restore() 重置格式和精度:```void restore(format f, precis p){cout.setf(f, std::ios_base::floatfield);cout.precision(p);}```
2. 使用 Brass 和 BrassPlus 类```// usebrass1.cpp -- testing bank account classes
// compile with brass.cpp
#include
#include"13.7_brass.h"int main(){using std::cout;using std::endl;Brass Piggy("Porcelot Pigg", 381299, 4000.00 );BrassPlus Hoggy("Horatio Hogg", 382288, 3000.00);Piggy.ViewAcct();cout << endl;cout << "Depositing $1000 into the Hogg Account:\n";Hoggy.Deposit(1000.00);cout << "New balance: $" << Hoggy.Balance() << endl;cout << "Withdrawing $4200 from the Pigg Account:\n";Piggy.Withdraw(4200.00);cout << "Pigg account balance: $" << Piggy.Balance() << endl;cout << "Withdrawing $4200 from the Hogg Account:\n";Hoggy.Withdraw(4200.00);Hoggy.ViewAcct();return 0;
}
// usebrass1.cpp -- testing bank account classes
// compile with brass.cpp
#include
#include"13.7_brass.h"int main(){using std::cout;using std::endl;Brass Piggy("Porcelot Pigg", 381299, 4000.00 );BrassPlus Hoggy("Horatio Hogg", 382288, 3000.00);Piggy.ViewAcct();cout << endl;cout << "Depositing $1000 into the Hogg Account:\n";Hoggy.Deposit(1000.00);cout << "New balance: $" << Hoggy.Balance() << endl;cout << "Withdrawing $4200 from the Pigg Account:\n";Piggy.Withdraw(4200.00);cout << "Pigg account balance: $" << Piggy.Balance() << endl;cout << "Withdrawing $4200 from the Hogg Account:\n";Hoggy.Withdraw(4200.00);Hoggy.ViewAcct();return 0;
}
如果数组成员指向的是 Brass 对象,则调用 Brass::ViewAcct();如果指向的是 BrassPlus 对象,则调用 BrassPlus::ViewAcct()。