C++类继承
一、什么是类继承
C++提供了比修改代码更改的方法来扩展和修改类,这种方法叫类继承,它能够从已有的类派生出新的类,而派生类继承了原有类(称为基类)的特征,包括方法。
通过继承派生出的类通常比设计新类要容易的多,下面是可以通过继承完成的一些工作,如下:
(1)可以在已有类的基础上添加功能。
(2)可以给类添加数据。
(3)可以修改类方法的行为。
注: 如果购买的类库只提供了类方法的头文件和编译后代码,仍可以使用库中的类派生出新的类。而且可以在不公开实现的情况下将自己的类分发给其他人,同时允许他们在类中添加新特性。
二、类继承示例
从一个类派生出另一个类时,原始类称为基类,继承类称为派生类。具体示例如下:
// 基类
class TableTennisPlayer
{
private:
std::string firstname;
std::string lastname;
bool hastable;
public:
TableTennisPlayer(const std::string & fn = "none", const std::string & ln = "none", bool ht = false);
~TableTennisPlayer();
void Name() const;
bool HasTable() { return hastable; }
void ResetTable(bool v) {hastable = v;}
};
// 派生类
// :(冒号)后面的public TableTennisPlayer指出基类是TableTennisPlayer
class RatedPlayer : public TableTennisPlayer
{
private:
unsigned int rating;
public:
RatedPlayer(unsigned int r = 0, const std::string & fn = "none",
const std::string & ln = "none", bool ht = false);
RatedPlayer(unsigned int r, TableTennisPlayer& tp);
unsigned int Rating();
void ResetRating(unsigned int r);
};
上述继承表明派生类RatedPlayer的基类是TableTennisPlayer,而且继承时使用的关键字是public,表明TableTennisPlayer是RatedPlayer的公有基类,这被称为公有派生(公有继承)。
继承特性: 派生类对象包含基类对象。使用公有派生,基类的公有成员将成为派生类的公有成员;基类的私有部分也成为派生类的一部分,但只能通过基类的公有和保护方法访问。
上述示例中的派生类的对象将具有以下特征:
(1)派生类对象存储了基类的数据成员(派生类继承了基类的实现)
(2)派生类对象可以使用基类的方法(派生类继承了基类的接口)
派生类需要在继承特性中再增加哪些工作:
(1)派生类需要自己的构造函数
(2)派生类可以根据需要添加额外的数据成员和成员函数
三、访问权限的考虑
派生类不能直接访问基类的私有成员,而必须通过基类方法访问。例如第二节示例中的RatedPlayer派生类构造函数不能直接设置继承的基类的成员(firstname、lastname、hastable),而必须使用基类的公有方法来访问基类的私有成员。具体的说,派生类构造函数必须使用基类构造函数。
基类对象创建方式: 创建派生类对象时,程序首先创建基类对象。从概念上讲,这意味着基类对象应当在程序进入派生类构造函数之间被创建。C++使用成员初始化列表来完成这项工作。具体示例如下所示:
// 使用成员初始化列表来创建基类对象,创建派生类对象时将创建一个嵌套的基类对象
RatedPlayer::RatedPlayer(unsigned int r, const std::string &fn, const std::string &ln, bool ht)
: TableTennisPlayer(fn, ln, ht)
{
rating = r;
}
四、省略成员初始化列表将会怎样?
首先必须创建基类对象,如果不调用基类构造函数(不使用成员初始化列表调用基类构造函数),程序将使用默认的基类构造函数,示例如下:
RatedPlayer::RatedPlayer(unsigned int r, const std::string &fn, const std::string &ln, bool ht)
{
rating = r;
}
// 上述派生类的构造函数,将与下面代码等效
RatedPlayer::RatedPlayer(unsigned int r, const std::string &fn, const std::string &ln, bool ht)
: TableTennisPlayer()
{
rating = r;
}
除非要使用默认构造函数,否则应显式调用正确的基类构造函数。
五、调用基类复制构造函数的情况
第二节示例代码中RatedPlayer派生类的含有TableTennisPlayer&形参的构造函数,在使用成员初始化列表调用基类构造函数时,将调用基类的复制构造函数,代码示例如下:
// 由于形参tp的类型为TableTennisPlayer &,因此将调用基类的复制构造函数
RatedPlayer::RatedPlayer(unsigned int r, TableTennisPlayer &tp)
: TableTennisPlayer(tp)
{
rating = r;
}
上述示例代码分析:
(1)如果基类没有定义复制构造函数,编译器将自动生成一个。
(2)第二节示例代码,派生类构造函数调用基类的复制构造函数,通过默认复制构造函数进行成员复制是合适的,应为派生类没有使用动态内存分配
(3)第二节示例代码,string成员确实使用了动态内存分配,但是成员复制将使用string类的复制构造函数来复制string成员
也可以对派生类成员使用成员初始化列表语法,示例代码如下:
RatedPlayer::RatedPlayer(unsigned int r, TableTennisPlayer &tp)
: TableTennisPlayer(tp)
, rating(r)
{
}
六、派生类构造函数要点
- 首先创建基类对象
- 派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数
- 派生类构造函数应初始化派生类新增的数据成员
注,详细说明:
- 派生类构造函数总是调用一个基类的构造函数
- 创建派生类对象时,程序将首先调用基类构造函数,然后再调用派生类构造函数。
- 基类构造函数负责初始化继承的数据成员
- 派生类构造函数主要用于初始化新增的数据成员
- 派生类构造函数可以使用初始化器列表指明要使用的基类构造函数,否则将使用默认的基类构造函数
- 使用成员初始化列表方式将值传递给基类构造函数时,派生类只能将值传递给相邻的基类,但是基类又可以使用成员初始化列表方式将值传递给相邻基类,依次类推。
- 成员初始化列表只能适用于构造函数
- 派生类对象过期时,释放对象的顺序与创建对象的顺序相反,即首先执行派生类的析构函数,然后自动调用基类的析构函数
七、使用派生类
使用派生类时,程序必须要能够访问基类声明,因此可以将基类声明与派生类声明,放在同一个头文件中,因为这两个类
是相关的,所以放在一起会更合适。
八、派生类和基类之间的特殊关系
- 派生类对象可以使用基类的方法,条件是方法不是私有的
- 基类指针可以在不进行显示类型转换的情况下指向派生类对象
- 基类引用可以在不进行显示类型转换的情况下引用派生类对象
- 基类指针或引用只能用于调用基类方法,如果基类方法时虚函数,则可以调用派生类重写的此函数
C++要求引用和指针类型与赋给的类型匹配,但这一规则对继承来说是例外。然而,这种例外是单向的,不可以将基类对象和地址赋给派生类引用和指针
8.1 基类引用和指针指向派生类对象的几种情形
(1)基类引用定义的函数或指针参数可用于基类对象或派生类对象,示例代码如下
// 形参rt是一个基类引用,它可以指向基类对象或派生类对象,所以可以在show()中传递TableTennisPlayer参数或RatedPlayer参数
void show(const TableTennisPlayer &rt)
{
rt.Name();
if (rt.HasTable())
std::cout << "yes.\n";
else
std::cout << "no.\n";
}
// TableTennisPlayer参数
TableTennisPlayer player1("Tom", "Boomdea", false);
show(palyer1);
// RatedPlayer参数
RatedPlayer palyer2(1234, "Tom", "Duck", true);
show(player2);
(2)对于形参为指向基类的指针的函数,也存在与形参是引用相似的情况。他可以使用基类对象的地址或派生类对象的地址作为参数,示例代码如下:
void Wohs(const TableTennisPlayer *pt);
// TableTennisPlayer*参数
TableTennisPlayer player1("Tom", "Boomdea", false);
Wohs(&palyer1);
// RatedPlayer*参数
RatedPlayer palyer2(1234, "Tom", "Duck", true);
Wohs(&player2);
(3)引用的兼容性属性也能够将基类对象初始化为派生类对象,示例代码如下:
RatedPlayer olaf1(1234, "Tom", "Duck", true);
TableTennisPlayer olaf2(olaf1);
// 要初始化olaf2,需要匹配如下构造函数;
TableTennisPlayer(const RatedPlayer &);
// 如果类中未定义此构造函数,则使用隐式的复制构造函数
// 形参是基类引用,因此它可以使用派生类对象
// 将olaf2初始化为olaf1时,将调用如下构造函数,换句话说,它将olaf2初始化为嵌套在RatedPlayer对象olaf1中的TableTennisPlayer对象
TableTennisPlayer(const TableTennisPlayer &);
(4)可以将派生类对象赋给基类对象,示例代码如下:
RatedPlayer olaf1(1234, "Tom", "Duck", true);
TableTennisPlayer olaf2;
olaf2 = olaf1;
// 程序将调用重载赋值运算符,如果未定义此函数,将调用隐式重载赋值运算符
// olaf2 = olaf1; 调用下方函数时,基类引用指向的也是派生类对象,因此olaf1的基类部分被赋值给olaf2
TableTennisPlayer& operator=(const TableTennisPlayer &) const;