类继承
第13章 类继承
-
类继承(class inheritance),从已有的类派生出新的类,而派生类继承了原有类(称为基类)的特征,包括方法。通过类继承完成的工作:
- 可以在已有类的基础上添加功能。
- 可以给类添加数据。
- 可以修改类方法的行为。
1. 一个简单的基类
class Myclass : public MyBaseClass
{
...
}
- 派生类对象的特征:
- 派生类对象存储了基类的数据成员。
- 派生类可以使用基类的方法。
- 派生类不能直接访问基类的私有成员,而必须通过基类的方法访问。
- 派生类的构造函数要点如下:
- 基类对象首先被创建。
- 派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数。
- 派生类构造函数应初始化派生类新增的数据成员。
- 派生类对象的释放顺序刚好与创建相反,首先执行派生类的析构函数,然后自动调用基类的析构函数。
2. 派生类和基类之间的特殊关系
- 派生类和基类的特殊关系:
- 派生类对象可以使用基类的方法,条件是方法不是私有的。
- 基类指针可以在不进行显式类型转换的情况下指向派生类对象。
- 基类引用可以在不进行显式类型转换的情况下引用派生类对象。
// 基类:TableTennisPlayer,派生类:RatedPlayer
RatedPlayer rplayer(1140, "Mallory", "Duck", true);
TableTennisPlayer& player1 = rplayer;
TableTennisPlayer* player2 = &rplayer;
player1.Name();
player2->Name();
- 利用引用兼容性属性,我们也能够使用派生类对象来初始化基类对象,这是因为基类的复制构造函数就是基类的引用(而且没提供复制构造函数时会自动提供)。
RatedPlayer olaf1(1840, "Olaf", "Loaf", true);
// 调用了隐式复制构造函数
TableTennisPlayer olaf2(olaf1);
3. 继承——is-a关系
- C++的3种继承方式:
- 公有继承是最常用的方式,它建立一种is-a关系,即派生类对象也是一个基类对象,可以对基类对象执行的任何操作,也可以对派生类对象执行。
4. 多态公有继承
- 实现多态公有继承的两种机制:
- 使用
virtual
,程序将根据引用或指针指向的对象的类型来选择方法,而不是引用或指针的类型。
- 方法在基类被声明为虚拟的后,它在派生类中将自动称为虚方法。
- 将源代码中的函数调用解释为执行执行特定的函数代码块被称为函数名联编(binding)。在编译过程中进行联编被称为静态联编(static binding),又称为早期联编(early binding)。由于虚函数的缘故,编译器不知道需要选择哪种类型(派生类或基类)的对象。所以,编译器必须生成能够在程序运行时选择正确的虚方法的代码,这被称为动态联编(dynamic binding),又称为晚期联编(late binding)。
- 将派生类引用或指针转换为基类引用或指针被称为向上强制转换(upcasting)。向上强制转换是可传递的。
- 将基类指针或引用转换为派生类指针或引用——称为向下强制转换(downcasting)。如果不使用显式类型转换,则向下强制转换是不允许的。
- 编译器对非虚方法采用静态联编,对虚方法采用动态联编。
- 编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员保存了一个指向函数地址数组的指针。这种数组称为虚函数表(virtual function table, vtbl)。
- 使用虚函数时在内存和速度方面的成本:
- 每个对象都将增大,增大量是存储地址的空间。
- 对每个类,编译器都将创建一个虚函数地址表(数组)。
- 每个函数调用都需要执行一步额外操作,即到表中查找地址。
- 析构函数应当是虚函数,除非类不用作基类。
// Employee是基类,Singer是派生类
Employee* p = new Singer;
...
delete p; // 如果析构函数不是虚函数,则只会调用Employee的析构函数,无法释放Singer的数据
- 友元不能是虚函数,因为友元不是类成员,只有成员才能是虚函数。
- 如果在派生类中重新定义函数,将不是使用相同的函数特征标覆盖基类声明,而是隐藏同名的基类方法,不管参数特征标如何。
// Hovel隐藏了基类的showperks(int)方法
class Dwelling
{
public:
virtual void showperks(int a) const;
};
class Hovel : public Dwelling
{
public:
// 函数特征标不同,将隐藏所有同名函数
virtual void showperks() const;
};
Hovel trump;
trump.showperks(5); // 无效,基类方法被隐藏
- 虚方法的两条经验规则:
- 如果重新定义继承的方法,应确保与原来的原型完全相同。但如果返回类型是基类引用或指针,则可以修改为指向派生类引用或指针,这种特性被称为返回类型协变(covariance of return type)。
- 如果基类声明被重载了,则应在派生类中重新定义所有的基类版本。
// 如果Hovel只定义其中一个版本,则另外两个被隐藏
class Dwelling
{
public:
virtual void showperks(int a) const;
virtual void showperks(double x) const;
virtual void showperks() const;
};
class Hovel : public Dwelling
{
public:
virtual void showperks(int a) const;
virtual void showperks(double x) const;
virtual void showperks() const;
};
5. 访问控制:protected
- 对于外部代码,保护成员的行为与私有成员相似;对于派生类,保护成员的行为与公有成员相似。
- 单例模式(singleton pattern)
class TheOnlyInstance
{
public:
static TheOnlyInstance* GetTheOnlyInstance();
// other methods
protected:
TheOblyInstance() {}
private:
// private data
};
TheOnlyInstance* TheOnlyInstance::GetTheOnlyInstance()
{
static TheOnlyInstance objTheOnlyInstance;
return &objTheOnlyInstance;
}
int main()
{
TheOnlyInstance noCanDo; // 不允许
TheOnlyInstance* pTheOnlyInstance = TheOnlyInstance::GetTheOnlyInstance();
}
6. 抽象基类
- C++通过使用纯虚函数(pure virtual function) 提供未实现的函数。纯虚函数声明的结尾为
=0
。
// 抽象基类
class BaseEllipse
{
...
public:
virtual double Area() const = 0; // 纯虚函数
}
- 当类声明包含纯虚函数时,则不能创建该类的对象。包含纯虚函数的类只能作为基类。要成为真正的抽象基类(Abstract base class, ABC),必须至少包含一个纯虚函数。
7. 继承和动态内存分配
- 继承中使用动态内存分配
- 只有基类使用动态内存:基类需实现复制构造函数,重载赋值操作符和析构函数,派生类则不需要。
- 派生类和基类都使用动态内存:派生类和基类都需要实现复制构造函数,重载赋值操作符和析构函数。
8. 类设计回顾
- 编译器自动生成的成员函数
- 默认构造函数
- 复制构造函数
- 赋值操作符
- 默认析构函数
- 类设计应注意的问题
- 构造函数:用于创建新的对象,不能被继承。
- 析构函数:使用
new
进行动态内存分配应显式定义析构函数;基类应提供虚拟析构函数。
- 转换函数:对于转换构造函数,可使用
explicit
禁止隐式转换。要将类转换为其他类型,应定义转换函数。
- 按值传递与按引用传递:一般作为函数参数,应使用按引用传递提高效率,特别是对于继承,引用可以接受派生类的对象。
- 返回对象和返回对象引用:唯一区别在于函数原型和函数头。对于返回引用,注意不能是临时对象。
- 使用
const
:用于参数修饰,则确保方法不修改参数;用于方法,则确保不修改调用它的对象(this
对应对象);用于返回类型,则确保不修改返回对象(或引用)的数据。
- 公有继承应注意的问题
- 遵循is-a关系
- 构造函数和析构函数不能被继承
- 不能继承赋值操作符
- 一般将基类的数据声明为私有,指定方法声明为保护。
- 派生类需要重新定义的方法声明为虚拟的,不需要重新定义则不必声明为虚拟的。
- 基类析构函数应当是虚拟的。
- 通过强制类型转换将派生类引用或指针转换为基类引用或指针来调用基类的友元函数。
- 类成员函数属性
函数 |
能否继承 |
成员/友元 |
可否默认生成 |
可否为虚函数 |
可否有返回类型 |
构造函数 |
否 |
成员 |
能 |
否 |
否 |
析构函数 |
否 |
成员 |
能 |
能 |
否 |
= |
否 |
成员 |
能 |
能 |
能 |
& |
能 |
任意 |
能 |
能 |
能 |
转换函数 |
能 |
成员 |
否 |
能 |
否 |
() |
能 |
成员 |
否 |
能 |
能 |
[] |
能 |
成员 |
否 |
能 |
能 |
-> |
能 |
成员 |
否 |
能 |
能 |
op= |
能 |
任意 |
否 |
能 |
能 |
new |
能 |
静态成员 |
否 |
否 |
void* |
delete |
能 |
静态成员 |
否 |
否 |
void* |
其他操作符 |
能 |
任意 |
否 |
能 |
能 |
其他成员 |
能 |
成员 |
否 |
能 |
能 |
友元 |
否 |
友元 |
否 |
否 |
能 |
网友评论