类继承
- 静态联编和动态联编
将源代码中的函数调用解释为执行特定的函数代码被称为函数名联编(binding)。
在编译过程中进行联编称为静态联编(static binding),又称为早期联编(early binding)。
然而,虚函数使这项工作变得更困难。 编译器必须生成能够在程序运行时选择正确的虚方法的代码,这被称为动态联编(dynamic binding),又称为晚期联编(late binding)。
- 指针和引用类型的兼容性
在C++中,动态联编与通过指针和引用调用方法相关,从某种程度上说,这是由继承控制的。
将派生类引用或指针转换为基类引用或指针被称为向上强制转换(upcasting),这使公有继承不需要进行显式类型转换。 该规则是is-a关系的一部分。 BrassPlus对象都是Brass对象,因为它继承了Brass对象所有的数据成员和成员函数。 所以,可以对Brass对象执行的任何操作,都适用于BrassPlus对象。
相反的过程——将基类指针或引用转换为派生类指针或引用——称为向下强制转换(downcasting)。 如果不使用显式类型转换,则向下强制转换是不允许的。 原因是is-a关系通常是不可逆的。
对于使用基类引用或指针作为参数的函数调用,将进行向上转换。 下面的代码段假定每个函数都调用虚方法ViewAcct():
void fr(Brass & rb); //使用rb.ViewAcct()
void fp(Brass * pb); //使用pb->ViewAcct()
void fv(Brass b); //使用b.ViewAcct()
int main()
{
Brass b(“Billy Bee”,123432,10000.0);
BrassPlus bp(“Betty Beep”,232313,12345.0);
fr(b); //使用Brass::ViewAcct()
fr(bp); //使用BrassPlus::ViewAcct()
fp(b); //使用Brass::ViewAcct()
fp(bp); //使用BrassPlus::ViewAcct()
fv(b); //使用Brass::ViewAcct()
fv(bp); //使用Brass::ViewAcct()
…
}
按值传递导致只将BrassPlus对象的Brass部分传递给函数fv()。 但随引用和指针发生的隐式向上转换导致函数fr()和fp()分别为Brass对象和BrassPlus对象使用Brass::ViewAcct()和BrassPlus::ViewAcct()。
隐式向上强制转换使基类指针或引用可以指向基类对象或派生类对象,因此需要动态联编。 C++使用虚成员函数来满足这种需求。
- 虚成员函数和动态联编
BrassPlus ophelia; //派生类对象
Brass *bp; //基类指针
bp = & ophelia; //Brass指针指向BrassPlus对象
bp->ViewAcct(); //哪一个版本?
正如前面介绍的,如果在基类中没有将ViewAcct()声明为虚的,则bp->ViewAcct()将根据据指针类型(Brass *)调用Brass::ViewAcct()。 指针类型在编译时已知,因此编译器在编译时,可以将ViewAcct()关联到Brass::ViewAcct()。 总之,编译器对非虚方法使用静态联编。
然而,如果在基类中将ViewAcct()声明为虚的,则bp->ViewAcct()根据对象类型(BrassPlus)调用BrassPlus::ViewAcct()。
- 有关虚函数注意事项
- 在基类方法的声明中使用关键字virtual可使该方法在基类以及所有的派生类中是虚的;
- 如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不使用为引用或指针类型定义的方法。 这称为动态联编或晚期联编。 这种行为非常重要,因为这样基类指针或引用可以指向派生类对象;
- 如果定义的类将被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚的。
- 访问控制:protected
关键字protected与private相似,在类外只能用公有成员来访问protected部分中的类成员。 private和protected之间的区别只有在基类派生的类中才会表现出来。 派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。
因此,对于外部世界来说,保护成员的行为与私有成员相似;但对于派生类来说,保护成员的行为与公有成员相似。
例如,加入Brass类将balance成员声明为保护的:
class Brass {
protected:
double balance;
...
};
在这种情况下,BrassPlus类可以直接访问balance,而不需要使用Brass方法。 例如,可以这样编写BrassPlus::Withdraw()的核心:
void BrassPlus::Withdraw(double amt)
{
if (amt < 0)
cout << "Withdrawal amount must be positive;"
<< "withdrawal cancelled.\n";
else if (amt <= balance) //直接访问balance
balance -= amt;
else if (amt <= balance + maxLoan - owesBank)
{
double advance = amt - balance;
owesBank += advance * (1, 0 + rate);
cout << "Bank advance: $" << advance << endl;
cout << "Finance charge: $" << advance * rate << endl;
Deposit(advance);
balance -= amt;
}
else
cout << "Credit limit exceeded. Transaction cancelled.\n";
}
- 抽象基类 (abstract base class, ABC)
可从两个类中抽象出它们的共性,将这些特性放到一个ABC中。 然后从该ABC派生出这两个类。
C++通过使用纯虚函数提供未实现的函数。 纯虚函数声明的结尾处为=0。 当类声明中包含纯虚函数时,则不能创建该类的对象。
总之,ABC描述的是至少使用一个纯虚函数的接口,从ABC派生出的类将根据派生类的具体特征,使用常规虚函数来实现这种接口。
- 应用ABC概念
首先定义一个名为AcctABC的ABC。 这个类包含Brass和BrassPlus类共有的所有方法和数据成员,而那些在BrassPlus类和Brass类中的行为不同的方法应被声明为虚函数。 至少应有一个虚函数是纯虚函数,这样才可能使AcctABC成为抽象类。
为了帮助派生类访问基类数据,AcctABC提供了一些保护方法;派生类方法可以调用这些方法,但它们并不是派生类对象的公有接口的组成部分。 另外,AcctABC类还有两个纯虚函数,所以它确实使抽象类。
//acctabc.h -- 银行账户类
#ifndef ACCTABC_H_
#define ACCTABC_H_
#include<iostream>
#include<string>
//抽象基类
class AcctABC
{
private:
std::string fullName;
long acctNum;
double balance;
protected:
struct Formatting
{
std::ios_base::fmtflags flag;
std::streamsize pr;
};
const std::string & FullName() const { return fullName; }
long AcctNum() const { return acctNum; }
Formatting SetFormat() const;
void Restore(Formatting & f) const;
public:
AcctABC(const std::string & s = "Nullbody", long an = -1,
double bal = 0.0);
void Deposit(double amt);
virtual void Withdraw(double amt) = 0; //纯虚函数
double Balance() const { return balance; };
virtual void ViewAcct() const = 0; //纯虚函数
virtual ~AcctABC(){}
};
//Brass Account类
class Brass :public AcctABC
{
public:
Brass(const std::string &s ="Nullbody",long an=-1,
double bal=0.0):AcctABC(s,an,bal){}
virtual void Withdraw(double amt);
virtual void ViewAcct() const;
virtual ~Brass(){}
};
//Brass Plus Account类
class BrassPlus :public AcctABC
{
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.10);
BrassPlus(const Brass & ba, double ml = 500, double r = 0.1);
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 // !ACCTABC_H_
//acctabc.cpp -- bank account class 方法
#include<iostream>
#include"acctabc.h"
using std::cout;
using std::ios_base;
using std::endl;
using std::string;
//抽象基类
AcctABC::AcctABC(const string &s, long an, double bal)
{
fullName = s;
acctNum = an;
balance = bal;
}
void AcctABC::Deposit(double amt)
{
if (amt < 0)
cout << "Negative deposit not allowed;"
<< "deposit is cancelled.\n";
else
balance += amt;
}
void AcctABC::Withdraw(double amt)
{
balance -= amt;
}
//控制格式的保护方法
AcctABC::Formatting AcctABC::SetFormat() const
{
//设置为###.##格式
Formatting f;
f.flag = cout.setf(ios_base::fixed, ios_base::floatfield);
f.pr = cout.precision(2);
return f;
}
void AcctABC::Restore(Formatting &f)const
{
cout.setf(ios_base::fixed, ios_base::floatfield);
cout.precision(f.pr);
}
//Brass方法
void Brass::Withdraw(double amt)
{
if (amt < 0)
cout << "WIthdrawal amount must be positive;"
<< "withdrawal cancelled.\n";
else if (amt <= Balance())
AcctABC::Withdraw(amt);
else
cout << "Withdrawal amount of $" << amt
<< " exceeds your balance.\n"
<< "Withdrawal cancelled.\n";
}
void Brass::ViewAcct() const
{
Formatting f = SetFormat();
cout << "Brass Client: " << FullName() << endl;
cout << "Account Number: " << AcctNum() << endl;
cout << "Balance: $" << Balance() << endl;
Restore(f);
}
//BrassPlus方法
BrassPlus::BrassPlus(const string &s, long an, double bal,
double ml, double r) :AcctABC(s, an, bal)
{
maxLoan = ml;
owesBank = 0.0;
rate = r;
}
BrassPlus::BrassPlus(const Brass &ba, double ml, double r)
:AcctABC(ba) //使用隐式复制构造函数
{
maxLoan = ml;
owesBank = 0.0;
rate = r;
}
void BrassPlus::ViewAcct() const
{
Formatting f = SetFormat();
cout << "BrassPlus Client: " << FullName() << endl;
cout << "Account Number: " << AcctNum() << endl;
cout << "Balance: $" << Balance() << endl;
cout << "Maxium Loan: $" << maxLoan << endl;
cout << "Owed to bank: $" << owesBank << endl;
cout.precision(3);
cout << "Loan Rate: " << 100 * rate << "%\n";
Restore(f);
}
void BrassPlus::Withdraw(double amt)
{
Formatting f = SetFormat();
double bal = Balance();
if (amt <= bal)
AcctABC::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);
AcctABC::Withdraw(amt);
}
else
cout << "Credit limit exceeded. Transaction cancelled.\n";
Restore(f);
//usebrass3.cpp -- 使用抽象基类的多态例子
#include<iostream>
#include<string>
#include"acctabc.h"
const int CLIENTS = 4;
int main()
{
using std::cin;
using std::cout;
using std::endl;
AcctABC *p_clients[CLIENTS];
std::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 Account: ";
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";
system("pause");
return 0;
}
程序运行结果:
Enter client's name: Harry Fishsong
Enter client's account number: 112233
Enter opening balance: $1500
Enter 1 for Brass Account or 2 for BrassPlus Account: 1
Enter client's name: Dinah Otternoe
Enter client's account number: 121213
Enter opening balance: $1800
Enter 1 for Brass Account or 2 for BrassPlus Account: 2
Enter the overdraft limit: $350
Enter tge interest rate as a decimal fraction: 0.12
Enter client's name: Brenda Birdherd
Enter client's account number: 212118
Enter opening balance: $5200
Enter 1 for Brass Account or 2 for BrassPlus Account: 2
Enter the overdraft limit: $800
Enter tge interest rate as a decimal fraction: 0.10
Enter client's name: Tim Turtletop
Enter client's account number: 233255
Enter opening balance: $688
Enter 1 for Brass Account or 2 for BrassPlus Account: 1
Brass Client: Harry Fishsong
Account Number: 112233
Balance: $1500.00
BrassPlus Client: Dinah Otternoe
Account Number: 121213
Balance: $1800.00
Maxium Loan: $350.00
Owed to bank: $0.00
Loan Rate: 12.000%
BrassPlus Client: Brenda Birdherd
Account Number: 212118
Balance: $5200.00
Maxium Loan: $800.00
Owed to bank: $0.00
Loan Rate: 10.000%
Brass Client: Tim Turtletop
Account Number: 233255
Balance: $688.00
Done.
在处理继承的问题上,ABC方法更具系统性、更规范。 可以将ABC看作是一种必须实施的接口。 ABC要求具体派生类覆盖其纯虚函数——迫使派生类遵循ABC设置的接口规则。