多态
多态:具有多种形态,同一个方法在派生类和基类的行为是不同的,随上下文而异。
实现多态公有继承的两种机制:
1)在派生类中重新定义基类的方法。
2)使用虚方法。
虚方法
虚方法:带关键字virtual
在基类中将派生类会重新定义的方法声明为虚方法。
使用了virtual,程序将根据引用或指针指向的对象的类型来选择方法
虚析构函数
基类要声明一个虚析构函数,为了确保释放派生类对象时,按正确的顺序调用析构函数
实例
银行开发两个类,Brass Account:用于表示基本支票账户;Brass Plus:用于表示支票账户,它添加了透支保护特性。如果用户签出一张超出其存款余额的支票——但是超出的数额并不大,银行将支付这张支票,对超出的部分收取额外的费用,并追加罚款,可以根据要保存的数据以及允许执行的操作来确定这两种账户的特征。
Brass Account支票账户的信息:
1)客户姓名
2)账号
3)当前结余
可执行的操作
1)创建账户
2)存款
3)取款
4)显示账户信息
银行希望Brass Plus支票账户包含Brass Account所有信息及以下信息:
1)透支上限
2)透支贷款利率
3)当前的透支总额
不需要新增操作,有两种操作的实现不同:
1)对于取款操作,必须考虑透支保护
2)显示操作必须显示Brass Plus账户的其他信息
开发Brass类和BrassPlus类
客户向银行提供如下信息:
1)Brass Plus账户限制了客户的透支款额,默认500元,但有些客户限额不同
2)银行可以修改客户的透支额度
3)Brass Plus账户对贷款收取利息,默认为11.125%。但有些客户利率不同
4)银行可以修改客户的利率
5)账户记录客户所欠银行的金额(加利息)。用户不能通过常规存款或从其他账户转账的方式偿付,而必须以现金的方式交给特定的银行人员。如有必要,银行可以找到该客户,欠款偿还后,欠款金额为零。
brass.h
新的类需要构造函数,而且构造函数应提供账户信息,设置透支上限和利率。另外,重新设置透支限额、利率和当前欠款的方法。要添加到Brass类中的,将在BrassPlus类声明中声明。
//brass.h
#ifndef BRASS_H
#define BRASS_H
#include <string>
using namespace std;
class Brass
{
private:
string fullName; //客户姓名
long acctNum; //账号
double balance; //当前结余
public:
Brass(const 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 string &s = "Nullbody", long an = -1, double bal = 0.0, double m1 = 500, double r = 0.11125);
BrassPlus(const Brass &ba, double m1 = 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
1)BrassPlus类在Brass类的基础上添加了3个私有数据成员和3个公有成员函数。
2)Brass类和BrassPlus类都声明了ViewAcct()和Withdraw()方法,但BrassPlus对象和Brass对象的这些方法的行为是不同的。
3)Brass类在声明ViewAcct()和Withdraw()时使用了新关键字virtual。这些方法被称为虚方法。
4)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(); //使用Brase::ViewAcct()
dot.ViewAcct(); //使用BrasePlus::ViewAcct()
Withdraw()也有2个版本,一个供Brass对象使用,另一个供BrassPlus对象使用。对于在两个类中行为相同的方法(Deposit()和Balance()),则只在基类中声明。
如果方法是通过引用或者指针而不是对象调用的,它将确定使用哪一种方法,如果没有使用关键字virtual,程序将根据引用类型或者指针类型选择方法。
如果使用了virtual,程序将根据引用或指针指向的对象的类型来选择方法。如果ViewAcct()不是虚的,则程序的行为如下:
//没有带有virtual的VieAcct()方法
//根据所指的类型来选择方法
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(); //使用Brase::ViewAcct()
b2_ref.ViewAcct(); //使用Brase::ViewAcct()
引用变量的类型为Brass,所以选择了Brass::ViewAccount()。使用Brass指针代替引用时,行为将与此类似。
如果ViewAcct()是虚的:
//带有virtual的VieAcct()方法
//根据对象的类型来选择方法
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(); //使用Brase::ViewAcct()
b2_ref.ViewAcct(); //使用BrasePlus::ViewAcct()
这里两个引用的类型都是Brass,但b2_ref引用的是一个BrassPlus对象,所以使用的是BrassPlus::ViewAcct()。使用Brass指针代替引用时,类似。
因此,经常在基类中将派生类会重新定义的方法声明为虚方法。方法在基类中被声明为虚的后,它在派生类中将自动成为虚方法。然而,在派生类声明中使用关键字virtual来指出哪些函数是虚函数。
基类声明了一个虚析构函数。为了确保释放派生对象时,按正确的顺序调用析构函数。
注意:如果要在派生类中重新定义基类的方法,通常应将基类方法声明为虚的。这样,程序将根据对象类型而不是引用或者指针的类型来选择方法版本。为基类声明一个虚析构函数也是一种惯例。
brass.cpp
virtual只用于类声明的方法原型中。
//brass.cpp
#include <iostream>
#include "brass.h"
using namespace std;
//格式化
typedef std::ios_base::fmtflags format;
typedef std::streamsize precis;
format setFormat();
void restore(format f, precis p);
//Brass类方法
Brass::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)
{
format 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
{
format initialState = setFormat();
precis prec = cout.precision(2);
cout << "Client: " << fullName << endl;
cout << "Account Number: " << acctNum << endl;
cout << "Balance: $" << balance << endl;
restore(initialState, prec);
}
//BrassPlus类方法
BrassPlus::BrassPlus(const string &s, long an, double bal, double m1, double r) :Brass(s, an, bal)
{
maxLoan = m1;
owesBank = 0.0;
rate = r;
}
BrassPlus::BrassPlus(const Brass &ba, double m1, double r) :Brass(ba)
{
//使用隐式复制构造函数
maxLoan = m1;
owesBank = 0.0;
rate = r;
}
//重定义ViewAcct()
void BrassPlus::ViewAcct() const
{
format initialState = setFormat();
precis prec = cout.precision(2);
Brass::ViewAcct();
cout << "Maximum loan: $" << maxLoan << endl;
cout << "Owed to bank: $" << owesBank << endl;
cout.precision(3); //###.###
cout << "Loan Rate: " << 100 * rate << "%\n";
restore(initialState, prec);
}
//重定义Withdraw()
void BrassPlus::Withdraw(double amt)
{
format 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()
{
//###.##
return 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);
}
派生类不能直接访问基类的私有数据,而必须使用基类的公有方法才能访问这些数据。
派生类构造函数在初始化基类私有数据时,采用成员初始化列表。
BrassPlus::BrassPlus(const string &s, long an, double bal, double m1, double r):Brass(s, an, bal)
{
maxLoan = m1;
owesBank = 0.0;
rate = r;
}
BrassPlus::BrassPlus(const Brass &ba, double m1, double r):Brass(ba)
{
//使用隐式复制构造函数
maxLoan = m1;
owesBank = 0.0;
rate = r;
}
这几个构造函数都使用成员初始化列表语法,将基类信息传递给基类构造函数,然后使用构造函数体初始化BrassPlus类新增的数据项。
非构造函数不能使用成员初始化列表语法,但派生类方法可以调用公有的基类方法。
void BrassPlus::ViewAcct() const
{
Brass::ViewAcct();
cout << "Maximum loan: $" << maxLoan << endl;
cout << "Owed to bank: $" << owesBank << endl;
cout << "Loan Rate: " << 100 * rate << "%\n";
}
BrassPlus::ViewAcct()显示新增的BrassPlus数据成员,并调用基类方法Brass::ViewAcct()来显示基类数据成员。在派生类方法中,标准技术是使用作用域解析运算符类调用基类方法。
代码必须使用作用域解析运算符。
如果代码没有使用作用域解析运算符,编译器认为ViewAcct()是BrassPlus::ViewAcct(),这将创建一个不会终止的递归函数。
//错误的重定义
void BrassPlus::ViewAcct() const
{
...
Brass::ViewAcct(); //递归调用
...
}
BrassPlus::Withdraw()方法。如果客户提取的金额超过结余,该方法将安排贷款。它可以使用Brass::Withdraw()来访问balance成员,但如果取款金额超过了结余,Brass::Withdraw()将发出一个错误消息。这种实现使用Deposit()方法进行放贷,然后在得到了足够的结余后调用Brass::Withdraw,从而避免了错误消息。
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()将浮点值的输出模式设置为定点,即包含两位小数。设置模式后,输出的模式将保持不变,因此该方法将格式模式重置为调用前的状态。为了避免代码重复,该程序将设置格式的代码放在辅助函数中。
//格式化
typedef 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);
}
usebrass1.cpp
使用一个Brass对象和一个BrassPlus对象测试类定义。通过对象调用,没有使用虚方法特性。
//usebrass1.cpp
#include <iostream>
#include "brass.h"
using namespace std;
int main()
{
Brass Piggy("Porcelot Pigg", 381299, 4000.00);
BrassPlus Hoggy("Horatio Hogg", 382288, 3000.00);
Piggy.ViewAcct();
cout << endl;
Hoggy.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;
}
usebrass2.cpp
虚函数,多态性
假设同时管理Brass和BrassPlus账户,如果能使用同一个数组来保存Brass和BrassPlus对象,将很有帮助,但这是不可能的。
数组中所有元素的类型必须相同,而Brass和BrassPlus是不同的类型。然而,可以创建指向Brass的指针数组。这样,每个元素的类型都相同,但由于使用的是公有继承模型,因此Brass指针既可以指向Brass对象,也可以指向BrassPlus对象。
因此,可以使用一个数组来表示多钟类型的对象。这就是多态性。
//usebrass2.cpp
#include <iostream>
#include <string>
#include "brass.h"
const int CLIENTS = 4;
int main()
{
Brass *p_clients[CLIENTS];
string temp;
long tempnum;
double tempbal;
char kind;
for(int i = 0; i < CLIENTS; i++)
{
cout << "Enter client's name: ";
getline(cin, temp);
cout << "Enter client's account number: ";
cin >> tempnum;
cout << "Enter opening balance: $";
cin >> tempbal;
cout << "Enter 1 for Brass Account or "
<< "2 for BrassPlus Acount: ";
while(cin >> kind && (kind != '1' && kind != '2'))
cout << "Enter either 1 or 2: ";
if(kind == '1')
p_clients[i] = new Brass(temp, tempnum, tempbal);
else
{
double tmax, trate;
cout << "Enter the overdraft limit: $";
cin >> tmax;
cout << "Enter the interest rate "
<< "as a decimal fraction: ";
cin >> trate;
p_clients[i] = new BrassPlus(temp, tempnum, tempbal, tmax, trate);
}
while(cin.get() != '\n')
continue;
}
cout << endl;
for(int i = 0; i < CLIENTS; i++)
{
p_clients[i]->ViewAcct();
cout << endl;
}
for(int i = 0; i < CLIENTS; i++)
{
delete p_clients[i];
}
cout << "Done/\n";
return 0;
}
如果数组成员指向的是Brass对象,则调用Brass::ViewAcct();如果指向的是BrassPlus对象,则调用BrassPlus::ViewAcct()。如果Brass::ViewAcct()被声明为虚的,则在任何情况下都将调用Brass::ViewAcct()。
多态性:
for (int i = 0; i < CLIENTS; i++)
{
p_clients[i]->ViewAcct();
cout << endl;
}
为何需要虚析构函数
使用delete释放由new分配的对象的代码说明了为何基类包含一个虚析构函数。
如果析构函数不是虚的,则将只调用对应指针类型的析构函数。只有Brass的析构函数被调用,即使指针指向的是一个BrassPlus对象。
如果析构函数是虚的,将调用相应对象类型的析构函数。如果指针指向的是BrassPlus对象,将调用BrassPlus的析构函数,然后自动调用基类的析构函数。
使用虚析构函数可以确保正确的析构函数序列被调用。
如果BrassPlus包含一个执行某些操作的析构函数,则Brass必须有一个虚析构函数,即使该析构函数不执行任何操作。