第十三章-类继承(上)13.1~13.3

2020/4/9 19:01:18 人评论 次浏览 分类:学习教程

类继承能从已有的类派生出新的类,而派生类继承了原有类(基类)的所有特征,包括方法。

13.1 一个简单的基类

从一个类派生出另一个类时,原始类称为基类,继承类称为派生类。假设一个乒乓球会员类:

定义在头文件中
#ifndef TABTENN0_H_
#define TABTENN0_H_
#include <string>
using namespace std;
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

成员函数定义

定义在源文件中
#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
{
	cout << lastname << ", " << firstname;
}

main函数:

定义在源文件中
#include <iostream>
using namespace std;
#include "tabtenn0.h"
int main()
{
	TableTennisPlayer player1("Chuck", "Blizzard", true);
	TableTennisPlayer player2("Tara", "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;
}
输出:
Blizzard, Chuck: has a table.
Boomdea, Tara: hasn't a table.

13.1.1 派生一个类

假设需要这样一个类,它能包括成员在比赛中的比分。与其从0开始写,不如从之前的TableTennisClass类派生一个类。

#include "tabtenn0.h"
class RatedPlayer : public TableTennisPlayer
{
...
};

冒号指出RatedPlayer类的基类是TableTennisPlayer类。public指明TableTennisPlayer是一个公有基类,这被称为公有派生。派生类对象包含基类对象。使用公有派生,基类的公有成员将成为派生类的公有成员;基类的私有部分也将成为派生类的一部分,但是只能通过基类的公有和保护方法访问私有部分
上述代码后。Ratedplayer对象将具有以下特征:

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

因此Ratedplayer类可以存储人物的姓名,判断有没有球桌。还可以使用TableTennisPlayer的Name,hasTable,ResetTable方法。
派生类需要自己的构造函数,并且根据需要可以添加额外的数据成员和成员函数。

#include "tabtenn0.h"
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;}
};

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

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

派生类不能直接访问基类的私有成员,必须通过基类方法访问。比如RatedPlayer构造函数不能直接设置继承的成员,而必须使用基类的公有方法来访问私有的基类成员。换句话说,派生类构造函数必须使用基类的构造函数
创建派生类对象时,程序首先创建基类对象。这意味着基类对象应当在程序进入派生类构造函数之前被创建。C++使用成员初始化列表语法完成这种工作,比如第一个构造函数的代码:

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

其中:TableTennisPlayer(fn, ln, ht)是成员初始化列表。它是可执行的代码,调用TableTennisPlayer构造函数。假设:RatedPlayer rplayer(1140, "Mallory", "Duck", true);,其中RatedPlayer构造函数将把 “Mallory”、"Duck"和 true存储在该对象中。然后程序进入RatedPlayer构造函数体,完成RatedPlayer对象的创建,并将参数r的值1140赋给rating成员。
如果省略成员初始化列表:

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

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

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

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

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

这里也将TableTennisPlayer的信息传递给了TableTennisPlayer构造函数TableTennisPlayer(tp)
由于tp的类型是TableTennisPlayer &,所以将调用基类的复制构造函数。饭时发现好像基类没有定义复制构造函数,其实如果需要复制构造函数但又没有定义的情况下,编译器将自动生成一个。此时执行成员复制的隐式复制构造函数是合适的,因为这个类没有使用动态内存分配
如果愿意,还可以对派生类成员使用成员初始化列表语法。此时应在列表中使用成员名,而不是类名。所以应如下编写:

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

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

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

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

13.1.3 使用派生类

类:

#ifndef TABTENN0_H_
#define TABTENN0_H_
#include <string>
using namespace std;
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 &fh = "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

方法定义:

#include <iostream>
#include "tabtenn0.h"
TableTennisPlayer::TableTennisPlayer(const string &fn,
									 const string &ln, bool ht) : firstname(fn), lastname(ln), hasTable(ht) {}
void TableTennisPlayer::Name() const
{
	cout << lastname << ", " << firstname;
}
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) {}

main函数:

#include <iostream>
#include <cstdlib>
using namespace std;
#include "tabtenn0.h"
int main()
{
	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;

	RatedPlayer rplayer2(1212, player1);
	cout << "Name: ";
	rplayer2.Name();
	cout << "; Rating: " << rplayer2.Rating() << endl;
	system("pause");
	return 0;
}
Duck, Mallory: has a table.
Boomdea, Tara: hasn't a table.
Name: Duck, Mallory; Rating: 1140
Name: Boomdea, Tara; Rating: 1212

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

有三个重要关系:

  1. 派生类对象可以使用基类对象,条件是方法不是私有的:
RatedPlayer rplayer1(1140, "Mallory", "Duck", true);
rplayer1.Name(); //派生对象使用基类方法
  1. 基类指针可以在不进行显式类型转换的情况下指向派生类对象;
  2. 基类引用可以在不进行显式类型转换的情况下引用派生类对象:
RatedPlayer rplayer1(1140, "Mallory", "Duck", true);
TableTennisPlayer &rt = rplayer;
TableTennisPlayer* pt = &rplayer;
rt.Name();
pt->Name();

然而基类指针或引用只能用于调用基类方法,因此,不能使用rt或pt来调用派生类的ResetRanking方法。
通常C++要求引用和指针类型与赋给的类型匹配,但这一规则对继承来说是例外。但这种例外是单向的,不可以将基类对象和地址赋给派生类引用和指针:

TableTennisPlayer player("Besty", "Bloop", true);
RatedPlayer &rr = player;  //不允许
RatedPlayer* pr = player;  //不允许

这个规则不无道理。例如,如果允许基类引用隐式地引用派生类对象,则可以使用基类引用为派生类对象调用基类地方法。因为派生类继承了基类的方法,所以这样做不会出问题。如果可以将基类对象赋给派生类引用,将会发生什么情况呢?派生类引用能够为基类对象调用派生类方法,这样做将出现问题。例如,将RatedPlayer::Rating()方法用于TableTennisPlayer对象是没有意义的,因为TableTennisPlayer对象没有rating成员。
如果基类引用和指针可以指向派生类对象,将出现一个有趣地结果。其中之一是基类引用定义的函数或指针参数可用于基类对象或派生类对象,比如:

void show(const TableTennisPlayer &rt)
{
	cout << "Name: ";
	rt.Name();
	cout << "\nTable: ";
	if (rt.HasTable())
		cout << "yes\n";
	else
		cout << "no\n";

形参rt是一个基类引用,它可以指向基类对象或派生类对象,所以可以在Show()中使用TableTennisPlayer参数或Ratedplayer参数:

TableTennisPlayer player1("Tara", "Boomdea", false);
RatedPlayer rplayer1(1140, "Mallory", "Duck", true);
Show(player1);
Show(rplayer1);

对于形参为指向基类的指针的函数,也存在相似的关系。它可以使用基类对象的地址或派生类对象的地址作为实参:

void Whos(const TableTennisPlayer *pt);
...
TableTennisPlayer player1("Tara", "Boomdea", false);
RatedPlayer rplayer1(1140, "Mallory", "Duck", true);
Whos(&player1);
Whos(&rplayer1);

引用兼容性属性也能让我们能够将基类对象初始化为派生类对象,假设代码:

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

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

TableTennisPlayer(const RatedPlayer &);

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

TableTennisPlayer(const TableTennisPlayer &);

形参是基类引用,因此它可以引用派生类。这样将olaf2初始化为olaf1时,将要使用该构造函数,它复制firstname、lastname和hasTable成员。换句话说,它将olaf2初始化为嵌套在RatedPlayer对象olaf1中的TableTennisPlayer对象。
同样,也可以将派生对象赋给基类对象:

RatedPlayer olaf1(1840, "Olaf", "Loaf", true);
TableTennisPlayer winner;
winner = olaf1;

在这种情况下,程序将使用隐式重载赋值运算符:

TableTennisPlayer &operator=(const TableTennisPlayer &) const;

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

13.2 继承:is-a关系

  • 公有继承建立一种is-a关系,即派生类对象也是基类对象。
  • 公有继承不建立has-a关系。比如午餐可能包括水果,但午餐不是水果。在午餐中加入水果的正确方法是将其作为一种has-a关系:即午餐有水果。
  • 公有继承不能建立is-like-a关系,即不采用明喻。比如说女人像老虎,但女人并不是老虎。继承可以在基类的基础上添加属性,但不能删除基类属性。可以设计一个包含共有特征的类,然后以is-a或has-a关系在这个类上派生相关类
  • 公有继承不建立is-implemented-as-a(作为…来实现)关系。例如可以用数组实现栈,但从Array类派生Stack类不合适,因此栈不是数组。通常让栈包含一个私有Array对象成员来隐藏数组实现。
  • 公有继承不建立uses-a关系。比如计算机能使用打印机,但从Computer派生出Printer类没有意义。但是可以用友元函数或类来处理Computer对象和Printer对象之间的通信

13.3 多态公有继承

如果希望同一个方法在派生类和基类中的行为是不同的该怎么做?这种较复杂的行为称为多态。
有两种重要的机制可用于实现多态公有继承:

  • 在派生类重新定义基类的方法
  • 使用虚方法
    现在假设一个例子:在Webtown俱乐部的我成了Pontoon银行的首席程序员。银行要我完成的第一项工作是开发两个类。一个类Brass Account用来表示基本支票账户,另一个类用于表示代表Brass Plus支票账户,它增加了透支保护特性。也就是说如果用户签出一张超出其存款余额的支票,但是超出的数额并不是很大,银行将支付这张支票,对超出的部分收取额外的费用,并追加罚款。
    Brass Account支票账户信息:
  • 客户姓名
  • 账号
  • 当前结余
    下面是可以执行的操作:
  • 创建账户
  • 存款
  • 取款
  • 显示账户信息

Pontoon银行希望Brass Plus支票账户包含Brass Account的所有信息及如下信息:

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

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

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

假设第一个类名为Brass,第二个类名为BrassPlus。那么BrassPlus满足is-a关系吗?满足,它们都保存客户姓名、账号和结余,都可以存款取款和显示信息。但是注意is-a不可逆,就像水果不是香蕉,Brass对象不具备BrassPlus对象的所有功能。

13.3.1 开发Brass类和BrassPlus类

Brass类和BrassPlus类具体实现

相关资讯

    暂无相关的资讯...

共有访客发表了评论 网友评论

验证码: 看不清楚?
    -->