《C++ primer plus》第13章:类继承(1)


面向对象编程的主要目的之一是提供可重用的代码。开发新项目,尤其是当项目十分庞大时,重用经过测试的代码比重新编写代码要好得多。使用已有的代码可以节省时间,由于已有的代码已被使用和测试过,因此有助于避免在程序中引入错误。另外,必须考虑的细节越少,便越能专注于程序的整体策略。

传统的C函数库通过预定义、预编译的函数(如strlen() 和 rand(),可以在程序中使用这些函数)提供了可重用性。很多厂商都提供了专用的C库,这些专用库提供标准C库没有的函数。例如,可以购买数据库管理函数和屏幕控制函数库。然而,函数库也有局限性。除非厂商提供了库函数的源代码(通常是不提供的),否则您将无法根据自己特定的需求,对函数进行扩展或修改,而必须根据库的情况修改自己的程序。即使厂商提供了源代码,在修改时也有一定的风险,如不经意地修改了函数的工作方式或改变了库函数之间的关系。

C++类提供了更高层次的重用性。目前,很多厂商提供了类库,类库由类声明和实现构成。因为类组合了数据表示和类方法,因此提供了比函数库更加完整的程序包。例如,单个类就可以提供用于管理对话框的全部资源。通常,类库是以源代码的方式提供的,这意味着可以对其进行修改,以满足需求。然而,C++提供了比修改代码更好的方法来扩展和修改类。这种方法叫做类继承,它能够从已有的类派生出新的类,而派生类继承了原有类(称为基类)的特征,包括方法。正如继承一笔财产要比自己白手起家容易一样,通过继承派生出的类通常比设计新类要容易得多。下面是可以通过继承完成的一些工作。

  • 可以在已有类的基础上添加功能。例如,对于数组类,可以添加数学运算。
  • 可以给类添加数据。例如,对于字符串类,可以派生出一个类,并添加指定字符串显式颜色的数据成员。
  • 可以修改类方法的行为。例如,对于代表提供给飞机乘客的服务的 Passenger 类,可以派生出提供更高级别服务的 FirstClassPassenger 类。

当然,可以通过复制原始类代码,并对其进行修改来完成上述工作,但继承机制只需提供新特性,甚至不需要访问源代码就可以派生出类。因此,如果购买的类库只提供了类方法的头文件和编译后代码,仍可以使用库中的类派生出新的类。而且可以在不公开实现的情况下将自己的类分发给其它人,同时允许他们在类中添加新特性。

继承是一种非常好的概念,其基本实现非常简单。但要对继承进行管理,使之在所有情况下都能正常工作,则需要做一些调整。本章将介绍继承简单的一面和复杂的一面。

一个简单的基类

从一个类派生出另一个类时,原始类称为基类,继承类称为派生类。为说明继承,首先需要一个基类。Webtown 俱乐部决定跟踪乒乓球会会员。作为俱乐部的首席程序员,需要设计一个简单的 TableTennisPlayer 类。

// tabtenn.h -- a table-tennis base class
#ifndef TABTENN_H_
#define TABTENN_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
// tabtenn.cpp -- simple base-class methods
#include "13.1_tabtenn.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;
}

使用构造函数的初始化列表语法可以将原来的构造函数的步骤(先调用string的构造函数,再调用string的复制运算符),减少到一个步骤(直接使用string的复制构造函数)。

// usett.cpp -- using a base class

#include<iostream>
#include"13.1_tabtenn.h"

int main(void){
    using std::cout;
    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;
}

注意到实例化对象时将C-风格字符串作为参数,但构造函数的形参类型被声明为 const string &。这导致类型不匹配,但string类有一个将 const char * 作为参数的构造函数,使用C-风格字符串初始化对象时,将自动调用这个构造函数。总之,可将string对象或C-风格字符串作为构造函数 TableTennisPlayer的参数;将前者作为参数时,将调用接受 const string & 作为参数的 string 构造函数,而将后者作为参数时,将调用接受 const char * 作为参数的 string 构造函数。

派生一个类

Webtown 俱乐部的一些成员曾经参加过当地的乒乓球锦标赛,需要这样一个类,它能包括成员在比赛中的比分。与其从零开始,不如从 TableTennisClass 类派生出一个类。首先将 RatedPlayer 类声明从 TabbleTennisClass 类派生而来:

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

冒号指出 RatedPlayer 类的基类是 TableTennisplayer 类。上述特殊的声明头表明 TableTennisPlayer 是一个公有基类,这被称为公有派生。派生类对象包含基类对象。使用公有派生,基类的公有成员将成为派生类的公有成员;基类的私有部分也将成为派生类的一部分,但只能通过基类的公有和保护方法访问(稍后将介绍保护成员)。
上述代码完成了哪些工作呢?RatedPlayer 的对象将具有以下特征:

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

因此,Ratedplayer 对象可以存储运动员的姓名及其是否有球桌。另外,RatedPlayer 对象还可以使用 TableTennisPlayer 类的 Name()、hasTable() 和 ResetTable() 方法(参见13.1).

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

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

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

// 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 构造函数,使用一个 TableTennisPlayer 参数,该参数包括firstname、lastname 和 hasTable。

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

派生类不能直接访问基类的私有成员,而必须通过基类方法进行访问。例如,RatedPlayer 构造函数不能直接设置继承的成员(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(fn,ln,ht){
	rating = r;
}

其中 : TableTennisPlayer(fn,ln,ht) 是成员初始化列表。它是可执行的代码,调用 TableTennisPlayer 构造函数。例如,假设程序包含如下声明:

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

其中 :TableTennisPlayer(fn,ln,ht) 是成员初始化列表。它是可执行的代码,调用 TableTennisPlayer 构造函数。例如,假设程序包含如下声明:

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

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

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

RatedPlayer::RatedPlayer(unsigned int r, const string & fn, const string & ln, bool ht)
// what if no initializer list?{
	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 &, 因此将调用基类的复制构造函数。基类没有定义复制构造函数,但前面介绍过,如果需要使用复制构造函数但又没有定义,编译器将自动生成一个。在这种情况下,执行成员复制的隐式复制构造函数是合适的,因为这个类没有使用动态内存分配(string 成员确实使用了动态内存分配,但之前说过,成员复制将使用string类的复制构造函数来复制string成员)

如果愿意,也可以对派生类成员使用成员初始化列表语法。所以,第二个构造函数可以按照下述方式编写:

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

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

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

这个例子没有提供显式析构函数,因此将使用隐式构造函数。释放对象的顺序与创建对象的顺序相反,即首先执行派生类的析构函数,然后自动调用基类的析构函数。

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

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

成员初始化列表
派生类构造函数可以使用初始化器列表机制将值传递给基类构造函数。请看下面的例子:

derived::derived(type1 x, type2 y) : base(x, y) // initializer list{
	...
}

其中 derived 是派生类,base 是基类,x 和 y是基类构造函数使用的变量。例如,如果派生类构造函数接收到参数 10 和 12,则这种机制将把 10 和 12 传递给被定义为接受这些类型的参数的基类构造函数。除虚基类外(参见第14章),类只能将值传递回相邻的基类,但后者可以使用相同的机制将信息传递给相邻的基类,依此类推。如果没有在成员初始化列表中提供基类构造函数,程序将使用默认的基类构造函数。成员初始化列表只能用于构造函数。

使用派生类

要使用派生类,程序必须要能够访问基类声明。下面的程序将这两种类的声明置于同一个头文件中。也可以将每个类放在独立的头文件中。

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

派生类与基类之间有一些特殊关系。其中之一是派生类对象可以使用基类的方法,条件是方法不是私有的:

RatedPlayer rplayer1(1140, "Mallory", "Duck", true);
rplayer1.Name();	// derived object use base method

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

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

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

TableTennisPlayer player("Betsy", "Bloop", true);
RatedPlayer & rr = player; // Not Allowed
RatedPlayer & pr = player; // Not Allowed

上述规则是有道理的。例如,如果允许基类引用隐式地引用派生类对象,则可以使用基类引用为派生类对象调用基类的方法。因为派生类继承了基类的方法,所以这样做不会出现问题。如果可以将基类对象赋给派生类引用,将发生什么情况呢?派生类引用能够为基对象调用派生类方法,这样做将出现问题。例如,将 RatedPlayer::Rating() 方法用于 TableTennisPlayer 对象是没有意义的,因为 TableTennisPlayer 对象没有 rating 成员。

如果基类引用和指针可以指向派生类对象。例如,在下面的函数中:

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);		// works with TableTennisPlayer argument
Show(rplayer1);		// works with RatedPlayer argument

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

void Wohs(const TableTennisPlayer * pt);	// function with pointer parameter
...
TableTennisPlayer player1("Tara", "Boomdea", false);
RatedPlayer rplayer1(1140, "Mallory", "Duck", true);
Wohs(&player1);		// works with TableTennisPlayer * argument
Wohs(&rplayer1);	// works with RatedPlayer * argument

引用兼容属性也让您能够将基类对象初始化为派生类对象,尽管不那么直接。假设有这样的代码:

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

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

TableTennisPlayer(const RatedPlayer &);		// doesn't exist

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

// implicit copy constructor
TableTennisPlayer(const TableTennisPlayer &);

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

同样,也可以将派生对象赋给基类对象:

RatedPlayer olaf1(1840, "Olaf", 'Loaf", true);
TableTenisPlayer winner;
winner = olaf1;	// assign derived to base object

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

TableTennisPlayer & operator=(const TableTennisPlayer &) const;

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值