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()

所以说,结论很简单:只要涉及继承,就把基类的析构函数声明为虚函数,保证派生类对象可以正确调用他自己的析构函数,不然如果派生类析构函数要做点什么却又无法被调用的话,就会出错

  • 在派生类的方法定义中调用基类方法的标准办法:使用基类类名限定符

但是如果派生类没有重新定义某个基类方法,那就直接调用,无需用基类类名限定

只是针对那些在派生类中被重新定义的基类方法,由于有两个同名方法,所以必须使用基类类名限定说清楚要调用基类的那个方法。否则编译器又不知所措了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值