本篇内容包括:
is-a
关系的继承- 如何以公有方式从一个类派生出另一个类
- 保护访问
- 构造函数成员初始化列表
- 向上和向下强制转换
- 虚成员函数
- 早期(静态)联编与晚期(动态)联编
- 抽象基类
- 纯虚函数
- 何时及如何使用公有继承
第13章 类继承
13.1 一个简单的基类
13.1.1 派生一个类
- 使用公有派生,基类的公有成员将成为派生类的公有成员;基类的私有部分也将成为派生类的一部分,但只能通过基类的公有和保护方法访问
13.1.2 构造函数:访问权限的考虑
- 派生类构造函数必须使用基类构造函数(通过成员初始化列表语法来完成这种工作)
- 除非要使用默认构造函数,否则应显式调用正确的基类构造函数
13.1.3 使用派生类
- 要使用派生类,程序必须要能够访问基类声明。一般将两个类的声明放在同一个头文件中
13.1.4 派生类和基类之间的特殊关系
- 派生类对象可以使用基类的方法,条件是方法不是私有的
- 基类指针可以在不进行显式类型转换的情况下指向派生类对象
- 基类引用可以在不进行显式类型转换的情况下引用派生类对象
- 当然,基类指针或引用只能用于调用基类方法
13.2 继承:is-a
关系
- C++ 有3种继承方式:公有继承、保护继承和私有继承
- 公有继承是最常用的方式
- 它建立一种
is-a
关系,即派生类对象也是一个基类对象,可以对基类对象执行的任何操作,也可以对派生类对象执行 - 公有继承不建立
has-a
关系。如应将Fruit
对象作为Lunch
类的数据成员而不是使用继承 - 公有继承不能建立
is-like-a
关系。继承可以在基类的基础上添加属性,但不能删除基类的属性。在有些情况下,可以设计一个包含共有特征的类,然后以is-a
或has-a
关系,在这个类的基础上定义相关的类 - 公有继承不建立
is-implemented-as-a
关系。例如,可以使用数组来实现栈,但从Array
类派生出Stack
类是不合适的,因为栈不是数组 - 公有继承不建立
uses-a
关系。可以使用友元函数或类来处理Printer
对象和Computer
对象之间的通信而不是继承
- 它建立一种
13.3 多态公有继承
13.3.1 开发 Brass
类和 BrassPlus
类
virtual
关键字不能在class
之外出现,写虚方法的定义时不必写virtual
13.4 静态联编和动态联编
- 将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编(
binding
)- 在编译过程中进行联编被称为静态联编(
static binding
),又称为早期联编(early binding
) - 编译器生成能够在程序运行时选择正确的虚方法的代码,这被称为动态联编(
dynamic binding
),又称为晚期联编(late binding
)
- 在编译过程中进行联编被称为静态联编(
13.4.1 指针和引用类型的兼容性
- 将派生类引用或指针转换为基类引用或指针被称为向上强制转换(
upcasting
),这使公有继承不需要进行显式类型转换。该规则是is-a
关系的一部分
13.4.2 虚成员函数和动态联编
- 为什么有两种类型的联编以及为什么默认为静态联编
- 效率:为使程序能够在运行阶段进行决策,必须采取一些方法来跟踪基类指针或引用指向的对象类型,这增加了额外的处理开销
- 概念模型:非虚函数指出不要在派生类里重新定义该函数
- 虚函数的工作原理
- C++ 规定了虚函数的行为,但将实现方法留给了编译器作者
- 通常,编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这种数组称为虚函数表(
virtual function table
,vtbl
) - 调用虚函数时,程序将查看存储在对象中的
vtbl
地址,然后转向相应的函数地址表。如果使用类声明中定义的第 n 个虚函数,则程序将使用数组中的第 n 个函数地址,并执行具有该地址的函数。 - 所以,使用虚函数的额外成本为:
- 每个对象都将增大,增大量为存储地址的空间
- 对于每个类,编译器都创建一个虚函数地址表(数组)
- 对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址
13.4.3 有关虚函数注意事项
- 构造函数不能是虚函数。(由于派生类不继承基类的构造函数,所以将类构造函数声明为虚函数没什么意义)
- 析构函数应当是虚函数,除非类不用做基类。因为考虑一个向上强制转换的指针,如果使用默认的静态联编,
delete
语句将只调用基类的析构函数;如果使用动态联编,则会先调用派生类的析构函数,再隐式调用基类的析构函数- 这意味着,即使基类不需要显式析构函数提供服务,也不应依赖于默认构造函数,而应提供虚析构函数
- 友元不能是虚函数,因为友元不是类成员,而只有成员才能是虚函数。如果由于这个原因引起了设计问题,可以通过让友元函数使用虚成员函数来解决
- 如果派生类没有重新定义函数,将使用该函数的基类版本。如果派生类位于派生链中,则将使用最新的虚函数版本
- 上一条的例外情况是基类版本是隐藏的
- 如果在派生类中重新定义函数,将隐藏同名的基类方法,不管参数特征标如何
- 这引出了两条经验规则
- 如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针(这种例外是新出现的)。这种特性被称为返回类型协变(
covariance of return type
),因为允许返回类型随类类型的变化而变化 - 如果基类声明被重载了,则应在派生类中重新定义所有的基类版本
- 如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针(这种例外是新出现的)。这种特性被称为返回类型协变(
- 即使基类的同名方法被隐藏,当基类的指针指向派生类时,调用的方法依旧是基类的,除非这个方法是虚函数
- 如果想不被隐藏,可以引用名称空间
1
2
3
4
5class Derived : public Base {
public:
using Base::Foo();
void Foo(int);
};
13.5 访问控制:protected
- 在类外只能用公有类成员来访问
protected
部分中的类成员,但在派生类的成员可以直接访问基类的保护成员 - 最好对类数据成员采用私有访问控制,不要使用保护访问控制;同时通过基类方法使派生类能够访问基类数据
13.6 抽象基类
- 抽象基类:(
abstract base class
,ABC
)- 程序中不能建立抽象基类的对象
- 比如圆和椭圆是
is-a
的关系,但是圆从椭圆中派生出来会有信息冗余。此时常常会构造一个抽象基类,里面有圆和椭圆的共性
- C++通过使用纯虚函数(
pure virtual function
)提供未实现的函数。纯虚函数声明的结尾处为= 0
。当然,纯虚函数也可以有定义(作为派生类的缺省虚函数)。总之,纯虚函数的作用是指出这个类是抽象基类1
2
3
4class ABC {
virtual void Foo() const = 0;
};
void ABC::Foo() const { /* details omitted */ }
13.7 继承和动态内存分配
13.7.1 第一种情况:派生类不使用 new
- 默认复制构造函数执行的成员复制将根据数据类型采用相应的复制方式,因此,复制类成员或继承的类组件时,是使用该类的复制构造函数完成的
13.7.2 第二种情况:派生类使用 new
- 在这种情况下,必须为派生类定义显式析构函数、复制构造函数和赋值运算符
- 派生类析构函数自动调用基类的析构函数,故其自身的职责是对派生类构造函数执行工作的进行清理
- 复制构造函数使用成员初始化列表来进行基类部分的复制构造
- 显式赋值运算符可以通过显式调用基类赋值运算符来进行基类部分的赋值
1
2
3
4
5
6
7
8
9
10class Bar : public Foo {
public:
Foo & operator=(const Foo &rhs) {
if (this == &rhs)
return *this;
Foo::operator=(rhs);
/* details omitted */
return *this;
}
};
13.7.3 使用动态内存分配和友元的继承示例
- 因为友元不是成员函数,所以不能使用作用域解析运算符来指出要使用哪个函数。这个问题的解决方法是使用强制类型转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14class Foo {
public:
friend std::ostream & operator<<(std::ostream &os, const Foo &rhs);
};
class Bar : public Foo {
public:
friend std::ostream & operator<<(std::ostream &os, const Bar &rhs);
};
std::ostream & operator<<(std::ostream &os, const Foo &rhs) { /* details omitted */ }
std::ostream & operator<<(std::ostream &os, const Bar &rhs) {
os << (const Foo &) rhs;
/* details omitted */
return os;
}
13.8 类设计回顾
13.8.3 公有继承的考虑因素
- 按引用传值时,虚函数等动态联编相关特性会得以保留;但按值传递时,由于进行了类型转换,派生类的派生内容将全部丢失
13.8.4 类函数小结
- 成员函数属性
函数 能否继承 成员还是友元 默认能否生成 能否为虚函数 是否可以有返回类型 构造函数 否 成员 能 否 否 析构函数 否 成员 能 能 否 =
否 成员 能 能 能 &
能 任意 能 能 能 转换函数 能 成员 否 能 否 ()
能 成员 否 能 能 []
能 成员 否 能 能 ->
能 成员 否 能 能 op=
(如+=
)能 任意 否 能 能 new
能 静态成员 否 否 void*
delete
能 静态成员 否 否 void
其他运算符 能 任意 否 能 能 其他成员 能 成员 否 能 能 友元 否 友元 否 否 能