C++程序设计 湖南大学 杜四春、银红霞
第 11章 继承和派生类
11.1 基类和派生类
11.2 单继承
11.3 多继承
11.4 虚基类
11.5 应用实例
C++程序设计 湖南大学 杜四春、银红霞
继承是面向对象程序设计的基本特征之一,
是从已有的类基础上建立新类。继承性是面向
对象程序设计支持代码重用的重要机制。面向
对象程序设计的继承机制提供了无限重复利用
程序资源的一种途径。通过 C++语言中的继承
机制,一个新类既可以共享另一个类的操作和
数据,也可以在新类中定义已有类中没有的成
员,这样就能大大的节省程序开发的时间和资
源。
C++程序设计 湖南大学 杜四春、银红霞
11.1 基类和派生类
继承是类之间定义的一种重要关系 。 定义类 B时,
自动得到类 A的操作和数据属性, 使得程序员只需定义
类 A中所没有的新成分就可完成在类 B的定义, 这样称
类 B继承了类 A,类 A派生了类 B,A是基类 ( 父类 ), B
是派生类 ( 子类 ) 。 这种机制称为继承 。
称已存在的用来派生新类的类为基类, 又称为父
类 。 由已存在的类派生出的新类称为派生类, 又称为
子类 。 派生类可以具有基类的特性, 共享基类的成员
函数, 使用基类的数据成员, 还可以定义自己的新特
性, 定义自己的数据成员和成员函数 。
在 C++语言中,一个派生类可以从一个基类派生,
也可以从多个基类派生。从一个基类派生的继承称为
单继承;从多个基类派生的继承称为多继承。图 11-1
反映了类之间继承和派生关系。
C++程序设计 湖南大学 杜四春、银红霞
B1,B2的派生类 ( 多继承 )
A
B1 B2
C1 C2 C3
A的派生类 ( 单继承 )
C3的基类
B1,B2的基类
A的派生类
C1,C2,C3的基类
B1的派生类
图 11-1 类之间的继承与派生关系
C++程序设计 湖南大学 杜四春、银红霞
11.1.1 派生类的定义格式
单继承的定义格式如下:
class<派生类名 >,<继承方式 ><基类名 >
{
public,//派生类新定义成员
members;
<private,>
members;
<protected,>
members;
};
C++程序设计 湖南大学 杜四春、银红霞
其中,<派生类名 >是新定义的一个类的名字,它
是从 <基类名 >中派生的,并且按指定的 <继承方式 >派
生的。 <继承方式 >常作用如下三种关键字给予表示:
public:表示公有继承;
private:表示私有继承, 可默认声明;
protected:表示保护继承 。
C++程序设计 湖南大学 杜四春、银红霞
多继承的定义格式如下:
class<派生类名 >, <继承方式 1><基类名 1>,<继承方式
2><基类名 2>,…
{
public,//派生类新定义成员
members;
<private,>
members;
<protected,>
members;
};
C++程序设计 湖南大学 杜四春、银红霞
11.1.2 派生类的三种继承方式
在介绍公有继承 ( public), 私有继承 ( private)
和保护继承 ( protected) 的继承方式前, 先看一个例
子 。
例 11-2:
参考上述实例, 说明公有继承 ( public), 私有继
承 ( private) 和保护继承 ( protected) 是常用的三种继
承方式:
C++程序设计 湖南大学 杜四春、银红霞
1,公有继承 ( public) 。
公有继承的特点是基类的公有成员和保护成员作
为派生类的成员时, 它们都保持原有的状态, 而基类
的私有成员仍然是私有的 。
2,私有继承 ( private) 。
私有继承的特点是基类的公有成员和保护成员作
为派生类的私有成员,并且不能被这个派生类的子类
访问。
3,保护继承 ( protected) 。
保护继承的特点是基类的所有公有成员和保护成
员都成为派生类的保护成员,并且只能被它的派生类
成员函数或友元访问,基类的私有成员仍然是私有的。
C++程序设计 湖南大学 杜四春、银红霞
继承方式 基类特性 派生类特性
公有继承
public public
protected protected
private 不可访问
私有继承
public private
protected private
private 不可访问
保护继承
public protected
protected protected
private 不可访问
表 11-1 不同继承方式的基类和派生类特性
C++程序设计 湖南大学 杜四春、银红霞
( 1)在公有继承时,派生类的对象可以访问基类
中的公有成员;派生类的成员函数可以访问基类中的
公有成员和保护成员。这里,一定要区分清楚派生类
的对象和派生类中的成员函数对基类的访问是不同的。
( 2)在私有继承时,基类的成员只能由直接派生
类访问,而无法再往下继承。
( 3)对于保护继承方式,这种继承方式与私有继
承方式的情况相同。两者的区别仅在于对派生类的成
员而言,对基类成员有不同的可访问性。
( 4)对于基类中的私有成员,只能被基类中的成
员函数和友元函数所访问,不能被其他的函数访问。
C++程序设计 湖南大学 杜四春、银红霞
11.1.3 访问控制
类通过派生定义,形成类的等级,派生类中用
“类名,,成员”访问基类成员。在建立一个类等级
后,通常创建某个派生类的对象来使用这个类等级,
包括隐含使用基类的数据和函数。
派生类对基类成员可以有不同的访问方式:
? 派生类可以覆盖基类成员
? 派生类不能访问基类私有成员
? 基类的公有段和保护段成员访问权对派生类保持
不变 (公有继承 )
? 基类的公有段和保护段成员成为派生类的私有成
员 (私有继承 )
具体说明如下:
C++程序设计 湖南大学 杜四春、银红霞
1,定义与派生类同名的成员
如果派生类定义了与基类同名的成员, 称派生类
的成员覆盖了基类的同名成员,若要在派生类中使用基
类同名成员, 可以显式地使用类名限定符:
类名,,成员
2,派生类不能访问基类私有成员
3,公有继承
派生类对基类的公有继承使用关键字 public描述。
C++程序设计 湖南大学 杜四春、银红霞
4,私有继承和保护继承
私有继承:派生类对基类的公有继承使用关键字
private 描述 ( 可缺省 ), 基类的所有公有段和保护段
成员都成为派生类的私有成员 。
保护继承:派生类对基类的公有继承使用关键字
protected 描述,基类的所有公有段和保护段成员都成
为派生类的保护成员,保护继承在程序中很少应用。
5,派生类中的静态成员
不管公有派生类还是私有派生类, 都不影响派生
类对基类的静态成员的访问, 派生类对基类静态成员
必须显式使用以下形式:
类名,,成员
C++程序设计 湖南大学 杜四春、银红霞
6,访问声明
C++提供一种调节机制,称为访问声明,使得 X
的某些成员能被类 Z 所访问。
注意:
( 1)访问声明仅仅调整名字的访问权限,不可说
明为任何类型
( 2) 访问声明不允许在派生类中降低或提升基类
成员的可访问性
( 3) 对重载函数名的访问声明将调整基类所有同
名函数的访问域
C++程序设计 湖南大学 杜四春、银红霞
11.1.4 基类与派生类的关系
任何一个类都可以派生出一个新类, 派生类也可
以再派生出新类, 因此, 基类和派生类是相对而言的 。
一个基类可以是另一个基类的派生类, 这样便形成了
复杂的继承结构, 出现了类的层次 。 一个基类派生出
一个派生类, 它又做另一个派生类的基类, 则原来的
基类为该派生类的间接基类 。
1,派生类是基类的具体化 。
2,派生类是基类定义的延续 。
3,派生类是基类的组合 。
派生类将其本身与基类区别开来的方法是添加数
据成员和成员函数 。 因此, 继承的机制将使得在创建
新类时, 只需说明新类与已有类的区别, 从而大量原
有的程序代码都可以复用 。
C++程序设计 湖南大学 杜四春、银红霞
11.2 单继承
单继承是指派生类有且只有一个基类的情况, 在
单继承中, 每个类可以有多个派生类, 但是每个派生
类只能有一个基类, 从而形成树形结构 。
11.2.1 构造函数
构造函数不能够被继承, C++提供一种机制, 使
得在创建派生类对象时, 能够调用基类的构造函数来
初始化基类数据
也就是说,派生类的构造函数必须通过调用基类
的构造函数来初始化基类子对象。所以,在定义派生
类的构造函数时除了对自己的数据成员进行初始化外,
还必须负责调用基类构造函数使基类的数据成员得以
初始化。
C++程序设计 湖南大学 杜四春、银红霞
派生类构造函数的一般格式如下:
<派生类名 >( <派生类构造函数总参数表 >), <基类构造函数 >( <
参数表 1>),
<子对象名 >( <参数表 2>)
{
<派生类中数据成员初始化 >
};
派生类构造函数的调用顺序如下:
( 1) 调用基类的构造函数, 调用顺序按照它们继
承时说明的顺序 。
( 2) 调用子对象类的构造函数, 调用顺序按照它
们在类中说明的顺序 。
( 3)派生类构造函数体中的内容。
C++程序设计 湖南大学 杜四春、银红霞
11.2.2 析构函数
由于析构函数也不能被继承,因此在执行派生类
的析构函数时,基类的析构函数也将被调用。执行顺
序是先执行派生类的析构函数,再执行基类的析构函
数,其顺序与执行构造函数时的顺序正好相反。
11.2.3 继承中构造函数的调用顺序
如果派生类和基类都有构造函数,在定义一派生
类时,系统首先调用基类的构造函数,然后再调用派
生类的构造函数。在继承关系下有多个基类时,基类
构造函数的调用顺序取决于定义派生类时基类的定义
顺序。
C++程序设计 湖南大学 杜四春、银红霞
11.2.4 应注意的问题
在实际应用中, 使用派生类构造函数时应注意如
下几个问题:
( 1)派生类构造函数的定义中可以省略对基类构
造函数的调用,其条件是在基类中必须有默认的构造
函数或者根本没有定义构造函数。当然,基类中没有
定义构造函数,派生类根本不必负责调用基类构造函
数。
( 2)当基类的构造函数使用一个或多个参数时,
则派生类必须定义构造函数,提供将参数传递给基类
构造函数途径。在有的情况下,派生类构造函数体可
能为空,仅起到参数传递作用。
C++程序设计 湖南大学 杜四春、银红霞
11.3 多继承
11.3.1 多继承的概念
可以为一个派生类指定多个基类, 这样的继承结
构称为多继承 。 多继承可以看作是单继承的扩展 。 所
谓多继承是指派生类具有多个基类, 派生类与每个基
类之间的关系仍可看作是一个继承 。
多继承下派生类的定义格式如下:
class<派生类名 >,<继承方式 1><基类名 1>,<继承方式 2><基类名
2>,…
{
<派生类类体 >
};
其中,<继承方式 1>,<继承方式 2>,… 是三种继
承方式,public,private和 protected之一。
C++程序设计 湖南大学 杜四春、银红霞
11.3.2 多继承的构造函数
在多继承的情况下, 多个基类构造函数的调用次
序是按基类在被继承时所声明的次序从左到右依次调
用, 与它们在派生类的构造函数实现中的初始化列表
出现的次序无关 。
派生类的构造函数格式如下:
<派生类名 >( <总参数表 >), <基类名 1>( <参数表 1>), <基类名
2>( <参数表 2>), … <子对象名 >( <参数表 n+1>), …
{
<派生类构造函数体 >
}
其中,<总参数表 >中各个参数包含了其后的各个
分参数表。
C++程序设计 湖南大学 杜四春、银红霞
派生类构造函数执行顺序是先执行所有基类的构
造函数, 再执行派生类本身构造函数 。 处于同一层次
的各基类构造函数的执行顺序取决于定义派生类时所
指定的各基类顺序, 与派生类构造函数中所定义的成
员初始化列表的各项顺序无关 。
多继承下派生类的构造函数与单继承下派生类构
造函数相似,它必须同时负责该派生类所有基类构造
函数的调用。同时,派生类的参数个数必须包含完成
所有基类初始化所需的参数个数。
C++程序设计 湖南大学 杜四春、银红霞
11.3.3 二义性和支配原则
一般说来, 在派生类中对基类成员的访问应该是
惟一的 。 但是, 由于多继承情况下, 可能造成对基类
中某个成员的访问出现了不惟一的情况, 则称为对基
类成员访问的二义性问题 。
1,同名成员的二义性
在多重继承中, 如果不同基类中有同名的函数,
则在派生类中就有同名的成员, 这种成员会造成二义
性 。
C++程序设计 湖南大学 杜四春、银红霞
如,class A {
public:
void f();
};
class B {
public:
void f();
void g();
};
class C,public A,public B
{
public:
void g();
void h();
};
C obj;
则对函数 f()的访问是二义的:
obj.f(); //无法确定访问 A
中或是 B中的 f()
C++程序设计 湖南大学 杜四春、银红霞
使用基类名可避免这种二义:
obj.A::f();//A中的 f();
obj.B::f();//B中的 f();
C类的成员访问 f( ) 时也必须避免这种二义 。
以上这种用基类名来控制成员访问的规则称为支
配原则 。
例如:
obj.g();//隐含用 C的 g( )
obj.B::g();//用 B的 g( )
以上两个语句是无二义的。
C++程序设计 湖南大学 杜四春、银红霞
成员的访问权限不能区分有二义性的同名成员:
class A {
public:
void fun();
};
class B {
protected;
void fun();
};
class C, public A,public B { };
虽然类 C中的两个 fun( ) 函数, 一个公有, 一个
私有, 但,C obj;
obj.fun(); //错误,仍是二义的
C++程序设计 湖南大学 杜四春、银红霞
如果同一个成员名在两个具有继承关系的类中进
行了定义,那么,在派生类中所定义的成员名具有支
配地位。在出现二义性时,如果存在具有支配地位的
成员名,那么编译器将使用这一成员,而不是给出错
误信息。以上面的代码为例,在类 A和类 B中都定义了
具有相同参数的成员函数 a,这样,尽管类 D中可以以
两种方式来解释成员函数名 a—— 即来自类 B的成员函
数 a和来自类 C的成员函数 a,但是,按照刚才所说的规
则,类 B的成员名 a相比类 A的成员名 a (即是类 C中的
成员名 a)处于支配地位,这样,编译器将调用类 B的
成员函数 a,而不产生二义性的错误。
C++程序设计 湖南大学 杜四春、银红霞
2,同一基类被多次继承产生的二义性
由于二义的原因, 一个类不能从同一类直接继承
二次或更多次 。 如:
class derived:public base,public base {… } 是错的。
如果必须这样, 可以使用中间类 。
然而,尽管可以使用作用域限定符来解决二义性
的问题,但是仍然不建议这样做,在使用多重继承机
制时,最好还是保证所有的成员都不存在二义性问题。
C++程序设计 湖南大学 杜四春、银红霞
11.3.4 赋值兼容规则
赋值兼容规则是指:在公有派生的情况下, 一个
派生类的对象可用于基类对象适用的地方 。 赋值兼容
规则有三种情况 ( 假定类 derived由类 base派生 ),
( 1) 派生类的对象可以赋值给基类的对象 。
derived d;
base b;
b=d;
( 2) 派生类的对象可以初始化基类的引用 。
derived d;
base& br=b;
C++程序设计 湖南大学 杜四春、银红霞
( 3) 派生类的对象的地址可以赋给指向基类的指
针 。
derived d;
base *pb=&d;
C++程序设计 湖南大学 杜四春、银红霞
11.4 虚基类
当某类的部分或全部直接基类是从另一个共同基
类派生而来时, 这些直接基类中从上一级基类继承来
的成员就拥有相同的名称, 也就是说, 这些同名成员
在内存中存在多个副本 。 而多数情况下, 由于它们的
上一级基类是完全一样的, 在编程时, 只需使用多个
副本的任一个 。
C++语言允许程序中只建立公共基类的一个副本,
将直接基类的共同基类设置为虚基类,这时从不同路
径继承过来的该类成员在内存中只拥有一个副本,这
样有关公共基类成员访问的二义性问题就不存在了。
C++程序设计 湖南大学 杜四春、银红霞
11.4.1 虚基类的引入
如果一个派生类从多个基类派生,而这些基类又
有一个共同的基类,则在对该基类中声明的名字进行
访问时,可能产生二义性。
引进虚基类的真正目的是为了解决二义性问题。
当基类被继承时,在基类的访问控制保留字的前面加
上保留字 virtual来定义。
如果基类被声明为虚基类,则重复继承的基类在
派生类对象实例中只好存储一个副本,否则,将出现
多个基类成员的副本。
C++程序设计 湖南大学 杜四春、银红霞
虚基类说明格式如下:
virtual<继承方式 ><基类名 >
其中,virtual是虚基类的关键字。虚基类的说明是
用在定义派生类时,写在派生类名的后面。
引进虚基类后,派生类(即子类)的对象中只存
在一个虚基类的子对象。当一个类有虚基类时,编译
系统将为该类的对象定义一个指针成员,让它指向虚
基类的子对象。该指针被称为虚基类指针。
C++程序设计 湖南大学 杜四春、银红霞
注意:如果一个派生类从多个基类派生, 而这些
基类又有一个共同的基类, 则在对该基类中声明的名
字进行访问时, 可能产生二义性 。 如果在多条继承路
径上有一个公共的基类, 那么在继承路径的某处汇合
点, 这个公共基类就会在派生类的对象中产生多个基
类子对象 。
要使这个公共基类在派生类中只产生一个子对象,
必须将这个基类声明为虚基类。虚基类声明使用关键
字 virtual。
C++程序设计 湖南大学 杜四春、银红霞
类之间的继承关系可以用一个有向无环图( DAG)
表示,称为类格。一个类在类格中,既可以被用作虚
基类,也可以被用作非虚基类。在派生类的对象中,
同名的虚基类只产生一个虚基类子对象,而每个非虚
基类产生各自的子对象。虚基类的构造函数按被继承
的顺序构造,建立虚基类的子对象时,虚基类构造函
数仅被调用一次。
C++程序设计 湖南大学 杜四春、银红霞
11.4.2 虚基类的构造函数
为了初始化基类的子对象,派生类的构造函数要
调用基类的构造函数。对于虚基类来讲,由于派生类
的对象中只有一个虚基类子对象。为保证虚基类子对
象只被初始化一次,这个虚基类构造函数必须只被调
用一次。由于继承结构的层次可能很深,规定将在建
立对象时所指定的类称为最直接派生类。虚基类子对
象是由最直接派生类的构造函数通过调用虚基类的构
造函数进行初始化的。如果一个派生类有一个直接或
间接的虚基类,那么派生类的构造函数的成员初始列
表中必须列出对虚基类构造函数的调用,如果未被列
出,则表示使用该虚基类的默认构造函数来初始化派
生类对象中的虚基类子对象。
C++程序设计 湖南大学 杜四春、银红霞
C++规定, 在一个成员初始化列表中出现对虚基
类和非虚基类构造函数的调用, 则虚基类的构造函数
先于非虚基类的构造函数执行 。
从虚基类直接或间接继承的派生类中的构造函数
的成员初始化列表中都要列出这个虚基类构造函数的
调用。但是,只有用于建立对象的那个派生类的构造
函数调用虚基类的构造函数,而该派生类的基类中所
列出的对这个虚基类的构造函数调用在执行中被忽略,
这样便保证了对虚基类的子对象只初始化一次。
C++程序设计 湖南大学 杜四春、银红霞
11.5 应用实例
例 11-19
例 11-20