C++ day22 继承(二)基类指针数组通过虚方法实现智能的多态
继承一共有三种:
- 公有继承
- 私有继承
- 保护继承
文章目录
公有继承
基类和派生类的关系
is-a(用公有继承表示“是一种”的关系)
是一种(比如香蕉是一种水果,是水果大类的一个特例,所以水果的所有属性和方法都适用于香蕉,所以很适合使用公有继承来实现)
has-a
有一种(比如午餐中有火腿,但是不要从Lunch类派生出Meat类,而是应该把Meat类的对象作为Lunch类的数据成员,即has-a关系)
uses-a
使用一种(比如computer类使用printer类,但是从Computer类派生Printer类也是不合理的)
is-like-a
像一种(比如律师像鲨鱼,但是律师和鲨鱼是完全不一样的,这是人类习惯使用的一种明喻,所以不能从Shark类派生出Lawyer类)
is-implemented-as-a
被实现为一种(比如把栈实现为数组,但是数组的很多属性和栈不一样,比如数组有索引,所以从Array类派生出Stack类不合理,应该让Stack类包含一个私有的Array类的数据成员)
这些关系中is-a用公有继承的方式实现,其他的关系一般不使用公有继承实现,虽然也可以,但是不太合理,容易导致编程问题。
多态公有继承
前面说的简单的继承是直接使用基类方法,不改写基类方法。多态继承则要多态,表现在:
- 改写基类的同类方法,这里要用到虚函数。下方示例。
- 基类方法有重载,这时候派生类如果要重写其中一个函数,就要重写基类的所有重载函数,否则会隐藏没被重写的那些。这一点在下一篇博文末尾有示例。
- 当用类对象的引用或指针调用虚方法时,编译器动态联编,根据对象类型判断到底使用该方法的基类版本还是派生类版本。实现多态。这一点很高级。真的很智能。见下方示例拓展。
- 还有什么多态呢?一时间想不起来
示例
//Brass.h
#ifndef BRASS_H_
#define BRASS_H_
#include <string>
class Brass{
private:
std::string fullname;
long account;//账户
double balance;//当前结余
public:
Brass(const std::string & fn = "None None", long ac = -1, double ba = 0);
virtual ~Brass(){}//虚析构函数
void Deposit(double amt);
virtual void Withdraw(double amt);
virtual void ViewAcct() const;
double Balance() const {return balance;}
};
#endif
//Brass.cpp
#include <iostream>
#include "Brass.h"
typedef std::ios_base::fmtflags format;//fmtflags,格式标志
typedef std::streamsize precis;//流的规模大小,即精度,位数
format setFormat();
void restore(format f, precis p);
Brass::Brass(const std::string & fn, long ac, double ba)
{
//std::cout << "call Brass::Brass(const std::string & fn, long ac, double ba)\n";
fullname = fn;
account = ac;
balance = ba;
}
//Brass:Brass(std::string & fn = "None None", long ac = -1, double ba = 0.0):fullname(fn),account(ac),balance(ba){}
void Brass::Deposit(double amt)
{
if (amt < 0)
std::cout << "Failure! Negative deposit not allowed!\n";
else
balance += amt;
}
void Brass::Withdraw(double amt)
{
format initialState = setFormat();
precis prec = std::cout.precision(2);
if (amt < 0)
std::cout << "Withdraw amount must be positive!\n"
<< "Withdraw cancelled!\n";
else if (amt <= balance)
balance -= amt;
else
std::cout << "Withdraw amount of $" << amt
<< " exceeds your balance.\n"
<< "Withdraw cancelled!\n";
restore(initialState, prec);
}
void Brass::ViewAcct() const
{
format initialState = setFormat();
precis prec = std::cout.precision(2);
std::cout << "Client: " << fullname << '\n'
<< "Account number: " << account << '\n'
<< "Balance: $" << balance << '\n';
restore(initialState, prec);
}
//自定义格式辅助函数
format setFormat()
{
//set up ###.## format
//设置为定点表示法,返回当前格式的标记
return std::cout.setf(std::ios_base::fixed, std::ios_base::floatfield);
}
void restore(format f, precis p)
{
//重置输出格式和精度
//恢复格式
std::cout.setf(f, std::ios_base::floatfield);
//恢复精度
std::cout.precision(p);
}
//BrassPlus.h
#ifndef BRASSPLUS_H_
#define BRASSPLUS_H_
#include <string>
#include "Brass.h"
class BrassPlus: public Brass //is-a关系,公有继承
{
private:
double maxLoan;//透支上限
double rate;//透支利率
double owesBank;//透支总额,即透支数额加利息
public:
BrassPlus(const Brass & ba, double max = 500.0, double r = 0.11125, double owe = 0.0):Brass(ba), maxLoan(max),rate(r), owesBank(owe){}
//把fn, ac, ba写在前面,和基类Brass(std::string fn = "None None", long ac = 0, double ba = 0);一样,防止出问题
BrassPlus(std::string fn = "None None", long ac = -1, double ba = 0, double max = 500.0, double r = 0.11125, double owe = 0.0):Brass(fn, ac, ba), maxLoan(max),rate(r), owesBank(owe){}
~BrassPlus(){}
virtual void ViewAcct() const;//虚方法
virtual void Withdraw(double amt);//虚方法
void ResetMaxLoan(double max){maxLoan = max;}
void ResetRate(double r){rate = r;}
void ResetOwesBank(){owesBank = 0;}//默认还款一次还清
double OwesBank() const {return owesBank;}
};
#endif
//BrassPlus.cpp
#include <iostream>
#include "BrassPlus.h"
typedef std::ios_base::fmtflags format;//fmtflags,格式标志
typedef std::streamsize precis;//流的规模大小,即精度,位数
format setFormat();
void restore(format f, precis p);
void BrassPlus::ViewAcct() const//虚方法
{
format initialState = setFormat();
precis prec = std::cout.precision(2);
/*
std::cout << "Client: " << fullname << '\n'
<< "Account number: " << account << '\n'
<< "Balance: $" << balance << '\n';
*/
Brass::ViewAcct();//直接调用基类的viewacct方法即可,重用代码
std::cout << "Maximum loan: $" << maxLoan << '\n'
<< "Owed to bank: $" << owesBank << '\n';
std::cout.precision(3);
std::cout << "Loan rate: " << 100 * rate << "%\n";
restore(initialState, prec);
}
void BrassPlus::Withdraw(double amt)//虚方法
{
format initialState = setFormat();
precis prec = std::cout.precision(2);
double bal = Balance();
if (amt < 0)
std::cout << "Withdraw amount must be positive!\n"
<< "Withdraw cancelled!\n";
else if (amt <= bal)//不可写amt <= balance,因为不能直接访问基类数据成员
Brass::Withdraw(amt);//不可写balance -= amt;
else if(amt - bal + owesBank <= maxLoan)
{
double advance = amt - bal;
owesBank += advance * (1.0 + rate);//rate是BrassPlus类的私有成员
std::cout << "Bank advance: $" << advance << '\n';
std::cout << "Finance charge: $" << advance * rate << '\n';
//分两步实现扣除账户全部余额(由于基类不允许取款金额超出余额,所以只能先存再取)
Deposit(advance);//放贷
Brass::Withdraw(amt);
}
else
std::cout << "Credit limit exceeded. Transaction cancelled.\n";
restore(initialState, prec);
}
//main.cpp
#include <iostream>
#include "Brass.h"
#include "BrassPlus.h"
int main()
{
Brass a;
Brass Piggy("Porcelot Pigg", 381299, 4000.00);
BrassPlus Hoggy("Hoartio Hogg", 382288, 3000.00);
Piggy.ViewAcct();
std::cout << std::endl;
Hoggy.ViewAcct();
std::cout << std::endl;
Hoggy.Deposit(1000.0);
Hoggy.ViewAcct();
std::cout << std::endl;
Piggy.Withdraw(4200.0);
Piggy.ViewAcct();
std::cout << std::endl;
Hoggy.Withdraw(4200.0);
Hoggy.ViewAcct();
std::cout << std::endl;
return 0;
}
输出
Client: Porcelot Pigg
Account number: 381299
Balance: $4000.00
Client: Hoartio Hogg
Account number: 382288
Balance: $3000.00
Maximum loan: $500.00
Owed to bank: $0.00
Loan rate: 11.125%
Client: Hoartio Hogg
Account number: 382288
Balance: $4000.00
Maximum loan: $500.00
Owed to bank: $0.00
Loan rate: 11.125%
Withdraw amount of $4200.00 exceeds your balance.
Withdraw cancelled!
Client: Porcelot Pigg
Account number: 381299
Balance: $4000.00
Bank advance: $200.00
Finance charge: $22.25
Client: Hoartio Hogg
Account number: 382288
Balance: $0.00
Maximum loan: $500.00
Owed to bank: $222.25
Loan rate: 11.125%
出现的错误
- 把字符串常量(右值)赋给string &, 报错,既然这里默认参数是字符串右值,那这里就只能用string类型,而不能用string &
Brass(std::string & fn = "None None", long ac = -1, double ba = 0);
如果把string & 改为const string &,则也可以,我用了这个方法
Brass(const std::string & fn = "None None", long ac = -1, double ba = 0);
- 注意派生类构造函数的参数顺序,要把基类的参数放在前面,这样如果调用派生类构造函数但只传入基类所需要的的参数,则也不会报错。
比如在下面示例中,应该把fn, ac, ba写在前面,和基类Brass(std::string fn = “None None”, long ac = 0, double ba = 0);一样,否则主程序中使用BrassPlus Hoggy(“Hoartio Hogg”, 382288, 3000.00);创建派生类对象就会出错,因为找不到匹配原型。
BrassPlus(std::string fn = "None None", long ac = -1, double ba = 0, double max = 500.0, double r = 0.11125, double owe = 0.0):Brass(fn, ac, ba), maxLoan(max),rate(r), owesBank(owe){}
- 多文件程序的链接问题:我的四个文件竟然没有添加在项目中·········于是一直报这种错误:
这些方法不能用,未定义引用等等
我到处找问题,把代码又好好过目一遍,确实没问题,最后实在没辙了着急了,开始怀疑到是不是没加到项目里,终于解决了
对文件右键,看到这个选项,说明根本不在项目里,于是编译就没有编译它,或者说链接没有把这个文件的机器码和程序其他部分链接在一起,所以主程序中调用这个文件中的方法出现undefine reference错误。说白了本质上,是没有把整个程序的所有翻译单元链接到一起。
对文件名右键,出现这个remove选项就表示该文件被添加到项目里的,会被链接到一起
-
使用ios_base但忘记导入iostream头文件,即之前没写第一个红框,报错
-
忘记在派生类BrassPlus的构造函数中给它的owesBank私有成员赋初始值。因为我当时想着欠银行多少钱要后面计算才知道,一时忘记了创建派生类对象时应该将其初始化为0,毕竟一开户总不能欠钱。由于我没初始化,后面主程序执行结果:
//正确版
BrassPlus(const Brass & ba, double max = 500.0, double r = 0.11125, double owe = 0.0):Brass(ba), maxLoan(max),rate(r), owesBank(owe){}
BrassPlus(std::string fn = "None None", long ac = -1, double ba = 0, double max = 500.0, double r = 0.11125, double owe = 0.0):Brass(fn, ac, ba), maxLoan(max),rate(r), owesBank(owe){}
//错误版
BrassPlus(const Brass & ba, double max = 500.0, double r = 0.11125):Brass(ba), maxLoan(max),rate(r){}
BrassPlus(std::string fn = "None None", long ac = -1, double ba = 0, double max = 500.0, double r = 0.11125):Brass(fn, ac, ba), maxLoan(max),rate(r){}
后果,欠银行的钱成了那个8字节内存块的当前值,未知,对后面的计算则造成很大的错误
Client: Porcelot Pigg
Account number: 381299
Balance: $4000.00
Client: Hoartio Hogg
Account number: 382288
Balance: $3000.00
Maximum loan: $500.00
Owed to bank: $-29550982773929114482976296534016.00
Loan rate: 11.125%
Client: Hoartio Hogg
Account number: 382288
Balance: $4000.00
Maximum loan: $500.00
Owed to bank: $-29550982773929114482976296534016.00
Loan rate: 11.125%
Withdraw amount of $4200.00 exceeds your balance.
Withdraw cancelled!
Client: Porcelot Pigg
Account number: 381299
Balance: $4000.00
Bank advance: $200.00
Finance charge: $22.25
Client: Hoartio Hogg
Account number: 382288
Balance: $0.00
Maximum loan: $500.00
Owed to bank: $-29550982773929114482976296534016.00
Loan rate: 11.125%
- 函数和变量命名不够贴切,短小
这个不算是错误,但是还是要修炼
我之前对透支上限的变量名是overdraftLimit, 利率是overdraftInterest, 欠款金额是overdraftAmount
很长,不方便,也不好看
例程用的是maxLoan, rate, owesBank,就很短小
返回欠款金额的方法,以及显示账户的方法,我的命名是 overAmount,showAccount
例程用的是OwesBank, ViewAcct
感觉还是例程的更好,更简短
另外我发现,例程喜欢把变量名的首字母小写,后面每个单词的首字母大写;方法名的所有首字母都大写。
这样的好处是,如果类的一个私有成员是balance,那么用于返回这个私有数据成员的值的公有方法的名字就是Balance(),这样一看就知道这个方法干嘛的,也方便使用。
- 书写不规范,格式不好看
这个不算是错误,但是我通过这次写这个银行账户的类,发现我的ViewAcct()方法,一是没有设置输出格式,客户查询金额会格式乱七八糟;二是在输出多条消息时,我没有注意让代码工整好看,有多长写多长。
我的
std::cout << "Withdraw amount of $" << amt << " exceeds your balance.\n" << "Withdraw cancelled!\n";
例程
std::cout << "Withdraw amount of $" << amt
<< " exceeds your balance.\n"
<< "Withdraw cancelled!\n";
- 没有时刻做到代码重用
void BrassPlus::ViewAcct() const//虚方法
{
format initialState = setFormat();
precis prec = std::cout.precision(2);
/*
//没重用代码,写了三行重复代码,这很不好
std::cout << "Client: " << fullname << '\n'
<< "Account number: " << account << '\n'
<< "Balance: $" << balance << '\n';
*/
Brass::ViewAcct();//直接调用基类的viewacct方法即可,重用代码
std::cout << "Maximum loan: $" << maxLoan << '\n'
<< "Owed to bank: $" << owesBank << '\n';
std::cout.precision(3);
std::cout << "Loan rate: " << 100 * rate << "%\n";
restore(initialState, prec);
}
示例拓展:基类指针数组:多态,虚方法(好高级)
建立一个Brass指针数组,每个元素都是一个Brass指针,可以指向Brass对象,也可以指向BrassPlus类对象
在用这些指针调用方法时,如果是虚方法,则方法会根据指针指向的对象的类型,而不是指针类型来判断到底调用Brass类的方法还是BrassPlus类的方法。
这样子,在遍历数组元素,依次调用数组每一个元素时,代码都一样,却会调用不同的方法,从而实现了多态。很牛逼
//基类
virtual void ViewAcct() const;
void Brass::ViewAcct() const
{
format initialState = setFormat();
precis prec = std::cout.precision(2);
std::cout << "Client: " << fullname << '\n'
<< "Account number: " << account << '\n'
<< "Balance: $" << balance << '\n';
restore(initialState, prec);
}
//派生类
virtual void ViewAcct() const;
void BrassPlus::ViewAcct() const
{
format initialState = setFormat();
precis prec = std::cout.precision(2);
Brass::ViewAcct();//直接调用基类的viewacct方法即可,重用代码
std::cout << "Maximum loan: $" << maxLoan << '\n'
<< "Owed to bank: $" << owesBank << '\n';
std::cout.precision(3);
std::cout << "Loan rate: " << 100 * rate << "%\n";
restore(initialState, prec);
}
//main.cpp
#include <iostream>
#include "Brass.h"
#include "BrassPlus.h"
const int NUM = 4;
void eatline();
int main()
{
using std::cout;
using std::cin;
Brass * p_clients[NUM];//基类指针数组
std::string tempName;
long tempAcct;
double openBal;
char kind;//会员种类,普通会员或plus会员
int i;
for (i = 0; i < NUM; ++i)
{
cout << "Enter client's name: ";
getline(cin, tempName);
//eatline();
cout << "Enter client's accout number: ";
cin >> tempAcct;
eatline();
cout << "Enter opening balance: $";
cin >> openBal;
eatline();
cout << "Enter 1 for Brass Account or 2 for BrassPlus Account:";
while ((kind = cin.get()) != '1' && (kind != '2'))
continue;
eatline();//否则换行符会被读取为下一个客户的名字
if (kind == '1')
{
p_clients[i] = new Brass(tempName, tempAcct, openBal);
cout << '\n';
}
else if (kind == '2')
{
cout << "Enter the overdraft limit: ";
double tempLimit;
cin >> tempLimit;
eatline();
cout << "Enter the interest rate: ";
double tempRate;
cin >> tempRate;
eatline();
p_clients[i] = new BrassPlus(tempName, tempAcct, openBal, tempLimit, tempRate);
cout << '\n';
}
}
//显示四位顾客的信息
for (i = 0; i < NUM; ++i)
{
cout << '\n';
p_clients[i]->ViewAcct();//展示多态特性的核心代码
delete p_clients[i];//总是记不住delete
}
return 0;
}
void eatline()
{
while (std::cin.get() != '\n')
;
}
输出
Enter client's name: Mary Hellen
Enter client's accout number: 123456
Enter opening balance: $4815
Enter 1 for Brass Account or 2 for BrassPlus Account:1
Enter client's name: Julie Rechard
Enter client's accout number: 465565
Enter opening balance: $1235
Enter 1 for Brass Account or 2 for BrassPlus Account:2
Enter the overdraft limit: 400
Enter the interest rate: 0.15
Enter client's name: Debbie Gallager
Enter client's accout number: 128796
Enter opening balance: $4865
Enter 1 for Brass Account or 2 for BrassPlus Account:2
Enter the overdraft limit: 89745
Enter the interest rate: 0.25
Enter client's name: Steeve Green
Enter client's accout number: 782169
Enter opening balance: $458664
Enter 1 for Brass Account or 2 for BrassPlus Account:1
Client: Mary Hellen
Account number: 123456
Balance: $4815.00
Client: Julie Rechard
Account number: 465565
Balance: $1235.00
Maximum loan: $400.00
Owed to bank: $0.00
Loan rate: 15.000%
Client: Debbie Gallager
Account number: 128796
Balance: $4865.00
Maximum loan: $89745.00
Owed to bank: $0.00
Loan rate: 25.000%
Client: Steeve Green
Account number: 782169
Balance: $458664.00
如果把Brass类的ViewAcct方法的virtual关键字(这其实就默认BrassPlus类的ViewAcct方法的virtual关键字也去掉了,就算你不去掉(virtual void ViewAcct() const;)也相当于去掉了,因为只有基类的方法前面的virtual关键字才说了算,起作用)
那么不管对象是什么类型,都只会调用基类的ViewAcct方法
Enter client's name: gf
Enter client's accout number: 4564
Enter opening balance: $465
Enter 1 for Brass Account or 2 for BrassPlus Account:1
Enter client's name: fadfa
Enter client's accout number: 4646
Enter opening balance: $464
Enter 1 for Brass Account or 2 for BrassPlus Account:2
Enter the overdraft limit: 565456
Enter the interest rate: 0.25
Enter client's name: fasdfa
Enter client's accout number: 6456
Enter opening balance: $4564
Enter 1 for Brass Account or 2 for BrassPlus Account:2
Enter the overdraft limit: 456
Enter the interest rate: 0.1
Enter client's name: fasfa
Enter client's accout number: 465465
Enter opening balance: $464
Enter 1 for Brass Account or 2 for BrassPlus Account:1
Client: gf
Account number: 4564
Balance: $465.00
Client: fadfa
Account number: 4646
Balance: $464.00
Client: fasdfa
Account number: 6456
Balance: $4564.00
Client: fasfa
Account number: 465465
Balance: $464.00
本示例知识点总结
-
派生类不能直接访问基类数据,要用基类公有方法间接访问
-
虚成员函数:把基类中会在派生类中重新定义的方法声明为虚函数
在基类的方法原型前面加个virtual关键字即可,和友元函数关键字friend一样,只出现在原型中,无需在定义中再写
基类中被声明为虚方法的方法,在派生类中自动成为虚方法,但是最好还是把带virtual的原型再在派生类的类声明中写一遍,这是个好习惯(但是如果基类某方法不是虚方法,那么即使派生类的该方法的原型前面有virtual关键字,也跟没有一样)。
- 虚析构函数:确保释放派生类对象时,按正确顺序调用析构函数
如果基类的析构函数声明为虚函数,那么派生类BrassPlus类对象会调用自己的析构函数,在其中会自动调用基类的析构函数,下面用示例说明,让两个析构函数打印一条消息
virtual ~Brass(){std::cout << "In virtual ~Brass()\n";}//虚析构函数
~BrassPlus(){std::cout << "In ~BrassPlus()\n";}
Enter client's name: hfjka
Enter client's accout number: 456
Enter opening balance: $456
Enter 1 for Brass Account or 2 for BrassPlus Account:1
Enter client's name: dasf
Enter client's accout number: 465456
Enter opening balance: $456456
Enter 1 for Brass Account or 2 for BrassPlus Account:2
Enter the overdraft limit: 465
Enter the interest rate: 0.1
Client: hfjka
Account number: 456
Balance: $456.00
In virtual ~Brass()
Client: dasf
Account number: 465456
Balance: $456456.00
In ~BrassPlus()
In virtual ~Brass()
如果不把基类析构函数声明为虚函数
~Brass(){std::cout << "In ~Brass()\n";}//非虚析构函数
~BrassPlus(){std::cout << "In ~BrassPlus()\n";}
则报警
警告内容:Brass基类拥有非虚析构函数,可能造成未知后果。
对应输出,Brass类对象和BrassPlus类对象都只调用基类Brass类的析构函数
Enter client's name: afd
Enter client's accout number: 4566
Enter opening balance: $456
Enter 1 for Brass Account or 2 for BrassPlus Account:1
Enter client's name: faffad
Enter client's accout number: 464655
Enter opening balance: $456456
Enter 1 for Brass Account or 2 for BrassPlus Account:2
Enter the overdraft limit: 456456
Enter the interest rate: 0.1
Client: afd
Account number: 4566
Balance: $456.00
In ~Brass()
Client: faffad
Account number: 464655
Balance: $456456.00
In ~Brass()
所以说,结论很简单:只要涉及继承,就把基类的析构函数声明为虚函数,保证派生类对象可以正确调用他自己的析构函数,不然如果派生类析构函数要做点什么却又无法被调用的话,就会出错
- 在派生类的方法定义中调用基类方法的标准办法:使用基类类名限定符
但是如果派生类没有重新定义某个基类方法,那就直接调用,无需用基类类名限定
只是针对那些在派生类中被重新定义的基类方法,由于有两个同名方法,所以必须使用基类类名限定说清楚要调用基类的那个方法。否则编译器又不知所措了。