C++基础学习之类继承(10)

面向对象编程的主要目的之一是提供可重用的代码。C++提供了更高层次的重用性方法来扩展和修改类。这种方法叫类继承,它能够从已有的类派生出新的类,而派生类继承了原有类(称为基类)的特征,包括方法。通过继承可完成的工作有:

  • 可以在已有类的基础上添加功能。例如,对于数组类,可以添加数学运算。
  • 可以给类添加数据。
  • 可以修改类方法的行为。

一个简单的基类(最简单的继承和初始化)

从一个类派生出另一个类时,原始类称为基类,继承类称为派生类。先设计一个简单的基类用于管理乒乓球会员。

// tabtenn0.h -- a table-tennis bass class
#ifndef TABTENN0_H_
#define TABTENN0_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; };
};
#endif
// tabtenn0.cpp -- simple base-class method
#include "tabtenn0.h"
#include <iostream>

TableTennisPlayer::TableTennisPlayer(const string & fn,
	const string & ln, bool ht) : firstname(fn), lastname(ln), hasTable(ht) {}

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

派生一个类

现在需要一个类表示参加过比赛的会员,这样就可以在会员的基类上扩展出来。语法如下:

// RatedPlyer derives from the TableTennisPlayer base class
class RatedPlayer : public TableTennisPlayer
{
...
};

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

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

继承特性中可以添加的部分:

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

所以RatedPlyer类应该如下声明:

// simple derived class
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 r, const TableTennisPlayer & tp);
	unsigned int Rating() const { return rating; }	// add a method
	void ResetRating(unsigned int r) { rating = r; }	// add a method
};

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

构造函数:访问权限的问题

派生类不能直接访问基类的私有成员,而必须通过基类方法进行访问。具体来说就是派生类构造函数必须使用基类构造函数。创建派生类对象时,程序首先创建基类对象。从概念上说,这意味着基类对象应当在程序进入派生类构造函数之前被创建。C++使用成员初始化列表语法来完成这种工作。例如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构造函数。
如果省略成员初始化列表,那么程序会使用默认基类构造函数,等同于:

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

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

  • 首先创建基类对象;
  • 派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数;
  • 派生类构造函数应初始化派生类新增的数据成员。
    另外,释放对象的顺序与创建对象的顺序相反,即首先执行派生类的析构函数,然后自动调用基类的析构函数。

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

派生类与基类之间有一些特殊的关系。一是派生类对象可以使用基类的公有方法;二是基类指针可以在不进行显式类型转换的情况下指向派生类对象;三是基类引用可以在不进行显式类型转换的情况下引用派生类对象。而且基类指针或引用只能用于调用基类方法。

多态公有继承

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

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

这里使用新的一个例子说明:

// brass.h -- bank account classes
#ifndef BRASS_H_
#define BRASS_H_
#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 Balance() 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; }
};
#endif

这个程序需要说明几点:

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

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

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

同样,Withdraw()也有2个版本,一个供Brass对象使用,另一个供BrassPlus对象使用。对于在两个类中行为相同的方法(如Deposit()和Balance()),则只在基类中声明。
第三点(使用virtual)比前两点要复杂。如果方法是通过引用或指针而不是对象调用的,它将确定使用哪一种方法。如果没有使用关键字virtual,程序将根据引用类型或指针类型选择方法;如果使用了virtual,程序将根据引用或指针指向的对象的类型来选择方法。如果ViewAcct()不是虚的,则程序的行为如下:

// behavior with non-virtual ViewAcct()
// method chosen according to reference type
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 Brass::ViewAcct()

引用变量的类型为Brass,所以选择了Brass::ViewAccount()。使用Brass指针代替引用时,行为将与此类似。
如果ViewAcct()是虚的,则行为如下:

// behavior with virtual ViewAcct()
// method chosen according to object type
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 BrassPlus::ViewAcct()

这里两个引用的类型都是Brass,但b2_ref引用的是一个BrassPlus对象,所以使用的是BrassPlus::ViewAcct()。使用Brass指针代替引用时,行为将类似。
第四点是,基类声明了一个虚析构函数。这样做是为了确保释放派生对象时,按正确的顺序调用析构函数。基类应该包含一个虚析构函数,如果析构函数不是虚的,那么如果用基类类型来保存派生类类型,那么最后将只调用基类的析构函数,而如果基类的析构函数是虚的,那么将调用相应的指向类型的虚构函数,也就是说将先调用派生类的析构函数,然后自动调用基类的析构函数。
注意: 如果要在派生类中重新定义基类的方法,通常应将基类方法声明为虚的。这样,程序将根据对象类型而不是引用或指针的类型来选择方法版本。为基类声明一个虚析构函数也是一种惯例。

猜你喜欢

转载自blog.csdn.net/x603560617/article/details/83991226