为什么继承
面向对象编程的主要目的之一是提供可重用的代码。传统的C函数库通过预定义、预编译函数实现。C++通过类继承实现,累继承的大致功能:
- 可以在已有的类的基础上添加功能
- 可以添加数据
- 修改类方法的行为
基类&派生类
从一个类派生出另一个类时,原始类称为基类,继承类称为派生类。
首先定义一个基类,表示乒乓球会会员。
// 乒乓球会会员类
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; };
};
TableTennisPlayer
类只是记录会员的姓名以及是否使用球桌。
定义一个派生类表示会员在比赛中的比分。
// 由TableTennisPlayer派生
class RatePlayer:public TableTennisPlayer
{
...
};
冒号表示 RatePlayer类 的基类是 TableTennisPlayer类 ,public表明这是一个公有继承。
- 派生类需要有自己的构造函数
- 派生类可以根据需要添加额外的数据成员和成员函数
// 基类指针可以在不进行显式类型转换的情况下指向派生类对象
TableTennisPlayer* p2 = p1;
// 基类引用可以在不进行显式类型转换的情况下引用派生类对象
TableTennisPlayer& p2 = p1;
// 基类指针或引用只能调用基类方法
继承类型
通常使用 public 继承,一般不使用 protected 或 private 继承。当使用不同类型的继承时,遵循以下几个规则:
- 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。
- 保护继承(protected): 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员。
- 私有继承(private):当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员。
构造函数及析构函数
析构函数
派生类构造函数主要有以下几点:
- 首先创建基类对象
- 如果是多层继承关系,一直向上首先创建基类对象
- 派生类构造函数通过成员初始化列表将基类信息传递给基类构造函数(直接调用基类的拷贝构造函数,避免再进行赋值运算)
- 派生类构造函数应该初始化派生类新增的数据成员
基类中的析构函数要定义成虚函数
C++类有继承时,析构函数必须为虚函数。如果不是虚函数,则使用时可能存在内在泄漏的问题。
首先,我们以这种方式继承:
RatePlayer* p1 = new RatePlayer();
delete p1;
在这种情况下,不管析构函数是否是虚函数(即是否加virtual关键词),delete时基类和子类都会被释放。
接下来,这样继承试试看:
TableTennisPlayer* p1 = new RatePlayer();
delete p1;
在这种情况下,基类的析构函数是否非虚函数就出现了差异:
- 若析构函数不是虚函数,delete时只释放基类,不释放子类。
- 若析构函数是虚函数,delete时基类和子类都会被释放。
同名隐藏
在类的派生层次结构中,基类的成员和派生类新增的成员都具有类作用域,二者的作用范围不同。
这时,如果派生类声明了一个和某个基类成员同名的新成员,派生的新成员就隐藏了基类同名成员,直接使用成员名只能访问到派生类的成员。
如果派生类中声明了与基类同名的新函数,即使函数的参数表不同,从基类继承的同名函数的所有重载形式也都被隐藏。如果要访问被隐藏的成员,就需要使用类作用域分辨符和基类名来限定。
- 成员变量:与类型无关
- 成员函数:与原型无关
作用域分辨符,就是“::”,它可以用来限定要访问的成员所在的类的名称。一般的使用形式是:
// 对象.类名::函数名;
p1.TableTennisPlayer::xxx();
单继承
一个子类只有一个直接父类时称这个继承关系为单继承。
以上两种方式都是单继承,都只有一个直接父类。
多继承
一个子类有两个或以上直接父类时称这个继承关系为多继承。
声明时基类之间用逗号分隔。
菱形继承
当愉快的使用单继承和多继承时,可能会出现这样的问题:
编译器报错,这是我们常说的 “二义性” 问题。
怎么解决这个问题呢?
虚拟继承
C++使用虚拟继承,解决从不同途径继承来的同名的数据成员在内存中有不同的拷贝造成数据不一致问题,将共同基类设置为虚基类。这时从不同的路径继承过来的同名数据成员在内存中就只有一个拷贝,同一个函数名也只有一个映射。
简单的来说就是在继承共同的基类时加上 virtual
关键字。
class B:virtual public A
{
public:
int _b;
};
class C :virtual public A
{
public:
int _c;
};
现在来看看,没加 virtual
是这样的,class A
在B类和C类中各保存了一份。
加上 virtual
后,保存类相对于D类的偏移量,通过偏移量就知道使用哪个类,不仅解决了二义性的问题,同时减少了内存的占用。
不能被继承的类
回顾继承知识点,只有友元是不能被继承的。所以我们需要构造一个友元来。在使用模板时,继承就需要用到友元。
#include<iostream>
using namespace std;
template<typename T>
class A
{
friend T;
private:
A()
{
cout << "123";
}
};
class B :virtual public A<B>
{
public:
B()
{
cout << "1212";
}
};
代码中 friend T
如果不包含的话会报错“A<B>::A”: 无法访问 private 成员(在“A<B>”类中声明)
,因为已经把基类的构造函数定义为私有的了,只有通过友元才能访问。到这里思路就清晰了,那么 B 类一定就是那个不能被继承的类了。
没错,当一个类继承了 B类之后,因为 B 类继承自 A 类,所以新的派生类就要到 A 类中寻找构造函数,但是 A 类的构造函数是私有的呀,派生类调不到,所以这样的派生类是不存在的,那么 B 类就是不能被继承的类了。