类继承
面向对象程序设计中最重要的一个概念是继承。继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。这样做,也达到了重用代码功能和提高执行效率的效果。
当创建一个类时,您不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为基类,新建的类称为派生类。
一、一个简单的基类
一个类可以派生多个类,这意味着,它可以从多个基类继承数据和函数。定义一个派生类,我们使用一个类派生列表来指定基类。类派生列表以一个或多个基类命名,形式如下:
class derived -class:access-specifier base-class
//access-specifier是:public,protected或private;
//base-class是之前定义过的某个类的名称;
//未使用访问修饰符access-specifier,则默认为private。
实例:设计一个简单的TableTennisPlayer类,如:
头文件:
#ifndef TABTENN0_H
#define TABTENN0_H
#include<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
源文件:
#include<iostream>
#include"tabtenn0.h"
using namespace std;
TableTennisPlayer::TableTennisPlayer(const string &fn,
const string &ln, bool ht):
firstname(fn),lastname(ln),hasTable(ht){
}//使用初始化列表
void TableTennisPlayer::Name()const
{
cout<<lastname<<","<<firstname;
}
源文件2:
#include<iostream>
#include"tabtenn0.h"
int main()
{
//将c-风格字符串作为参数
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对象(调用
const string &
作为参数的string构造函数)或c-风格字符串(调用const char *
作为参数的string构造函数)作为构造函数TableTennisPlayer
的参数。
二、派生类
将RatedPlayer声明为TableTennisPlayer派生的基类:
class RatedPlayer:public TableTennisPlayer
{
}
注:
- public指明这是公有派生,使用公有派生,基类的公有成员将成为派生类的公有成员;
- 私有部分也成为派生类的一部分,但只能通过基类的公有和保护方法访问。
RatedPlayer对象将具有的特征:
- 派生类对象存储了基类的数据成员(派生类继承了基类的实现);
- 派生类对象可以使用基类的方法(派生类继承了基类的接口);
- 因此,派生类可以使用基类的
Name(),hasTable()和RestTable()
方法:
3.继承特性中需要添加什么?
-
派生类需要自己的构造函数。
-
派生类可以根据需要添加额外的数据成员和成员函数。
在例子中,派生类需要另一个数据来存储比分,还应包含检索比分的方法和重置比分的方法。因此,类声明与下面相似:
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 TableTnnisPlayer & tp);//使用基类的形参
unsigned int Rating()const{
return rating;}
void ResetRating (unsigned int r){
rating = r;}
构造函数必须给新成员(如果有)和继承的成员提供数据。
三、构造函数:访问权限的考虑
-
派生类不能直接访问基类的私有成员,而必须通过基类方法访问。
-
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构造函数。
例如:假设程序声明如下
:TableTennisPlayer(fn,ln,ht)
//则ReadPlayer构造函数将把实参`"Mallor","Duck"和“true”`赋给形参`fn,ln,ht`;
//然后将这些参数作为实参传递给TableTennisPlayer构造函数,后者将创建一个嵌套TableTennisPlayer对象,并将数据存储在该对象中。
//然后,程序进入ReadPlayer构造函数体,完成RealPlayer对象的创建,并将参数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 TableTnnisPlayer & tp):TableTnnisPlayer(tp)
{
rating=r;
}
由于tp的类型为TableTennisPlayer&
,因此将调用基类的复制构造函数。基类没有定义复制构造函数。但前面讲过需要复制构造函数而没定义,编译器会自动提供一个。
也可以对派生类成员使用初始化列表,这时,应该在列表中使用成员名,而不是类名。方法如下:
RatedPlayer(unsigned int r,const TableTnnisPlayer & tp):TableTennisPlayer(tp),rating(r)
{
}
注:有关派生类构造函数的要点:
- 首先创建对象。
- 派生类构造函数应通过成员初始化列表将基类信息传递给基类的构造函数。
- 派生类构造函数应初始化派生类新增的数据成员。
四、继承中的构造和析构顺序
例:
#include <iostream>
using namespace std;
class Base
{
public:
Base()
{
cout<<"基类到的无参构造"<<endl;
}
~Base()
{
cout<<"基类的析构函数"<<endl;
}
};
class Son:public Base
{
public:
Son()
{
cout<<"派生类的无参构造"<<endl;
}
~Son()
{
cout<<"派生类的析构函数"<<endl;
}
};
int main()
{
Son ob;
}
注:
- 构造顺序:基类到派生类;
- 析构顺序:派生类到基类;
五、派生类中有基类对象成员,构造和析构顺序
例:
#include <iostream>
using namespace std;
class Other
{
public:
Other()
{
cout<<"对象成员的构造函数"<<endl;
}
~Other()
{
cout<<"对象成员的析构函数"<<endl;
}
};
class Base
{
public:
Base()
{
cout<<"基类到的无参构造"<<endl;
}
~Base()
{
cout<<"基类的析构函数"<<endl;
}
};
class Son:public Base
{
public:
Son()
{
cout<<"派生类的无参构造"<<endl;
}
~Son()
{
cout<<"派生类的析构函数"<<endl;
}
Other ob;//对象成员
};
int main()
{
Son ob;
}
注:
-
创建派生类对象时,程序首先调用基类构造函数,然后再调用派生类构造函数。
-
基类构造函数负责初始化基类的数据成员。
-
派生类构造函数主要用于初始化新增的数据成员。
-
派生类的构造函数总是调用一个基类构造函数,可以使用初始化列表语法指明要使用的基类构造函数,否则将使用默认的基类构造函数;
-
派生类对象过期时,程序首先调用派生类析构函数,然后再调用基类析构函数。
六使用派生类
要使用派生类,程序必须要能够访问基类声明。下面的程序将这两种类的声明放在同个文件中。
//头文件:
#ifndef TENN_H
#define TENN_H
#include<iostream>
#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 RestTable(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 // TENN_H
//两个类定义
#include"tenn.h"
#include<iostream>