前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >类继承

类继承

原创
作者头像
Alan_1
发布2023-04-30 12:23:32
1.3K0
发布2023-04-30 12:23:32
举报
文章被收录于专栏:Alan的blogAlan的blog

类继承:它能够从已有的类派⽣出新的类,⽽派⽣类继承了原有类(称为基类)的特征,包括⽅法

可以通过继承完成的⼀些⼯作:
  • 可以在已有类的基础上添加功能。
  • 可以给类添加数据成员。
  • 可以修改类⽅法的⾏为。
Note:

string类有⼀个将const char *作为 参数的构造函数,使⽤C-⻛格字符串初始化string对象时,将⾃动调⽤这 个构造函数

1.公共继承:

代码语言:c++
复制
class Son : public Father
{
    ...
};
  • “:”指出Father是一个公有基类
  • 派⽣类对象包含基类对象。
  • 使⽤公有派⽣,基类的公有成员将成为派⽣类的公有成员;基类的私有部分也将成为派⽣类的⼀部分,但只能通过基类的公有和保护⽅法访问.
Son对象将具有以下特征:
  • 派⽣类对象存储了基类的数据成员(派⽣类继承了基类的实现);
  • 派⽣类对象可以使⽤基类的⽅法(派⽣类继承了基类的接⼝)。
可以在继承特性中添加:
  • 派⽣类需要⾃⼰的构造函数。

构造函数必须给新成员(如果有的话)和继承的成员提供数据。

  • 派⽣类可以根据需要添加额外的数据成员和成员函数。

1.2 构造函数:访问权限的考虑

  • 派⽣类构造函数必须使⽤基类构造函数对基类的私有成员进行初始化。
派生类对象创建过程:
  1. 创建派⽣类对象时,程序⾸先调⽤基类构造函数创建基类对象。(这意味着基类对象应当在程序进⼊派⽣类构造函数之前被创建。)
  2. 调用派生类构造函数对新增数据成员进行初始化。
构造函数使用成员初始化列表:

情况1:

代码语言:c++
复制
//Father:TableTennisPlayer
//Son:RatedPlayer
RatedPlayer::RatedPlayer(unsigned int r , const string& fn ,
	const string& ln , bool ht ):TableTennisPlayer(fn,ln,ht)
{
	rating = r;
}

其中:TableTennisPlayer(fn,ln,ht)是成员初始化列表。它是可执⾏的 代码,调⽤TableTennisPlayer构造函数

情况2:

代码语言:c++
复制
RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer& tp)
	:TableTennisPlayer(tp)
{
	rating = r;
}
  • 由于tp的类型为TableTennisPlayer &,因此将调⽤基类的复制构造函数
  • 如果需要使⽤基类复制构造函数但基类⼜没有定义,编译器将⾃动⽣成⼀个基类复制构造函数。
  • 在这种情况下,执⾏成员复制的隐式复制构造函数是合适的,因为这个类没有使⽤动态内存分配。
  • 动态内存分配:使用new和delete进行的内存管理分配。
  • 只要存在指针类型数据成员,就一定要定义复制构造函数进行深度复制,防止发生内存错误问题(例如,同一内存区域进行两次释放)。
  • 也可以对派⽣类成员使⽤成员初始化列表语法:在列表中使⽤成员名。
代码语言:javascript
复制
  RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer& tp)
  	:TableTennisPlayer(tp),rating(t)
  {
  }

有关派⽣类构造函数的要点如下:

  • ⾸先创建基类对象;
  • 派⽣类构造函数应通过成员初始化列表将基类信息传递给基类构造函数;
  • 派⽣类构造函数应初始化派⽣类新增的数据成员。
如果省略成员初始化列表:
代码语言:c++
复制
RatedPlayer::RatedPlayer
    (unsigned int r , const string& fn ,
     const string& ln , bool ht )
{
	rating = r;
}
  1. ⾸先创建基类对象,如果不调⽤基类构造函数,程序将使⽤默认的基类构造函数。
  2. 除⾮要使⽤默认构造函数,否则应显式调⽤正确的基类构造函数

上述等效于:

代码语言:javascript
复制

   RatedPlayer::RatedPlayer(unsigned int r , const string& fn , 
                        const string& ln , bool ht )//:TabTennisPlayer()
   {    
   rating = r;
   }

调用派生类构造函数进行初始化:

Note:
  • 创建派⽣类对象时,程序⾸先调⽤基类构造函数,然后再调⽤派⽣类构造函数.
  • 基类构造函数负责初始化继承的数据成员;派⽣类构造函数主要⽤于初始化新增的数据成员
  • 派⽣类的构造函数总是调⽤⼀个基类构造函数。
  • 可以使⽤初始化器列表语法指明要使⽤的基类构造函数,否则将使⽤默认的基类构造函数。
  • 派⽣类对象过期时,程序将⾸先调⽤派⽣类析构函数,然后再调⽤基类析构函数
成员初始化列表:
  • 派⽣类构造函数可以使⽤初始化器列表机制将值传递给基类构造函数。
代码语言:c++
复制
Son::Son(type1 x, type2 y):Father(x,y)
{
    ...
}

其中Son是派⽣类,Father是基类,x和y是基类构造函数使⽤的变量。

  • 除虚基类外,类只能将值传递回相邻的基类,但后者可以使⽤ 相同的机制将信息传递给相邻的基类,依此类推。
  • 如果没有在成员初始化列表中提供基类构造函数,程序将使⽤默认的基类构造函数。
  • 成员初始化列表只能⽤于构造函数

1.3 使⽤派⽣类:

  • 要使⽤派⽣类,程序必须要能够访问基类声明

示例:

tabtenn.h

代码语言:c++
复制
#pragma once
//tabtenn.h -- a table-tennis base class

#ifndef TABTENN1_H_
#define TABTENN1_H_

#include<string>

using std::string;
//simple base class
class TableTennisPlayer
{
private:
	string firstname;
	string lastname;
	bool hasTable;
public:
	TableTennisPlayer(const string& fn = "none", const string& ln = "none", bool ht = false);
	void Name()const;
	bool HasTable()const { return hasTable; };
	void ResetTable(bool v) { hasTable = v; };
};

//simple derived class
class RatedPlayer :public TableTennisPlayer
{
private:
	unsigned int rating;
public:
	RatedPlayer(unsigned int r=0, const string& fn = "none", const string& ln = "none", bool ht = false);
	RatedPlayer(unsigned int r ,const TableTennisPlayer& tp);
	unsigned int Rating()const { return rating; }
	void ResetRating(unsigned int r) { rating = r; }
};

#endif // !TABTENN1_H_

tabtenn1.cpp

代码语言:c++
复制
//tabtenn1.cpp -- simple base-class methods
#include"tabtenn1.h"
#include<iostream>

//TableTennisPlayer
TableTennisPlayer::TableTennisPlayer(const string& fn , const string& ln , bool ht )
	:firstname(fn),lastname(ln),hasTable(ht)//注意默认参数
{

}
void TableTennisPlayer::Name()const
{
	std::cout << this->lastname << ", " << this->firstname ;
}

//RatePlayer
RatedPlayer::RatedPlayer(unsigned int r , const string& fn, const string& ln, bool ht)
	:TableTennisPlayer(fn,ln,ht)
{
	rating = r;
}
RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer& tp)
	:TableTennisPlayer(tp),rating(r)
{

}

usett1.cpp

代码语言:c++
复制
//usett1.cpp -- using base class and derived class
#include"tabtenn1.h"
#include<iostream>

int main()
{
	using std::cout;
	using std::endl;
	TableTennisPlayer player1("Tara", "Boomdea", false);
	RatedPlayer rplayer1(1140, "Mallory", "Duck", true);
	rplayer1.Name();
	if (rplayer1.HasTable())
		cout << ":has a table.\n";
	else
		cout << ":hasn't a table.\n";
	player1.Name();
	if (player1.HasTable())
		cout << ":has a table.\n";
	else
		cout << ":hasn't a table.\n";
	cout << "Name: ";
	rplayer1.Name();
	cout << "; Rating: " << rplayer1.Rating() << endl;
//initialize RatedPlayer using TableTennisPlayer object
	RatedPlayer rplayer2(1212, player1);
	cout << "Name: ";
	rplayer2.Name();
	cout << "; Rating: " << rplayer2.Rating() << endl;

	return 0;
}

1.4 派⽣类和基类之间的特殊关系:

  • 派⽣类对象可以使⽤基类的⽅法,条件是⽅法不是私有的。
  • 基类指针可以在不进⾏显式类型转换的情 况下指向派⽣类对象
  • 基类引⽤可以在不进⾏显式类型转换的情况下引⽤派⽣类对象:
  • 基类引⽤定义的函数或指针参数可⽤于基类对象派⽣类对象;
代码语言:javascript
复制
```c++

void Show(const TableTennisPlayer& rt)

{

using std::cout;

cout<<"Name: ";

rt.Name();

cout<<"\nTable: ";

if(rt.HasTable())

    cout<<"yes\n";

else

    cout<<"no\n";

}

//形参rt是⼀个基类引⽤,它可以指向基类对象或派⽣类对象,所以可以在Show( )中使⽤TableTennis参数或Ratedplayer参数:

TableTennisPlayer player1("Tara", "Boomdea", false);

RatedPlayer rplayer1(1140, "Mallory", "Duck", true);

show(player1);

show(rplayer1);
  • 对于形参指向基类的指针的函数,可以使⽤基类对象的地址派⽣类对象的地址作为实参:
代码语言:javascript
复制
```c++

void Wohs(const TableTennisPlayer* pt);

...

TableTennisPlayer player1("Tara", "Boomdea", false);

RatedPlayer rplayer1(1140, "Mallory", "Duck", true);

Wohs(&player1);

Wohs(&rplayer1);

  • 引⽤兼容性属性也让您能够将基类对象初始化为派⽣类对象,尽管不那么直接。
代码语言:javascript
复制
```c++

RatedPlayer olaf1(1840,"Olaf","Loaf",true);

TableTennisPlayer olaf2(olaf1);		

//它将olaf2初始化为嵌套在RatedPlayer对象olaf1中的TableTennisPlayer对象。

//要初始化olaf2,匹配的构造函数的原型如下:

TableTennisPlayer(const RatedPlayer&);

//类定义中没有这样的构造函数,但存在隐式复制构造函数:

//implicit copy constructor

TableTennisPlayer(const TableTennisPlayer&)		

//形参是基类引⽤,因此它可以引⽤派⽣类.将olaf2初始化为olaf1时,将要使⽤该构造函数,它复制firstname、lastname和hasTable成员。

代码语言:javascript
复制
//也可以将派⽣对象赋给基类对象

RatedPlayer olaf1(1840,"Olaf","Loaf",true);

TableTennisPlayer winner;

winner=olaf1;

//程序将使⽤隐式重载赋值运算符:

代码语言:javascript
复制
TableTennisPlayer& operator=(const TableTennisPlayer& )const;

//基类引⽤指向的也是派⽣类对象,因此olaf1的基类部分被复制给winner。

基类指针或引⽤只能⽤于调⽤基类⽅法;不可以将基类对象和地 址赋给派⽣类引⽤和指针:

2.继承:is-a关系

  • C++有3种继承⽅式:公有继承、保护继承和私有继承。
  • 公有继承建⽴⼀种is-a关系,即派⽣类对象也是⼀个基类对象,可以对基类对象执⾏的任何操作,也可以对派⽣类对象执⾏。
  • 新类继承原始类的所有数据成员。
  • 因为派⽣类可以添加特性,所以,将这种关系称为is-a-kind-of(是⼀种)关系可能 更准确,但是通常使⽤术语is-a。

3.多态公有继承:

  • 起源:希望同⼀个⽅法在派⽣类 和基类中的⾏为是不同的。
  • 两种重要的机制可⽤于实现多态公有继承:
  • 在派⽣类中重新定义基类的⽅法
  • 使⽤虚⽅法。

brass.h

代码语言:javascript
复制
  #pragma once
  #ifndef BRASSH
  #define BRASSH
  #include<string>
  //Brass Account Class
  class 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 Blance()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; }
  };

代码语言:javascript
复制
  #endif // !BRASSH
  • Brass类和BrassPlus类都声明了ViewAcct( )和Withdraw( )⽅法,但BrassPlus对象和Brass对象的这些⽅法的⾏为是不同的;
  • 两个 ViewAcct( )原型表明将有2个独⽴的⽅法定义。基类版本的限定名为 Brass::ViewAcct( ),派⽣类版本的限定名为BrassPlus::ViewAcct( )。
  • 程序 将使⽤对象类型来确定使⽤哪个版本:
代码语言:javascript
复制
```c++

Brass dom("Dominic Banker",11224,4183.45);

BrassPlus dot("Dorothy Banker",12118.2592.00);

dom.ViewAcct();		//use Brass::ViewAcct( )

dot.ViewAcct();		//use BrassPlus::ViewAcct( )
  • Brass类在声明ViewAcct()和Withdraw()时使⽤了新关键字virtual。这些⽅法被称为虚⽅法(virtual method);
  • 如果没有使⽤关键字 virtual,程序将根据引⽤类型指针类型选择⽅法;
  • 如果使⽤了 virtual,程序将根据引⽤或指针指向的对象的类型来选择⽅法。

//如果ViewAcct( )不是虚的,则程序的⾏为如下:

代码语言:javascript
复制
  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 Bbrass::ViewAcct()

//使⽤Brass指针代替引⽤时,⾏为将与此类似。

//如果ViewAcct( )是虚的,则⾏为如下:

代码语言:javascript
复制
  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 BbrassPlus::ViewAcct()

//使⽤Brass指针代替引⽤时,⾏为将与此类似。

  • Brass类还声明了⼀个虚析构函数,这样做是为了确保释放派⽣对象时,按正确的顺序调⽤析构函数。
note:
  • 如果要在派⽣类中重新定义基类的⽅法,通常应将基类⽅法声明为虚的。
  • 这样,程序将根据对 象类型⽽不是引⽤或指针的类型来选择⽅法版本。
  • 为基类声明⼀个虚析构函数也是⼀种惯例。
3.1类实现:
  • 注意,关键字virtual只 ⽤于类声明的⽅法原型中,⽽没有⽤于程序⽅法定义中。

brass.cpp

代码语言:c++
复制
//brass.cpp -- bank class methods
#include<iostream>
#include"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 methods
Brass::Brass(const std::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 ###.## format
	format initialState = setFormat();
	precis prec = cout.precision(2);
	
	if (amt < 0)
	{
		cout << "Withdrawal amount must e positive; "
			<< "withdrawal canceled.\n";
	}
	else if (amt <= balance)
	{
		balance -= amt;
	}
	else
	{
		cout << "Withdrawal amount of $" << amt
			<< " exceeds your alance.\n"
			<< "Withdrawal canceled.\n";
	}
	restore(initialState, prec);
}
double Brass::Balance()const
{
	return balance;
}
void Brass::ViewAcct()const
{
	//set up ###.## format
	format initialState = setFormat();
	precis prec = cout.precision(2);
	cout << "Client: " << fullName << endl;
	cout << "Account Numbber: " << acctNum << endl;
	cout << "Balance: $" << balance << endl;
	restore(initialState, prec);		//restore original format
}

//BrassPlus Methods
//派⽣类构造函数在初始化基类私有数据时,采⽤的是成员初始化列表语法。
//都使⽤成员初始化列表语法,将基类信息传递给基类构造函数,然后使⽤构造函数体初始化BrassPlus类新增的数据项。
BrassPlus::BrassPlus(const std::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;
}
//⾮构造函数不能使⽤成员初始化列表语法,但派⽣类⽅法可以调⽤公有的基类⽅法。
void BrassPlus::ViewAcct()const
{
	format initialState = setFormat();
	precis prec = cout.precision(2);
    //BrassPlus::ViewAcct( )显⽰新增的BrassPlus数据成员,并调⽤基类⽅法Brass::ViewAcct( )来显⽰基类数据成员。
    //在派⽣类⽅法中,标准技术是使⽤作⽤域解析运算符来调⽤基类⽅法。
	Brass::ViewAcct();		//display base portion
    //如果代码没有使⽤作⽤域解析运算符,编译器将认为ViewAcct( )是BrassPlus::ViewAcct( ),这将创建⼀个不会终⽌的递归函数。
	cout << "Maximum loan: $" << maxLoan << endl;
	cout << "Owed to bank: $" << owesBank << endl;
	cout.precision(3);
	cout << "Loan Rate: " << 100 * rate << "%\n";
	restore(initialState, prec);
}

//该⽅法使⽤基类的Balance( )函数来确定结余。因为派⽣类没有重新定义该⽅法,代码不必对Balance( )使⽤作⽤域解析运算符。
void BrassPlus::Withdraw(double amt)
{
	//set up ###.## format
	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);
}

//⽅法ViewAcct( )和Withdraw( )使⽤格式化⽅法setf( )和precision( )将浮点值的输出模式设置为定点,即包含两位⼩数。
//设置模式后,输出的模式将保持不变,因此该⽅法将格式模式重置为调⽤前的状态。  

//为避免代码重复,该程序将设置格式的代码放在辅助函数中
//函数setFormat( )设置定点表⽰法并返回以前的标记设置:
format setFormat()
{
	//set up ###.## format
	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);
}
  • 记住,派⽣类并不能直接访问基类的私有数据,⽽必须使⽤基类的公有⽅法才能访问这些数据。
3.2使用Brass和BrassPlus类

usebrass1.cpp

代码语言:c++
复制
//usebrass1.cpp -- testing bank account classes
//compile with brass.cpp
#include<iostream>
#include"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;

//⽅法是通过对象(⽽不是指针或引⽤)调⽤的,没有使⽤虚⽅法特性。
	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;
}
//请注意为何Hogg受透⽀限制,⽽Pigg没有

输出结果:

代码语言:c++
复制
Client: Porcelot Pigg
Account Numbber: 381299
Balance: $4000.00

Client: Horatio Hogg
Account Numbber: 382288
Balance: $3000.00
Maximum loan: $500.00
Owed to bank: $0.00
Loan Rate: 11.125%

Depositing $1000 into the Hogg Account:
New balance: $4000
Withdrawing $4200 from the Pigg Account:
Withdrawal amount of $4200.00 exceeds your alance.
Withdrawal canceled.
Pigg account balance: $4000
Withdrawing $4200 from the Hogg Account:
Bank advance: $200.00
Finance charge: $22.25
Client: Horatio Hogg
Account Numbber: 382288
Balance: $0.00
Maximum loan: $500.00
Owed to bank: $222.25
Loan Rate: 11.125%
3.3演示虚方法的行为:
  • 假设要同时管理Brass和BrassPlus账⼾
  • 可以创建指向 Brass的指针数组。
  • 每个元素的类型都相同,但由于使⽤的是公有继承模型,因此Brass指针既可以指向Brass对象,也可以指向BrassPlus 对象。
  • 可以使⽤⼀个数组来表⽰多种类型的对象。这就是多态性.

usebrass2.cpp

代码语言:c++
复制
//usebrass2.cpp -- polymorphic example
//compile with brass.cpp
#include<iostream>
#include<string>
#include"brass.h"

const int CLIENTS = 4;

int main()
{
	using std::cout;
	using std::endl;
	using std::cin;

	Brass* 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 BbrassPlus 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;
	}
//如果数组成员指向的是Brass对象,则调⽤Brass::ViewAcct( );
//如果指向的是BrassPlus对象,则调⽤BrassPlus::ViewAcct( )。
//如果Brass::ViewAcct( )被声明为非虚的,则在任何情况下都将调⽤Brass::ViewAcct( )。
    
	for (int i = 0; i < CLIENTS; i++)
	{
		delete p_clients[i];		//free memory
	}
	cout << "Done.\n";

	return 0;
}

输出结果:

代码语言:c++
复制
Enter client's name:Harry Fishsong
Enter client's account number: 112233
Enter opening balance: $1500
Enter 1 for Brass Account or 2 for BbrassPlus Account: 1
Enter client's name:Dinah Otternoe
Enter client's account number: 121213
Enter opening balance: $18800
Enter 1 for Brass Account or 2 for BbrassPlus Account: 2
Enter the overdraft limit: $350
Enter the 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 BbrassPlus Account: 2
Enter the overdraft limit: $800
Enter the 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 BbrassPlus Account: 1



Client: Harry Fishsong
Account Numbber: 112233
Balance: $1500.00

Client: Dinah Otternoe
Account Numbber: 121213
Balance: $18800.00
Maximum loan: $350.00
Owed to bank: $0.00
Loan Rate: 12.000%

Client: Brenda Birdherd
Account Numbber: 212118
Balance: $5200.00
Maximum loan: $800.00
Owed to bank: $0.00
Loan Rate: 10.000%

Client: Tim Turtletop
Account Numbber: 233255
Balance: $688.00

Done.
3.4为何需要虚析构函数
  • 如果析构函数不是虚的,则将只调⽤对应于指针类型的析构函数。
    • 对于程序usebrass2.cpp这意味着只有Brass的析构函数被调⽤,即使指针指向的是⼀个BrassPlus对象
  • 如果析构函数是虚的,将调⽤相应对象类型的析构函数。
  • 如果指针指向的是BrassPlus对象,将调⽤BrassPlus的析构函数,然后⾃动调⽤基类的析构函数。
  • 使⽤虚析构函数可以确保正确的析构函数序列被调⽤。
  • 如果BrassPlus包含⼀个执⾏某些操作的析构函数,则Brass必须有⼀个虚析构函数,即使该析构函数不执⾏任何操作。

4.静态联编和动态联编:

  • 将源代码中的函数调⽤解释为执⾏特定的函数代码块被称为函数名联编(binding)。
  • C/C++编译器可以在编译过程完成这种联编。在编译过程中进⾏联编被称为静态联编(static binding),⼜称为早期联编(early binding)。
  • 编译器必须⽣成能够在程序运⾏时选择正确的虚⽅法的代码,这被称为动态联编(dynamic binding), ⼜称为晚期联编(late binding)
4.1指针和引用类型的兼容性
  • 公有继承建⽴is-a关系的⼀种⽅法是如何处理指向对象的指针和引⽤。
  • 通常,C++不允许将⼀种类型的地址赋给另⼀种类型的指针,也不允许⼀种类型的引⽤指向另⼀种类型:
代码语言:javascript
复制
  double x=2.5;
  int * pi= &x;		//invalid assignment, mismatched pointer types
  int& r1= x;		//invalid assignment, mismatched reference type

代码语言:txt
复制
指向基类引⽤或指针可以引⽤派⽣类对象,⽽不必进⾏显式类型转换。例如,下⾯的初始化是允许的:
代码语言:javascript
复制
  BrassPlus dilly("Annie Dill",493222,2000);
  Brass* pb=&dilly;		//ok
  Brass& rb=dilly;		//ok
  • 派⽣类引⽤或指针转换基类引⽤或指针被称为向上强制转换(upcasting),这使公有继承不需要进⾏显式类型转换。
  • 将指向对象的指针作为函数参数时,也是如此。
  • 向上强制转换是可传递的,也就是说,如果从BrassPlus派⽣出BrassPlusPlus类,则Brass指针或引⽤可以引⽤Brass对象、BrassPlus对象或BrassPlusPlus对象。
  • 将基类指针或引⽤转换为派⽣类指针或引⽤——称为向下强制转换(downcasting)。
  • 如果不使⽤显式类型转换,则向下强制转换是不允许的。原因是is-a关系通常是不可逆的。
  • 派⽣类可以新增数据成员,因此使⽤这些数据成员的类成员函数不能应⽤于基类。
  • 对于使⽤基类引⽤或指针作为参数的函数调⽤,将进⾏向上转换。

//假定每个函数都调⽤虚⽅法ViewAcct( ):

代码语言:javascript
复制
  void fr(Brass& rb);
  void fp(Brass* pb);
  void fv(Brass b);
  int main()
  {
  	Brass b("Billy Bee", 123432, 10000.0);
  	BrassPlus bp("Betty Beep", 232313, 12345.0);

//随引⽤和指针发⽣的隐式向上转换导致函数fr( )和fp( )分别为Brass对象和BrassPlus对象使⽤Brass::ViewAcct( )和BrassPlus::ViewAcct( )。

代码语言:javascript
复制
  	fr(b);		//uses Brass::ViewAcct();
  	fr(bp);		//uses BrassPlus::ViewAcct();
  	fp(b);		//uses Brass::ViewAcct();
  	fp(bp);		//uses BrassPlus::ViewAcct();

//按值传递导致只将BrassPlus对象的Brass部分传递给函数fv( )。

代码语言:javascript
复制
  	fv(b);		//uses Brass::ViewAcct();
  	fv(bp);		//uses Brass::ViewAcct();
  	...
  }

向上强制转换和向下强制转换

  • 隐式向上强制转换使基类指针或引⽤可以指向基类对象或派⽣类对象,因此需要动态联编
4.2虚成员函数和动态联编
代码语言:c++
复制
BrassPlus ophelia;		//drived-class object
Brass * bp;				//base-class pointer
bp=&ophelia;			//Brass pointer to BrasPlus object
bp->ViewAcct();
  • 编译器对⾮虚⽅法使⽤静态联编。
    • 如果在基类中没有将ViewAcct( )声明为虚的,则bp->ViewAcct( )将根据指针类型(Brass *)调⽤Brass::ViewAcct( )。
  • 编译器对虚⽅法使⽤动态联编。
    • 如果在基类中将ViewAcct( )声明为虚的,则bp->ViewAcct( ) 根据对象类型(BrassPlus)调⽤BrassPlus::ViewAcct( )。
4.2.1为什么有两种类型的联编以及为什么默认为静态联编
  • 效率和概念模型:
    • 效率:为使程序能够在运⾏阶段进⾏决策,必须采取⼀些⽅法来跟踪基类指针或引⽤指向的对象类型,这增加了额外的处理开销。
      • 使⽤静态联编更合理,效率也更⾼。
      • 如果类不会⽤作基类,则不需要动态联编。
      • 如果派⽣类(如RatedPlayer)不重新定义基类的任何⽅法,也不需要使⽤动态联编。
    • C++的指导原则之⼀是,不要为不使⽤的特性付出代价(内存或者处理时间)。仅当程序设计确实需要虚函数时,才使⽤它们。
    • 概念模型:在设计类时,可能包含⼀些不在派⽣类重新定义的成员函数。
      • 例如,Brass::Balance( )函数返回账⼾结余,不应该重新定义。
      • 不将该函数设置为虚函数,有两⽅⾯的好处:⾸先效率更⾼;其次,指出不要重新定义该函数。
note:

如果要在派⽣类中重新定义基类的⽅法,则将它设置为虚⽅法;否则,设置为⾮虚⽅法。

4.2.2虚函数的工作原理:
  • 通常,编译器处理虚函数的⽅法是:给每个对象添加⼀个隐藏成员
    • 隐藏成员中保存了⼀个指向函数地址数组的指针。这种数组称为虚函数表(virtual function table,vtbl)。
      • 虚函数表中存储了为类对象进⾏声明的虚函数的地址。

例如:

  • 基类对象包含⼀个指针,该指针指向基类中所有虚函数的地址表。
    • 派⽣类对象将包含⼀个指向独⽴地址表的指针。
      • 如果派⽣类提供了虚函数的新定义,该虚函数表将保存新函数的地址
      • 如果派⽣类没有重新定义虚函数,该vtbl将保存函数原始版本的地址
      • 如果派⽣类定义了新的虚函数,则该函数的地址也将被添加到vtbl
    • 注意,⽆论类中包含的虚函数是1个还是10个,都只需要在对象中添加1个地址成员,只是表的⼤⼩不同⽽已。
  • 调⽤虚函数时,程序将查看存储在对象中的vtbl地址,然后转向相应的函数地址表。
    • 如果使⽤类声明中定义的第⼀个虚函数,则程序将使⽤数组中的第⼀个函数地址,并执⾏具有该地址的函数。
    • 如果使⽤类声明中的第三个虚函数,程序将使⽤地址为数组中第三个元素的函数。
  • 使⽤虚函数时,在内存和执⾏速度⽅⾯有⼀定的成本,包括:
    • 每个对象都将增⼤,增⼤量为存储地址的空间
    • 对于每个类,编译器都创建⼀个虚函数地址表(数组);
    • 对于每个函数调⽤,都需要执⾏⼀项额外的操作,即到表中查找地址。
  • 虽然⾮虚函数的效率⽐虚函数稍⾼,但不具备动态联编功能。
4.3有关虚函数注意事项:
  • 在基类⽅法的声明中使⽤关键字virtual可使该⽅法在基类以及所有的派⽣类(包括从派⽣类派⽣出来的类)中是虚的。
  • 如果使⽤指向对象的引⽤或指针来调⽤虚⽅法,程序将使⽤为对象类型定义的⽅法,⽽不使⽤为引⽤或指针类型定义的⽅法。这称为动态联编或晚期联编。这种⾏为⾮常重要,因为这样基类指针或引⽤可以指向派⽣类对象
  • 如果定义的类将被⽤作基类,则应将那些要在派⽣类中重新定义的类⽅法声明为虚的
4.3.1构造函数:
  • 构造函数不能是虚函数。
  • 创建派⽣类对象时,将调⽤派⽣类的构造函数,⽽不是基类的构造函数,然后,派⽣类的构造函数将使⽤基类的⼀个构造函数,这种顺序不同于继承机制。
  • 派⽣类不继承基类的构造函数。
4.3.2析构函数:
  • 析构函数应当是虚函数,除⾮类不⽤做基类。
  • 即使基类不需要显式析构函数提供服务,也不应依赖于 默认构造函数,⽽应提供虚析构函数,即使它不执⾏任何操作
  • 通常应给基类提供⼀个虚析构函数,即使它并不需要析构函数。
4.3.3友元:
  • 友元不能是虚函数,因为友元不是类成员,⽽只有成员才能是虚函数。
  • 如果由于这个原因引起了设计问题,可以通过让友元函数使⽤虚成员函数来解决。
4.3.4没有重新定义:
  • 如果派⽣类没有重新定义函数,将使⽤该函数的基类版本。
  • 如果派 ⽣类位于派⽣链中,则将使⽤最新的虚函数版本。
  • 例外的情况是基类版本是隐藏的。
4.3.5重新定义将隐藏原定义方法:
代码语言:c++
复制
class Dwelling
{
public:
    virtual void showperks(int a) const;
   ...
};
class Hovel:public Dwelling
{
public:
    //新定义将showperks( )定义为⼀个不接受任何参数的函数。
    virtual void showperks()const;
    //重新定义不会⽣成函数的两个重载版本,⽽是隐藏了接受⼀个int参数的基类版本。
    ...
};

//这将导致问题,可能会出现类似于下⾯这样的编译器警告:
Waring:Havel::showperks(void) hides Dwelling::showperks(int)
//也可能不会出现警告。但不管结果怎样,代码将具有如下含义:
   Hovel trump;
trump.showperks();		//valid
trump.showperks(5);		//invalid
  • 重新定义继承的⽅法并不是重载。
  • 如果在派⽣类中重新定义函数,将不是使⽤相同的函数特征标覆盖基类声明,
  • ⽽是隐藏同名的基类⽅法,不管参数特征标如何。
  • 两条经验规则:
  • 如果重新定义继承的⽅法,应确保与原来的原型完全相同,
代码语言:txt
复制
- 如果**返回类型**是**基类引⽤或指针**,则**可以修改**为**指向派⽣类的引⽤或指针**(这种例外是新出现的)。这种特性被称为返回类型协变(covariance of return type),因为允许返回类型随类类型的变化⽽变化。
- 注意,这种例外只适⽤于**返回值**,⽽不适⽤于参数。
  • 如果基类声明被重载了,则应在派⽣类中重新定义所有的基类版本
代码语言:txt
复制
- 如果**只重新定义⼀个版本**,则**另外两个版本将被隐藏**,派⽣类对象将⽆法使⽤它们。
- 注意,如果不需要修改,则新定义可**只调⽤基类版本**
代码语言:txt
复制
```c++
代码语言:javascript
复制
void Hovel::showperks()const{Dwelling::showperks();}

5.访问控制:protected

  • 类外只能⽤公有类成员来访问protected部分中的类成员。
  • private和protected之间的区别只有 在基类派⽣的类中才会表现出来。
    • 派⽣类的成员可以直接访问基类的保护成员,
    • 不能直接访问基类的私有成员。
  • 使⽤保护数据成员可以简化代码的编写⼯作,但存在设计缺陷。
  • 对于成员函数来说,保护访问控制很有⽤,它让派⽣类能够访问公众不能使⽤的内部函数
note:

最好对类数据成员采⽤私有访问控制,不要使⽤保护访问控制;同时通过基类⽅法使派⽣类能够访问基类数据。

6.抽象基类(abstract base class,ABC):

  • ABC描述的是⾄少使⽤⼀个纯虚函数的接⼝,从ABC派⽣出的类将根据派⽣类的具体特征使⽤常规虚函数来实现这种接⼝
  • 从Ellipse和Circle类中抽象出它们的共性,将这些特性放到⼀个ABC中。
  • 然后从该ABC派⽣出Circle和Ellipse类。添加所需的成员来完成每个类。
  • 这样,便可以使⽤基类指针数组同时管理Circle和Ellipse对象,即可以使⽤多态⽅法)。
  • 使⽤这些类的程序将能够创建Ellipse对象和Circle对象,但是不能创建BaseEllipse对象。
    • 由于Circle和Ellipse对象的基类相同,因此可以⽤ BaseEllipse指针数组同时管理这两种对象。
    • 像Circle和Ellipse这样的类有时被称为具体(concrete)类,这表⽰可以创建这些类型的对象。
代码语言:c++
复制
class BaseEllipse		//ABC
{
private:
    double x;
    double y;
    ...
public:
    BaseEllipse(double x0=0,double y0=0):x(x0),y(y0){}
    virtual ~BaseEllipse(){}
    //,也许所有的基类⽅法都与Move( )⼀样,可以在基类中进⾏定义,但您仍需要将这个类声明为抽象的。
    void Move(int nx,int ny){x=nx; y=ny;}
    //在这种情况下,可以将原型声明为虚的:
    //	void Move(int nx,int ny)=0;
    //这将使基类成为抽象的,但您仍可以在实现⽂件中提供⽅法的定义:
    //	void BaseEllipse::Move(int nx,int ny){x=nx; y=ny;}
    virtual double Area()const=0;
    //在原型中使⽤=0指出类是⼀个抽象基类,在类中可以不定义该函数。
    ...
};
  • C++通过使⽤纯虚函数(pure virtual function)提供未实现的函数。纯虚函数声明的结尾处为=0
代码语言:c++
复制
virtual double Area()const=0;
//原型中的=0使虚函数成为纯虚函数。
//C++允许纯虚函数有定义
  • 当类声明中包含纯虚函数时,则不能创建该类的对象
  • 包含纯虚函数的类只⽤作基类
  • 要成为真正的ABC,必须⾄少包含⼀个纯虚函数
  • 原型中使⽤=0指出类是⼀个抽象基类,在类中可以不定义该函数
6.1应用ABC概念:
  1. ,⾸先定义⼀个名为AcctABC的ABC。这个类包含 Brass和BrassPlus类共有的所有⽅法和数据成员,
  2. 那些在BrassPlus类和 Brass类中的⾏为不同的⽅法应被声明为虚函数。
  3. ⾄少应有⼀个虚函数是纯虚函数,这样才能使AcctABC成为抽象类。

acctabc.h

代码语言:c++
复制
#pragma once
//为帮助派⽣类访问基类数据,AcctABC提供了⼀些保护⽅法;
//派⽣类⽅法可以调⽤这些⽅法,但它们并不是派⽣类对象的公有接⼝的组成部分。
//AcctABC还提供⼀个保护成员函数,⽤于处理格式化(以前是使⽤⾮成员函数处理的)。
//另外,AcctABC类还有两个纯虚函数,所以它确实是抽象类。
//acctabc.h -- bank account classes
#ifndef ACCTABC_H_
#define ACCTABC_H_
#include<iostream>
#include<string>

//Abstract Base Class
class AcctABC
{
private:
	std::string fullName;
	long acctNum;
	double balance;
protected:
    //这个版本定义了⼀个结构,⽤于存储两项格式设置;并使⽤该结构来设置和恢复格式,
	struct Formatting
	{
		std::ios_base::fmtflags flag;
		std::streamsize pr;
	};
    
    //保护⽅法FullName( )和AcctNum( )提供了对数据成员fullName和acctNum的只读访问,使得可以进⼀步定制每个派⽣类的ViewAcct( )。
	const std::string& FullName()const { return fullName; }
	long AcctNum()const { return acctNum; }
    
    //只需调⽤两个函数来设置和恢复格式
    //	Formatting f = SetFormat();
    //	...
    //	Restore(f);
	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
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(){}
};

//BrassPlus Account Class
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.0);
	BrassPlus(const Brass& ba, double ml = 500, double r = 0.10);
	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

代码语言:c++
复制
//acctabc.cpp -- bank account class methods
#include<iostream>
#include"acctabc.h"

using std::cout;
using std::ios_base;
using std::endl;
using std::string;

//Abstract Base Class
AcctABC::AcctABC(const std::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;
}

//protected methods for formatting
AcctABC::Formatting AcctABC::SetFormat()const
{
	//set up ###.## format
	Formatting f;
	f.flag=cout.setf(std::ios_base::fixed, std::ios_base::floatfield);
	f.pr = cout.precision(2);
	return f;
}
void AcctABC::Restore(Formatting& f)const
{
	cout.setf(f.flag, ios_base::floatfield);
	cout.precision(f.pr);
}

//Brass methods
void Brass::Withdraw(double amt)
{
	if (amt < 0)
	{
		cout << "Withdrawal amount must e positive; "
			<< "withdrawal canceled.\n";
	}
	else if (amt <= Balance())
	{
		AcctABC::Withdraw(amt);
	}
	else
	{
		cout << "Withdrawal amount of $" << amt
			<< " exceeds your alance.\n"
			<< "Withdrawal canceled.\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 Methods
BrassPlus::BrassPlus(const std::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 << "Maximum 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);
}
  • 旧版本存在的问题是
    • setFormat( )和restore( )都是独⽴的函数,这些函数与客⼾定义的同名函数发⽣冲突。
    • 解决这种问题的⽅式:
      • ⼀种⽅式是将这些函数声明为静态的,这样它们将归⽂件brass.cpp及其继任acctabc.cpp私有。
      • 另⼀种⽅式是,将这些函数以及结构Formatting放在⼀个独⽴的名称空间中
  • 将这些结构和函数放在了类定义的保护部分。这使得它们对基类和派⽣类可⽤,同时向外隐藏了它们。
  • 对于Brass和BrassPlus账⼾的这种新实现,使⽤⽅式与旧实现相同,因为类⽅法的名称和接⼝都与以前⼀样。
  • 例如:为使程序清单13.10能够使⽤新的实现,需要采取下⾯的步骤将usebrass2.cpp转换为 usebrass3.cpp:
  • 使⽤acctabc.cpp⽽不是brass.cpp来链接usebrass2.cpp。
  • 包含⽂件acctabc.h,⽽不是brass.h。
  • 将下⾯的代码:Brass p_clientsCLIENTS; 替换为AcctABC p_clientsCLIENTS;

usebrass3.cpp

代码语言:javascript
复制
  //usebrass3.cpp -- polymorphic example using an abstract base class
  //compile with acctabc.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_clientsCLIENTS;
  	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 1or 2: ";
  		if (kind == '1')
  		{
  			p_clientsi = 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_clientsi = new BrassPlus(temp, tempnum, tempbal, tmax, trate);
  		}
  		while (cin.get() != '\n')
  			continue;
  	}
  	cout << endl;
  	for (int i = 0; i < CLIENTS; i++)
  	{
  		p_clientsi->ViewAcct();
  		cout << endl;
  	}
  	for (int i = 0; i < CLIENTS; i++)
  	{
  		delete p_clientsi;
  	}
  	cout << "Done.\n";
  	return 0;
  }

//该程序本⾝的⾏为与⾮抽象基类版本相同,因此如果输⼊与给程序usebrass2.cpp提供的输⼊相同,输出也将相同。

6.2ABC理念:
  • 设计ABC之前,⾸先应开发⼀个模 型——指出编程问题所需的类以及它们之间相互关系
  • 如果要设计类继承层次,则只能将那些不会被⽤作基类的类设计为具体的类。
  • 可以将ABC看作是⼀种必须实施的接⼝。
    • ABC要求具体派⽣类覆盖其纯虚函数——迫使派⽣类遵循ABC设置的接⼝规则。
    • 在基于组件的编程模式中,使⽤ABC使得组件设计⼈员能够制定“接⼝约定”,这样确保了从ABC派⽣的所有组件都⾄少⽀持 ABC指定的功能。

7.继承和动态内存分配:

7.1第⼀种情况:派⽣类不使⽤new
代码语言:c++
复制
//Base Class Using DMA
class baseDMA
{
private:
	char* label;	//use new in constructors
	int rating;

public:
	baseDMA(const char* l = "null", int r = 0);
    //复制构造函数
	baseDMA(const baseDMA& rs);
    //析构函数
	virtual ~baseDMA();
    //重载赋值运算符
	baseDMA& operator=(const baseDMA& rs);
	...
};
  • 从baseDMA派⽣出lackDMA类,⽽后者不使⽤new,也未包 含其他⼀些不常⽤的、需要特殊处理的设计特性:
代码语言:c++
复制
//drived class without DMA
class lacksDMA :public baseDMA
{
private:
	char color[40];

public:
	...
};
//不需要为lackDMA类定义显式析构函数、复制构造函数和赋值运算符。
  • 析构函数:
    • 如果没有定义析构函数,编译器将定义⼀个不执⾏任何操作的默认构造函数
    • 派⽣类的默认构造函数在执⾏⾃⾝的代码后会调⽤基类析构函数。
  • 复制构造函数:
    • 默认复制构造函数执⾏成员复制,这对于动态内存分配来说是不合适的,但对于新的lacksDMA 成员来说是合适的。
    • 只需考虑继承的baseDMA对象
    • 成员复制将根据数据类型采⽤相应的复制⽅式
      • 将long复制到long中是通过使⽤常规赋值完成的;
      • 复制类成员继承的类组件时,则是使⽤该类的复制构造函数完成的
    • lacksDMA类的默认复制构造函数使⽤显式baseDMA复制构造函数来复制lacksDMA对象的baseDMA部分。
  • 赋值运算符:
    • 类的默认赋值运算符将⾃动使⽤基类的赋值运算符来对基类组件进⾏赋值。
  • 派⽣类对象的这些属性也适⽤于本⾝是对象的类成员
7.2第二种情况:派⽣类使⽤new
  • 在这种情况下,必须为派⽣类定义显式析构函数复制构造函数赋值运算符
  • 派⽣类析构函数⾃动调⽤基类的析构函数,故其⾃⾝的职责对派⽣类构造函数执⾏⼯作的进⾏清理。
代码语言:c++
复制
//derived class with DMA
class hasDMA :public baseDMA
{
private:
	char* style;	//use new in constructors
public:
	...
};
//hasDMA析构函数必须释放指针style管理的内存,并依赖于baseDMA的析构函数来释放指针label管理的内存
baseDMA::~baseDMA()
{
    delete [] label;
}

hasDMA::~hasDMA()
{
    delete [] style;
}
//BaseDMA的复制构造函数遵循⽤于char数组的常规模式
//即使⽤strlen( )来获悉存储C-⻛格字符串所需的空间、分配⾜够的内存(字符数加上存储空字符所需的1字节)并使⽤函数strcpy()将原始字符串复制到⽬的地:
baseDMA::baseDMA(const baseDMA& rs)
{
    label=new char[std::strlen(rs.label)+1];
    std::strcpy(label,rs.label);
    rating=rs.rating;
}
//hasDMA复制构造函数只能访问hasDMA的数据
//它必须调⽤baseDMA复制构造函数来处理共享的baseDMA数据:
hasDMA::hasDMA(const hasDMA& rs)
    :baseDMA(hs)		//成员初始化列表将⼀个hasDMA引⽤传递给baseDMA构造函数。
    {	
        //因为复制构造函数baseDMA有⼀个baseDMA引⽤参数,⽽基类引⽤可以指向派⽣类型。
        //因此,baseDMA复制构造函数将使⽤hasDMA参数的baseDMA部分来构造新对象的baseDMA部分。
        style=new char[std::strlen(hs.style)+1];
        std::strcpy(style,hs.style);
    }

baseDMA& baseDMA::operator=(const baseDMA& rs)
{
    if(this==&rs)
        return *this;
    delete [] label;
    label=new char[std::strlen(rs.label)+1];
    std::strcpy(label,rs.label);
    rating=rs.rating;
    return *this;
}

//由于hasDMA也使⽤动态内存分配,所以它也需要⼀个显式赋值运算符。
//派⽣类的显式赋值运算符必须负责所有继承的baseDMA基类对象的赋值,可以通过显式调⽤基类赋值运算符来完成这项⼯作
hasDMA& hasDMA::operator=(const hasDMA& hs)
{
    if(this==&hs)
        return *this;
    
    //通过使⽤函数表⽰法,⽽不是运算符表⽰法,可以使⽤作⽤域解析运算符。
    //该语句的含义: *this=hs;		//use baseDMA::operator=()
    hasDMA::operator=(hs);		//copy base portion
    //使⽤函数表⽰法使得赋值运算符被正确调⽤。
    delete [] style;
    style= new char[std::strlen(hs.style)+1];
    std::strcpy(style,hs.style);
    return *this;
}
  • 基类派⽣类都采⽤动态内存分配时,派⽣类的析构函 数、复制构造函数、赋值运算符都必须使⽤相应的基类⽅法来处理基类元素。
  • 对于析构函数,这是⾃动完成的;
  • 对于构造函数,这是通过在初始化成员列表中调⽤基类的复制构造函数来完成的;如果不这样做,将⾃动调⽤基类的默认构造函 数。
  • 对于赋值运算符,这是通过使⽤作⽤域解析运算符显式地调⽤基类的赋值运算符来完成的。
7.3 使⽤动态内存分配和友元的继承⽰例

dma.h

代码语言:c++
复制
#pragma once
#ifndef DMA_H_
#define DMA_H_
#include<iostream>

//Base Class Using DMA
class baseDMA
{
private:
	char* label;
	int rating;

public:
	baseDMA(const char* l = "null", int r = 0);
	baseDMA(const baseDMA& rs);
	virtual ~baseDMA();
	baseDMA& operator=(const baseDMA& rs);
	friend std::ostream& operator<<(std::ostream& os, const baseDMA& rs);
};

//drived class without DMA
//no destructor needed
//uses implicit copy constructor
//uses implicit assignment operator
class lacksDMA :public baseDMA
{
private:
	enum{COL_LEN=40};
	char color[COL_LEN];

public:
	lacksDMA(const char* c = "blank", const char* l = "null", int r = 0);
	lacksDMA(const char* c, const baseDMA& rs);
	friend std::ostream& operator<<(std::ostream& os, const lacksDMA& rs);
	
};

//derived class with DMA
class hasDMA :public baseDMA
{
private:
	char* style;	//use new in constructors
public:
	hasDMA(const char* s = "none", const char* l = "null", int r = 0);
	hasDMA(const char* s, const baseDMA& rs);
	hasDMA(const hasDMA& hs);
	~hasDMA();
	hasDMA& operator=(const hasDMA& rs);
	friend std::ostream& operator<<(std::ostream& os, const hasDMA& rs);
};

#endif // !DMA_H_

dma.cpp

代码语言:c++
复制
//dma.cpp -- dma class methods
#pragma warning(disable : 4996)

#include "dma.h"
#include<cstring>

//baseDMA methods
baseDMA::baseDMA(const char* l , int r )
{
	label = new char[std::strlen(l) + 1];
	std::strcpy(label, l);
	rating = r;
}
baseDMA::baseDMA(const baseDMA& rs)
{
	label = new char[std::strlen(rs.label) + 1];
	std::strcpy(label, rs.label);
	rating = rs.rating;
}
baseDMA::~baseDMA()
{
	delete[] label;
}
baseDMA& baseDMA::operator=(const baseDMA& rs)
{
	if (this == &rs)
		return *this;
	delete[] label;
	label = new char[std::strlen(rs.label) + 1];
	strcpy(label, rs.label);
	rating = rs.rating;
	return *this;
}
std::ostream& operator<<(std::ostream& os, const baseDMA& rs)
{
	os << "Label: " << rs.label << std::endl;
	os << "Rating: " << rs.rating << std::endl;
	return os;
}

//lacksDMA methods
lacksDMA::lacksDMA(const char* c, const char* l, int r)
	:baseDMA(l,r)
{
	std::strncpy(color, c, 39);
	color[39] = '\0';
}
lacksDMA::lacksDMA(const char* c, const baseDMA& rs)
	:baseDMA(rs)
{
	std::strncpy(color, c, COL_LEN - 1);
	color[COL_LEN - 1] = '\0';
}
std::ostream& operator<<(std::ostream& os, const lacksDMA& ls)
{
	os << (const baseDMA&)ls;
	os << "Color: " << ls.color << std::endl;
	return os;
}

//hasDMA methods
hasDMA::hasDMA(const char* s, const char* l, int r)
	:baseDMA(l,r)
{
	style = new char[std::strlen(s) + 1];
	std::strcpy(style, s);
}
hasDMA::hasDMA(const char* s, const baseDMA& rs)
	:baseDMA(rs)
{
	style = new char[std::strlen(s) + 1];
	std::strcpy(style, s);
}
hasDMA::hasDMA(const hasDMA& hs)
	:baseDMA(hs)
{
	style = new char[std::strlen(hs.style) + 1];
	strcpy(style, hs.style);
}
hasDMA::~hasDMA()
{
	delete[] style;
}
hasDMA& hasDMA::operator=(const hasDMA& hs)
{
	if (this == &hs)
		return *this;
	baseDMA::operator=(hs);
	delete[] style;
	style = new char[std::strlen(hs.style) + 1];
	strcpy(style, hs.style);
	return *this;
}
	
std::ostream& operator<<(std::ostream& os, const hasDMA& hs)
{
	//作为hasDMA类的友元,该函数能够访问style成员。
	//使⽤baseDMA类的友元函数operator<<( ),访问baseDMA类成员lable和rating
	//解决⽅法是使⽤强制类型转换,以便匹配原型时能够选择正确的函数。
	//友元不是成员函数,所以不能使⽤作⽤域解析运算符来指出要使⽤哪个函数。
	os << (const baseDMA&) hs;//代码将参数const hasDMA &转换成类型为const baseDMA& 的参数:
	os << "Style: " << hs.style << std::endl;
	return os;
	
}

usedma.cpp

代码语言:c++
复制
//usedma.cpp -- inheritance,friends, and DMA
//compile with dma.cpp
#include<iostream>
#include"dma.h"

int main()
{
	using std::cout;
	using std::endl;

	baseDMA shirt("Portabelly", 8);
	lacksDMA balloon("red", "Blimpo", 4);
	hasDMA map("Mercator", "Buffalo Keys", 5);
	cout << "Displaying baseDMA object:\n";
	cout << shirt << endl;
	cout << "Displaying lacksDMA object:\n";
	cout << balloon << endl;
	cout << "Display hasDMA object:\n";
	cout << map << endl;
	lacksDMA balloon2(balloon);
	cout << "Result of lacksDMA copy:\n";
	cout << balloon2 << endl;
	hasDMA map2;
	map2 = map;
	cout << "Result of hasDMA assignment:\n";
	cout << map2 << endl;
	return 0;
}

输出结果:

代码语言:c++
复制
Displaying baseDMA object:
Label: Portabelly
Rating: 8

Displaying lacksDMA object:
Label: Blimpo
Rating: 4
Color: red

Display hasDMA object:
Label: Buffalo Keys
Rating: 5
Style: Mercator

Result of lacksDMA copy:
Label: Blimpo
Rating: 4
Color: red

Result of hasDMA assignment:
Label: Buffalo Keys
Rating: 5
Style: Mercator

8.类设计回顾:

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 可以通过继承完成的⼀些⼯作:
  • Note:
  • 1.公共继承:
    • Son对象将具有以下特征:
      • 可以在继承特性中添加:
      • 1.2 构造函数:访问权限的考虑
        • 派生类对象创建过程:
          • 构造函数使用成员初始化列表:
            • 如果省略成员初始化列表:
              • Note:
                • 成员初始化列表:
                • 1.3 使⽤派⽣类:
                • 1.4 派⽣类和基类之间的特殊关系:
                • 2.继承:is-a关系
                • 3.多态公有继承:
                  • note:
                    • 3.1类实现:
                      • 3.2使用Brass和BrassPlus类
                        • 3.3演示虚方法的行为:
                          • 3.4为何需要虚析构函数
                          • 4.静态联编和动态联编:
                            • 4.1指针和引用类型的兼容性
                              • 4.2虚成员函数和动态联编
                                • 4.2.1为什么有两种类型的联编以及为什么默认为静态联编
                                • note:
                                • 4.2.2虚函数的工作原理:
                              • 4.3有关虚函数注意事项:
                                • 4.3.1构造函数:
                                • 4.3.2析构函数:
                                • 4.3.3友元:
                                • 4.3.4没有重新定义:
                                • 4.3.5重新定义将隐藏原定义方法:
                                • note:
                            • 5.访问控制:protected
                            • 6.抽象基类(abstract base class,ABC):
                              • 6.1应用ABC概念:
                                • 6.2ABC理念:
                                • 7.继承和动态内存分配:
                                  • 7.1第⼀种情况:派⽣类不使⽤new
                                    • 7.2第二种情况:派⽣类使⽤new
                                      • 7.3 使⽤动态内存分配和友元的继承⽰例
                                      • 8.类设计回顾:
                                      相关产品与服务
                                      对象存储
                                      对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
                                      领券
                                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档