第十三章——类继承

news2024/11/24 21:35:40

面向对象编程的主要目的之一是提供可重用的代码。(重用经过测试的代码比重新编写代码要好的多)

C++类提供了更高层次的重用性。很多厂商提供了类库,类组合了数据表示和类方法,因此提供了比函数库更加完整的程序包。通常类库是以源代码的方式提供的,这意味着可以对其进行修改。而且C++也提供了比修改源代码更好的方法来扩展和修改类——类继承,它能从已有的类派生出新的类,而派生类继承了原有类(称为基类)的特征,包括方法

可以通过继承完成一些工作

  • 可以在已有类的基础上添加功能
  • 可以给类添加数据
  • 可以修改类方法的行为

一个简单的基类 

 从一个类派生出另一个类时,原始类称为基类,继承类称为派生类。为说明继承,首先需要一个基类

设计一个TableTennisPlayer类

tabtenn0.h

#ifndef TABTENN0_H_
#define TABTENN0_H_
#include<string>
using std::string;
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; };

};
#endif // !TABTENN0_H_

tabenn0.cpp

#include<iostream>
#include"tabtenn0.h"

//成员初始化列表方式
TableTennisPlayer::TableTennisPlayer(const string & fn,const string &ln,bool ht)
	:firstname(fn),lastname(ln),hasTable(ht){}

//也可以这样写
//TableTennisPlayer::TableTennisPlayer(const string& fn, const string& ln, bool ht)
//{
//	firstname = fn;
//	lastname = ln;
//	hasTable = ht;
//}

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

usett0.cpp

#include<iostream>
#include"tabtenn0.h"
int main()
{
	using std::cout;
	TableTennisPlayer player1("Chuck", "Blizzard", true);
	TableTennisPlayer player2("Trar", "Boomdea", false);
	player1.Name();
	if (player1.HasTable())
	{
		cout << ": has a table.\n";
	}
	else
		cout << ": hasn't a table.\n";
	player2.Name();
	if (player2.HasTable())
	{
		cout << ": has a table.\n";
	}
	else
		cout << ": hasn't a table.\n";
	return 0;
}

输出 

 派生一个类

现在假设需要一个这样的类,它记录运动员在比赛中的比分。与其从零开始,不如从TableTennisPlayer类中派生出一个类。

首先将RatedPlayer类声明为从TableTennisPlayer类派生而来:

class RatedPlayer : public TableTennisPlayer
{
    ...
};

冒号指出RatedPlayer类的基类是 TableTennisPlayer类。

上述特殊的声明头表明TableTennisPlayer是一个公有基类,这被称为公有派生(public)。派生类对象包含基类对象。使用公有派生,基类的公有成员将成为派生类的公有成员;基类的私有成员也将成为派生类的一部分,但只能通过基类的公有和保护方法访问

RatedPlayer对象将具有以下特征:

  • 派生类对象存储了基类的数据成员(派生类继承了基类的实现)
  • 派生类对象可以使用基类的方法(派生类继承了基类的接口) 

需要在继承特性中添加什么呢?

  • 派生类需要自己的构造函数
  • 派生类可以根据需要添加额外的数据成员和成员函数 

在这个例子中,派生类需要另一个数据成员来存储比分,还应包含检索比分的方法和重置比分的方法。

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

 构造函数必须给新成员(如果有的话)和继承的成员提供数据。在第一个RatedPlayer构造函数中,每个成员对应一个形参;而第二个RatedPlayer构造函数使用一个TableTennisPlayer参数,该参数包括firstname、lastname和hasTable。

 构造函数访问权限

派生类不能直接访问基类的私有成员,而必须通过基类方法进行访问。例如RatedPlayer构造函数不能直接设置继承的成员(firstname、lastname、hasTable),而必须使用基类的公有方法来访问私有的基类成员。具体地说,派生类构造函数必须使用基类构造函数

创建派生类对象时,程序首先创建基类对象。这意味着基类对象应当在程序进入派生类构造函数之前被创建。C++使用成员初始化列表语法来完成这种工作,如

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

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

假设程序包含如下声明:

: TableTennisPlayer(fn,ln,ht)

则RatedPlayer构造函数将把实参“mally","Duck",和true赋给形参fn、ln、ht,然后将这些参数作为实参传递给TableTennisPlayer构造函数,后者将创建一个嵌套TableTennisPlayer对象,并将数据“mally","Duck",和true存储在该对象中。然后程序进入RatedPlayer构造函数体,完成RatedPlayer对象的创建,并将参数r的值赋给rating成员

如果省略成员初始化列表,情况将会如何?

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

必须首先创建基类对象,如果不调用基类构造函数,程序将使用默认的基类构造函数,因此上述代码和下面等效:

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

除非要使用默认构造函数,否则应显式调用正确的基类构造函数

下面来看第二个构造函数的代码:

RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer& tp) :TableTennisPlayer(tp)
{
	rating = r;
}

这里也将TableTennisPlayer的信息传递给了TableTennisPlayer构造函数:

TableTennisPlayer(tp)

由于tp的类型为TableTennisPlayer&,因此将调用基类的复制构造函数。基类没有定义复制构造函数,所以编译器将自动生成一个,

也可以对派生类成员使用成员初始化列表语法

RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer& tp) :TableTennisPlayer(tp),rating(r)
{
	
}

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

  • 首先创建基类对象
  • 派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数
  • 派生类构造函数应初始化派生类新增的成员函数

 释放对象的顺序与创建对象的顺序相反,即首先执行派生类的析构函数,然后自动调用基类的析构函数

注意:创建派生类对象时,程序首先调用基类构造函数,然后再调用派生类构造函数。基类构造函数负责初始化继承的数据成员;派生类构造函数主要用于初始化新增的数据成员。派生类的构造函数总是调用一个基类构造函数。可以使用初始化列表语法指明要使用的基类构造函数,否则将使用默认的基类构造函数

派生类对象过期时,程序将首先调用派生类析构函数,然后再调用基类构造函数

 使用派生类

要使用派生类,程序必须要能够访问基类声明。故将这两种类的声明置于同一个头文件中

tabtenn1.h

#ifndef TABTENN1_H_
#define TABTENN1_H_
#include<string>
using std::string;
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; };
};

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

#include<iostream>
#include"tabtenn1.h"

TableTennisPlayer::TableTennisPlayer(const string &fn,const string &ln,bool ht)
	:firstname(fn),lastname(ln),hasTable(ht){}
void TableTennisPlayer::Name() const
{
	std::cout << lastname << ", " << firstname;
}

//RatedPlayer methods
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

#include<iostream>
#include"tabtenn1.h"
int main()
{
	using std::cout;
	using std::endl;
	TableTennisPlayer player1("Tara", "Boomdea", false);
	RatedPlayer rplayer1(1140, "Mallory", "Duck", true);
	rplayer1.Name();    //derived object use base method
	if (rplayer1.HasTable())
	{
		cout << ": has a table.\n";
	}
	else
	{
		cout << ": hasn't a table.\n";
	}

	player1.Name();	//base object uses base method
	if (player1.HasTable())
	{
		cout << ": has a table.\n";
	}
	else
	{
		cout << ": has't a table.\n";
	}

	cout << "Name: ";
	rplayer1.Name();
	cout << "; Rating: " << rplayer1.Rating() << endl;

	RatedPlayer rplayer2(1212, player1);
	cout << "Name: ";
	rplayer2.Name();
	cout << "; Rating: " << rplayer2.Rating() << endl;
	return 0;
}

 

 派生类和基类之间的特殊关系

派生类于基类之间有一些特殊关系。

之一是派生类对象可以使用基类的方法,条件是方法不是私有的:

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

另外两个重要的关系是:基类指针可以在不进行显式类型转换的情况下指向派生类对象;基类引用可以在不进行显式类型转换的情况下引用派生类对象

RatedPlayer rplayer1(1140, "Mallory", "Duck", true);
TableTennisPlayer& rt = rplayer1; 
TableTennisPlayer* pt = &rplayer1;
rt.Name();	//invoke Name() with reference
pt->Name();  //invoke Name() wither pointer

 但是基类指针或引用只能调用基类方法

通常C++要求引用和指针类型与赋给的类型匹配,但这一规则对继承来说是例外,只是这种例外是单向的,不可以将基类对象和地址赋给派生类引用和指针:

TableTennisPlayer player("Betsy", "Bloop", true);
RatedPlayer& rr = player;	//NOT ALLOWED
RatedPlayer* pr = &player;	//NOT ALLOWED

 继承:is-a关系

C++有3种继承方式:公有继承、保护继承、和私有继承。公有继承是最常用的方式,它建立一种is-a关系,即派生类对象也是一个基类对象,可以对基类对象执行的任何操作也可以对派生类对象执行。

多态公有继承 

前面示例的RatedPlayer继承很简单。派生类对象使用基类的方法而未做任何修改。实际上可能会遇到这样的情况,即希望同一个方法在派生类和基类中的行为是不同的。换句话说,方法的行为应取决于调用该方法的对象。这种较复杂的方法称为多态——具有多种形态,即同一个方法的行为随上下文而异。有两种重要的机制可用于实现多态公有继承

  • 在派生类中重新定义基类的方法
  • 使用虚方法 

 现在来看另外一个例子,一个类用于表示基本支票账户——Brass Account,另外一个用于表示Brass Plus Account 账户,它添加了透支保护特性。

 Brass的信息和可执行的操作

  • 客户姓名
  • 账号
  • 当前结余
  • 创建账户
  • 存/取款
  • 显式账户信息

Brass Plus包含Brass的所有信息以及还包括了

  • 透支上限
  • 透支贷款利率
  • 当前的透支总额 

不需要新增操作,但有两种操作的实现不同

  • 对于取款操作,必须考虑透支保护
  • 显示操作必须显式BrassPlus账户的其他信息 

BrassPlus 账户限制了客户的透支款额。默认为500元,但有些客户的限额可能不同;银行可以修改客户的透支限额;BrassPlus账户对贷款收取利息,默认为11.125%,但有些客户的利率可能不一样;银行可以修改客户的利率 

开发Brass和Brass Plus类

brass.h

#ifndef BRASS_H_
#define BRASS_H_
#include<string>

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 Balance() const;
	virtual void ViewAcct() const;
	virtual ~Brass() {};
};

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; }
};

#endif
  •  BrassPlus类在Brass类的基础上添加了3个私有数据成员和3个共有数据成员
  • Brass类和BrassPlus类都声明了ViewAcct()和Withdraw()方法,但是BrassPlus对象和Brass对象的这些方法的行为是不同的
  • Brass类在声明ViewAcct()和Withdraw()时使用了virtual关键字。这些方法被称为虚方法
  • Brass类还声明了一个虚析构函数,该虚构函数不执行任何操作

 对上面提到的四点进行解释

第一点:派生类在基类的基础上新增加了数据成员和方法

第二点:介绍了声明如何指出方法在派生类的行为的不同。两个ViewAcct()原型表明将有两个独立的方法定义。基类的限定名为Brass::ViewAcct(),派生类版本的限定名为BrassPlus::ViewAcct(),程序将使用对象类型来确定使用哪个版本:

Brass dom("Dominic Banker", 11224, 4183.45);
BrassPlus dot("Dorothy Banker", 12118, 2529.00);
dom.ViewAcct();		//use Brass::ViewAcct()
dot.ViewAcct();		//use BrassPlus::ViewAcct()

对于在两个类中行为相同的方法,则只在基类中声明。 

第三点:(使用virtual)。如果方法是通过引用或指针而不是对象调用,它将决定使用哪一种方法。如果没有使用关键字virtual,程序将根据引用类型或指针类型选择方法;如果使用了virtual,程序将根据指针或引用指向的对象的类型来选择方法

如果ViewAcct()不是虚的,则

Brass dom("Dominic Banker", 11224, 4183.45);
BrassPlus dot("Dorothy Banker", 12118, 2529.00);
Brass& b1_ref = dom;
Brass& b2_ref = dot;
b1_ref.ViewAcct();		//use Brass::ViewAcct()
b2_ref.ViewAcct();		//use Brass::ViewAcct()

如果ViewAcct()是虚函数,则

Brass dom("Dominic Banker", 11224, 4183.45);
BrassPlus dot("Dorothy Banker", 12118, 2529.00);
Brass& b1_ref = dom;
Brass& b2_ref = dot;
b1_ref.ViewAcct();		//use Brass::ViewAcct()
b2_ref.ViewAcct();		//use BrassPlus::ViewAcct()

 经常在基类中将派生类会重新定义的方法声明为虚方法。方法在基类中被声明为虚方法后,它在派生类中将自动成为虚方法

第四点:基类声明了一个虚析构函数。这样做是为了确保释放派生类对象时,按正确的顺序调用析构函数

注意:如果要在派生类中重新定义基类的方法,通常应将基类方法声明为虚的。这样程序将根据对象类型而不是引用或指针的类型来选择方法。为基类声明一个虚析构函数也是一种惯例

类实现

brass.cpp 

#include<iostream>
#include"brass.h"
using std::cout;
using std::endl;
using std::string;

Brass::Brass(const string& s, long an, double bal)
{
	fullName = s;
	acctNum = an;
	balance = bal;
}
void Brass::Deposit(double amt)
{
	if (amt < 0)
	{
		cout << "Neagtive deposit not allowed; "
			<< "deposit is cancelled.\n";
	}
	else
	{
		balance += amt;
	}
}
void Brass::Withdraw(double amt)
{
	if (amt < 0)
	{
		cout << "Withdraw amount must be positive; "
			<< "Withdraw canceled.\n";
	}
	else if (amt <= balance)
	{
		balance -= amt;
	}
	else
	{
		cout << " Withdrawal amount of $" << amt
			<< " exceeds your balance.\n"
			<< " withdraw canceled.\n";
	}
}
double Brass::Balance() const
{
	return balance;
}

void Brass::ViewAcct() const
{
	cout << "Client: " << fullName << endl;
	cout << "Account Number: " << acctNum << endl;
	cout << "Balance: $" << balance << endl;
}


//BrassPlus Methods
BrassPlus::BrassPlus(const string& s, long an, double bal, double ml, double r) :Brass(s, an, bal)
{
	maxLoan = ml;
	rate = r;
	owesBank = 0.0;
}
BrassPlus::BrassPlus(const Brass& ba, double ml, double r) :Brass(ba)
{
	maxLoan = ml;
	owesBank = 0.0;
	rate = r;
}
void BrassPlus::ViewAcct() const
{
	Brass::ViewAcct();//display base portion
	cout << "Maximum loan: $" << maxLoan << endl;
	cout << "Owed to bank: $" << owesBank << endl;
	cout << "Loan Rate: " << 100 * rate << "%\n";
}
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";
	}
}

 派生类不能直接访问基类的私有数据,而必须使用基类的公有方法才能访问这些数据

使用Brass和BrassPlus类

#include<iostream>
#include"brass.h"

int main()
{
	using std::endl;
	using std::cout;
	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 Hoggy Account:\n";
	Hoggy.Deposit(1000.00);
	cout << "New balance: $" << Hoggy.Balance() << endl;

	cout << "Withdrawing $4200 from the Piggy Account:\n";
	Piggy.Withdraw(4200.00);
	cout << "Piggy account balance: $" << Piggy.Balance() << endl;
	cout << "Withdrawing $4200 from the Hoggy Account:\n";
	Hoggy.Withdraw(4200.00);
	Hoggy.ViewAcct();

	return 0;
}

 

 为何需要虚析构函数?

如果析构函数不是虚的,则将只调用对应于指针类型的析构函数。如果析构函数是虚的,将调用相应对象类型的析构函数。因此如果指针指向的是BrassPlus对象,将调用BrassPlus的析构函数,然后自动调用基类的析构函数。因此虚析构函数可以确保正确的析构函数序列被调用

  • 构造函数不能是虚函数

  • 析构函数应当是虚函数,除非类不用做基类。(给类定义一个虚析构函数并非错误,即使这个类不用做基类,这只是一个效率问题)

  • 友元不能是虚函数,因为友元不是类成员,而只有成员才能是虚函数

  • 如果派生类没有重新定义函数,将使用该函数的基类版本。如果派生类位于派生链中,则将使用最新的虚函数版本

访问控制:protected 

 前面已经有了关键字public和private来控制对类成员的访问,还存在另外一个访问类别,用关键字protected表示。关键字protected与private相似,在类外只能用公有类成员来访问protected部分中的类成员函数。private和protected之间的区别只有在基类派生的类中才会表现出来。派生类的成员可以直接访问基类中的保护成员,但不能直接访问基类中的私有成员;

因此对外部世界来说,保护成员的行为与私有成员相似;但对于派生类来说,保护成员的行为与公有成员相似

抽象基类

 抽象基类(abstract base class,ABC)。就是从近似的类中抽象出它们的共性,将这些特性放到一个ABC中。比如从Ellipse和Circle类中抽象出一个ABC,然后从该ABC派生出Circle和Ellipse类,这样便可以使用基类指针数组同时管理Circle和Ellipse对象(即可以使用多态方法)

 纯虚函数声名的结尾处为=0;当类声明中包含纯虚函数时,则不能创建该类的对象。包含纯虚函数的类只用作基类,要成为真正的ABC,必须至少包含一个纯虚函数。原型中的=0使虚函数成为纯虚函数。

应用ABC概念(仍然是举一个例子)

 将ABC用于Brass和BrassPlus账户,首先定义一个名为AcctABC的ABC。这个类包含Brass和BrassPlus类共有的所有方法和数据成员,而那些在Brass Plus和Brass类中的行为不同的方法应被声明为虚函数,至少应该有一个虚函数是纯虚函数,这样才能使AcctABC 成为抽象类

acctabc.h

#ifndef ACCTABC_H_
#define ACCTABC_H_
#include<iostream>
#include<string>

//Abstract base class
class AcctABC
{
private:
	std::string fullName;
	long acctNum;
	double balance;
protected:
	const std::string& FullName() const { return fullName; }
	long AcctNum() const { return acctNum; }
public:
	AcctABC(const std::string& s = "Nullbody", long an = -1, double bal = 0.0);
	void Deposit(double amt);
	virtual void Withdraw(double amt) = 0;	//pure virtual function
	double Balance() const { return balance; };
	virtual void ViewAcct()const = 0;	//pure virtual function
	virtual ~AcctABC() {}
};

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() {}
};

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.cpp

#include<iostream>
#include"acctabc.h"
using std::cout;
using std::endl;
using std::string;

//Abstract Base Class
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;
}

//Brass methods
void AcctABC::Withdraw(double amt)
{
	if (amt < 0)
	{
		cout << "Withdrawal amount must be positive; withdrawal canceled.\n";
	}
	else if (amt <= Balance())
	{
		AcctABC::Withdraw(amt);
	}
	else

	{
		cout << "Withdrawal amount of $" << amt << " exceeds your balance.Withdrawal canceled.\n";
	}
}
void Brass::ViewAcct()const
{
	cout << "Brass Client: " << FullName() << endl;
	cout << "Account Number: " << AcctNum() << endl;
	cout << "Balance: $" << Balance() << endl;
}

//BrassPlus methods
BrassPlus::BrassPlus(const string& s, long an, double bal, double ml, double r) :AcctABC(s, an, bal)
{
	maxLoan = ml;
	rate = r;
	owesBank = 0.0;
}
BrassPlus::BrassPlus(const Brass& ba, double ml, double r) :AcctABC(ba)
{
	maxLoan = ml;
	owesBank = 0.0;
	rate = r;
}
void BrassPlus::ViewAcct()const
{
	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 << "Loan Rate: " << 100 * rate << "%\n";
}
void BrassPlus::Withdraw(double amt)
{
	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";
	}
}

类设计回顾

 编译器生成的成员函数

编译器会自动生成一些公有成员函数——特殊成员函数。

1、默认构造函数

默认构造函数要么没有参数,要么所有的参数都有默认值。如果没有定义任何构造函数,编译器将定义默认构造函数,使程序能够创建对象

自动生成的默认构造函数的另一项功能是,调用基类的默认构造函数以及调用本身是对象的成员所属类的默认构造函数

另外如果派生类构造函数的成员初始化列表中没有显式调用基类构造函数,则编译器将使用基类的默认构造函数来构造派生类对象的基类部分,在这种情况下,如果基类没有构造函数,将导致编译阶段错误。

如果定义了某种构造函数,则编译器将不会定义默认构造函数。在这种情况下,如果需要默认构造函数,则必须自己提供

提供构造函数的动机之一是确保对象总能被正确地初始化

2、复制构造函数

复制构造函数接受其所属类的对象作为参数

在下述情况下,将使用复制构造函数:

  • 将新对象初始化为一个同类对象
  • 按值将对象传递给函数
  • 函数按值返回对象
  • 编译器生成临时对象

3、赋值运算符

默认的赋值运算符用于处理同类对象之间的赋值。不要将赋值与初始化混淆了。

如果语句创建新的对象,则使用初始化;如果语句修改已有对象的值,则是赋值;

类的其他方法 

定义类时,还需要注意其他几点

1、构造函数

构造函数不同于其他类方法,因为它创建新的对象,而其他类的方法只是被现有的对象调用。这是构造函数不被继承的原因之一。继承意味着派生类对象可以使用基类的方法,然而构造函数在完成其工作内容之前,对象并不存在。

2、析构函数

一定要定义显式析构函数来释放类构造函数使用new分配的所有内存,并完成类对象所需的任何特殊的清理工作。对于基类,即使它不需要析构函数,也应提供一个虚析构函数

3、转换

使用一个参数就可以调用的构造函数定义了从参数类型到类类型的转换

在带一个参数的构造函数原型中使用explicit将禁止进行隐式转换,但仍允许显式转换

4、按值传递对象与传递引用

通常,编写使用对象作为参数的函数时,应按引用而不是按值来传递对象。这样做的原因之一是为了提高效率。按值传递对象涉及到生成临时拷贝,即调用复制构造函数,然后调用析构函数,调用这些函数需要时间,复制大型对象比传递引用花费的时间要多得多。如果函数不修改对象,应将参数声明为const引用。

按引用传递对象的另一个原因是,在继承使用虚函数时,被定义为接受基类引用参数的函数可以接受派生类

5、返回对象和返回引用

有些成员函数直接返回对象,而另一些则返回引用

首先在代码方面,直接返回对象与返回引用之间唯一的区别在于函数原型和函数头:

Star noval(const Star &);
Star &nova2(const Star &);

 直接返回对象与按值传递对象相似:它们都生成临时副本。同样,返回引用与按引用传递对象相似:调用和被调用的函数对同一个对象进行操作。

然而并不总是可以返回引用。函数不能返回在函数中创建的临时对象的引用,因为当函数结束时,临时对象将消失,因此这种引用将是非法的。

6、使用const

使用const时应特别注意,可以用它来保证方法不修改参数

公有继承的考虑因素

1.is-a关系

要遵循is-a关系。如果派生类不是一种特殊的基类,则不要使用公有派生。这种关系就是,无需进行显式类型转换,基类指针就可以指向派生类对象,基类引用可以引用派生类对象。反过来是行不通的

2、什么不能被继承

构造函数不能被继承

析构函数不能被继承

赋值运算符不能被继承

3、赋值运算符

如果编译器发现程序将一个对象赋给同一个类的另一个对象,它将自动为这个类提供一个赋值运算符

4、私有成员与保护成员

对派生类而言,保护成员类似于公有成员;但对于外部而言,保护成员与私有成员类似。派生类可以直接访问基类的保护成员,但只能通过基类的成员函数来访问私有成员。

5、虚方法 

 设计基类时,必须确定是否将类方法声明为虚的。如果希望派生类能够重新定义方法,则应在基类中将方法定义为虚的;如果不希望重新定义方法,则不必将其声明为虚的。

6、析构函数

基类的析构函数应该是虚的。这样,当通过指向对象的基类指针或引用来删除派生对象时,程序将首先调用派生类的析构函数,然后调用基类的析构函数,而不仅仅是调用基类的析构函数

7、友元函数

由于友元函数并非类成员,因此不能继承。但是可以通过强制类型转换,将派生类引用或指针转换为基类引用或指针,然后使用转换后的指针或引用来调用基类的友元函数

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/753903.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

文件IO_文件读写(附Linux-5.15.10内核源码分析)

目录 1.什么是文件偏移量&#xff1f; 1.1 文件偏移量介绍 1.2 文件偏移量重点 1.3 文件偏移量工作原理 2.文件偏移量设置 2.1 lseek函数 2.2 lseek内核源码分析 3.写文件 3.1 write函数 3.2 write内核源码分析 4.读文件 4.1 read函数 4.2 read内核源码分析 5.文…

物流难统计、难管理?学会这招,问题迎刃而解

在当今数字化的时代&#xff0c;商家面临着诸多物流挑战。物流数据人工统计成本高、管理难、决策难是很常见的问题。本文将探讨如何通过智能数据分析解决这些问题&#xff0c;帮助商家提升物流效率和管理能力。 物流数据人工统计&#xff0c;难决策 物流数据沉淀全靠人工线下…

Vue3_简介、CompositionVPI、新的组件

文章目录 Vue3快速上手1.Vue3简介2.Vue3带来了什么1.性能的提升2.源码的升级3.拥抱TypeScript4.新的特性 一、创建Vue3.0工程1.使用 vue-cli 创建2.使用 vite 创建 二、常用 Composition API1.拉开序幕的setup2.ref函数3.reactive函数4.Vue3.0中的响应式原理vue2.x的响应式Vue3…

labview实现呼吸灯颜色渐变效果

呼吸灯效果具有美好的视觉观感&#xff0c;前一段时期感受了一位大佬在MCU中实现呼吸灯颜色渐变效果&#xff0c;很是震撼。这引起了我的兴趣&#xff0c;本文则是实现一种呼吸灯效果(主要在于颜色的渐变体现)。 程序整体视图 程序框图 公式节点程序 int red_is_0 red 0 ?…

探索MR与AIGC技术的发展机遇:教育、医疗领域的前景展望

在当今科技迅猛发展的时代&#xff0c;混合现实&#xff08;MR&#xff09;和增强智能生成创作&#xff08;AIGC&#xff09;技术正逐渐成为教育、医疗领域中的关键驱动力。这两项前沿技术的结合为我们带来了无限的可能性和创新的机遇。 MR技术在教育领域中的发展与机遇是广泛而…

非洲秃鹫优化算法(AVOA)(含MATLAB代码)

先做一个声明&#xff1a;文章是由我的个人公众号中的推送直接复制粘贴而来&#xff0c;因此对智能优化算法感兴趣的朋友&#xff0c;可关注我的个人公众号&#xff1a;启发式算法讨论。我会不定期在公众号里分享不同的智能优化算法&#xff0c;经典的&#xff0c;或者是近几年…

LayUI框架实现OA会议系统——增删改查

目录 前言 1. 配置准备 1.1 Layui框架 1.2 mysql数据库表 1.3 用户管理JSP页面 1.4 新增、修改用户共用界面 2. 后台编写 2.1 编写UserDao类增删改查方法 2.2 R工具类 2.3 BaseDao数据库查询方法 2.4 UserAction控制器类 3. 前台JS编写 3.1 userManage页面JS 3.2…

测试报告?Python自动化测试-Allure测试报告使用大全,一篇全通透

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 安装并配置环境变…

工业相机的基础参数释义

景深&#xff1a; 可以清晰拍摄被测物的距离范围。 工作距离&#xff1a; 相机镜头至被测物的距离。 物像距离&#xff1a; 被测物至芯片靶面的距离。 像元尺寸&#xff1a; 一个像素在长和宽方向上所代表的实际大小&#xff0c;单位通常为微米。像元尺寸越大&#xff0c;分辨率…

记录stm32c8t6使用TIM4_CH1、TIM4_CH2输出PWM波控制编码电机出现的问题

由于之前是使用PB9、PB7引脚即TIM4_ch3\TIM4_ch4&#xff0c;由于项目更改为c8t6的PB、PB7引脚&#xff08;TIM4_ch3\TIM4_ch4&#xff09; 改为配置后发现只有一边的轮子可以转到&#xff0c;明明配置没什么问题&#xff0c;编译也没有报错&#xff0c;最后将pwm的调制模式更改…

Android 进程与进程之间的通信--AIDL详细教程,以传递对象为例,两个app实现

我这里案例是 通过 IPC 传递对象 &#xff08;以DemoBean类为例&#xff09; 如下&#xff1a; AIDL 使用一种简单语法&#xff0c;允许您通过一个或多个方法&#xff08;可接收参数和返回值&#xff09;来声明接口。参数和返回值可为任意类型&#xff0c;甚至是 AIDL 生成的其…

TypeScript 学习笔记 环境安装-类型注解-语法细节-类-接口-泛型

文章目录 TypeScript 学习笔记概述TypeScript 开发环境搭建 类型注解类型推断 数据类型JS的7个原始类型Array数组object、Object 和 {}可选属性 ? 和 可选链运算符?. function函数TS类型: any类型 | unknow类型TS类型: void类型TS类型&#xff1a;never类型 &#xff08;几乎…

数据库应用:CentOS 7离线安装MySQL与Nginx

目录 一、理论 1.安装依赖 二、实验 1.离线安装MySQL与Nginx 2.离线安装Nginx 三、问题 1.执行nginx -v命令报错 四、总结 一、理论 1.安装依赖 &#xff08;1&#xff09;概念 安装依赖是指在软件开发中&#xff0c;为了运行或者编译一个程序或者库&#xff0c;在计…

JVM——类加载和垃圾回收

目录 前言 JVM简介 JVM内存区域划分 JVM的类加载机制 1.加载 双亲委派模型 2.验证 验证选项 3.准备 4.解析 5.初始化 触发类加载 JVM的垃圾回收策略 GC 一&#xff1a;找 谁是垃圾 1.引用计数 2.可达性分析 &#xff08;这个方案是Java采取的方案&#x…

k210学习篇(一)环境搭建

一、为什么选择Canmv开发板? 便宜!便宜!便宜!淘宝200即可买到一个能带摄像头和LCD屏等等的开发板 二、利用Maix Hub在线训练 Maix Hub官网https://maixhub.com/home Maix Hub使用教程:K210学习笔记——MaixHub在线训练模型(新版) 注意: 三、配置开发环境 1.MaixPy IDE…

某网站提交登陆信息加密JS逆向实战分析

1. 写在前面 对于爬虫开发者来说&#xff0c;职业生涯中可能或多或少会遇到各种各样的网站&#xff0c;其中有些必要要求登陆才能浏览。那么模拟登陆的时候发现提交的登陆信息&#xff08;用户名、密码&#xff09;都是经过加密后的&#xff0c;如何处理&#xff1f;这里找到了…

我只改五行代码,接口性能提升了 10 倍!

背景 某公司的一个 ToB 系统&#xff0c;因为客户使用的也不多&#xff0c;没啥并发要求&#xff0c;就一直没有经过压测。这两天来了一个“大客户”&#xff0c;对并发量提出了要求&#xff1a;核心接口与几个重点使用场景单节点吞吐量要满足最低500/s的要求。 当时一想&am…

C++ IO流

文章目录 C语言的输入与输出流是什么?CIO流C标准IO流C文件流 stringstream的简单介绍 C语言的输入与输出 在C语言中,我们使用最频繁的输入输出方式为: scanf 和 printf. scanf : 从输入设备(键盘)读取数据,并将值存放在变量中.printf: 将指定的文字/字符串输出到标准输出设备…

数据库约束与表的关系(数据库系列4)

目录 前言&#xff1a; 1.数据库的约束 1.1约束类型 1.1.1 not null 1.1.2 unique 唯一约束 1.1.3 default 默认值约束 1.1.4 primary key 主键约束 1.1.5 foreign key 外键约束 2.表的关系 2.1 一对一 2.2 一对多 2.3 多对一 3.新增 4.聚合查询 4.1聚合函数 4.…

Pinecone - 向量数据库

文章目录 关于 PineconeRoadMapSemantic SearchChatbots购买查看 API Key创建索引代码调用安装库 pinecone-client查看已经创建的索引创建索引插入数据获取索引统计分析信息查询索引,获取相似向量删除索引关于 Pinecone 官网 : https://www.pinecone.i