第 7章 继承与派生第 7章 继承与派生
7.1 继承与派生
7.2 多继承
7.3 类的继承方式
7.4 派生类的构造和析构函数
7.5 派生中成员的标识与访问
7.6 虚基类第 7章 继承与派生
7.1 继承与派生
7.1.1 继承与派生的概念在面向对象的程序设计中,大量使用继承和派生。
例如,我们要定义不同的窗口,因为窗口都具有共同的特征,如窗口标题,窗口边框及窗口最大、最小等,
我们不需要也没有必要将每一个窗口定义一遍。这时我们可以先定义一个窗口类(系统的类库中已存在),
然后以这个窗口类作为基类派生出其它不同的窗口类。
第 7章 继承与派生所谓继承,就是新的类从已有类那里得到已有的特性 。 从另一个角度来看,从已有类产生新类的过程就是类的派生 。 已有的类称为基类或父类,产生的新类称为派生类或子类 。 派生类同样也可以作为基类再派生新的类,这样就形成了类的层次结构 。
第 7章 继承与派生类的继承和派生的层次结构,可以说是人们对自然界中的事物进行分类、分析和认识的过程在程序设计中的体现。现实世界中的事物都是相互联系、相互作用的,人们在认识过程中,根据事物的实际特征,
抓住其共同特性和细小差别,利用分类的方法进行分析和描述。例如,对于交通工具的分类见图 7-1。
第 7章 继承与派生图 7-1 交通工具分类层次图第 7章 继承与派生这个分类树反映了交通工具的派生关系。最高层是抽象程度最高的交通工具,是最具有普遍和一般意义的概念。下层(火车、汽车、飞机和轮船)具有了上层交通工具的特性,同时加入了自己的新特征。而最下层是最为具体的,例如汽车又可分为卡车、小汽车和旅游车。在这个层次结构中,由上到下,是一个具体化、特殊化的过程;由下到上,是一个抽象化的过程。上下层之间的关系就可以看作是基类与派生类的关系。
第 7章 继承与派生类的派生实际是一种演化、发展过程,即通过扩展、更改和特殊化,从一个已知类出发建立一个新类。
通过类的派生可以建立具有共同关键特征的对象家族,
从而实现代码的重用。这种继承和派生的机制对于已有程序的发展和改进是极为有利的。
第 7章 继承与派生
7.1,2 派生类的声明在 C++中,派生类的一般声明语法如下,
class<派生类名 >:[继承方式 ]<基类名 >
{
派生类成员声明;
};
其中:
① class是类声明的关键字,用于告诉编译器下面声明的是一个类 。
② 派生类名是新生成的类名 。
第 7章 继承与派生
③ 继承方式规定了如何访问从基类继承的成员 。
继承方式关键字为 private,public和 protected,分别表示私有继承,公有继承和保护继承 。 如果不显式地给出继承方式关键字,系统的默认值就认为是私有继承
( private) 。 类的继承方式指定了派生类成员以及类外对象对于从基类继承来的成员的访问权限,这将在 7.3
节中详细介绍 。
第 7章 继承与派生
④ 派生类成员指除了从基类继承来的所有成员之外,新增加的数据和函数成员 。 这些新增的成员正是派生类不同于基类的关键所在,是派生类对基类的发展 。 当重用和扩充已有的代码时,就是通过在派生类中新增成员来添加新的属性和功能 。 可以说,这就是类在继承基础上的进化和发展 。
第 7章 继承与派生例如,从基类 vehicle( 汽车 ) 公有派生 car( 小汽车 ) 类的声明形式如下:
classvehicle //基类 vehicle类的声明
{
private,//私有数据成员
intwheels;
floatweight;
public,//公有函数成员第 7章 继承与派生
voidinitvehicle(intin_wheels,floatin_weight);
intget_wheels();
floatget_weight();
//...
};
classcar:publicvehicle //派生类 car类的声明
{
private,//新增私有数据成员第 7章 继承与派生
intpassenger_load;
public,//新增公有函数成员
voidinitcar(intin_wheels,floatin_weight,intpeople=4);
intget_passengers();
//...
};
第 7章 继承与派生
7.1.3 派生类生成过程在 C++程序设计中,进行派生类的声明,给出该类的成员函数的实现之后,整个类就算完成了,这时就可以由它来生成对象进行实际问题的处理。仔细分析派生新类这个过程,实际是经历了三个步骤:吸收基类成员,改造基类成员和添加新的成员。
第 7章 继承与派生面向对象的继承和派生机制,其最主要的目的是实现代码的重用和扩充 。 因此,吸收基类成员就是一个重用的过程,而对基类成员进行调整,改造以及添加新成员就是原有代码的扩充过程,二者是相辅相成的 。
下面以某公司人员管理系统为例,分别对这几个步骤进行解释 。 基类 employee和派生类 technician声明如下,
类的实现部分略去 。
第 7章 继承与派生
classemployee
{
protected:
char*name; //姓名
intindividualEmpNo; //个人编号
intgrade; //级别
floataccumPay; //月薪总额
staticintemployeeNo; //本公司职员编号目前最大值第 7章 继承与派生
public:
employee();
~employee();
voidpay(); //计算月薪函数
voidpromote(int); //升级函数
voiddisplayStatus(); //显示人员信息
};
classtechnician:publicemployee
第 7章 继承与派生
{
private:
floathourlyRate; //每小时酬金
intworkHours; //当月工作时数
public:
technician(); //构造函数
voidpay(); //计算月薪函数
voiddisplayStatus(); //显示人员信息
};
第 7章 继承与派生
1,吸收基类成员在类继承中,第一步是将基类的成员全盘接收,
这样派生类实际上就包含了它的所有基类中除构造和析构函数之外的所有成员。注意,在派生过程中,构造函数和析构函数都不被继承。这一点将在 7.4节中详细介绍。这里派生类 technician继承了基类 employee中除构造和析构函数之外的所有成员,name,
individualEmpNo,grade,accumPay,employeeNo,
pay(),promote(int),displayStatus()。经过派生过程,
这些成员便存在于派生类之中。
第 7章 继承与派生
2,改造基类成员对基类成员的改造包括两个方面。第一是基类成员的访问控制,主要依靠派生类声明时的继承方式来控制,将在 7.3节中详细介绍。第二是对基类数据或函数成员的覆盖,就是在派生类中声明一个和基类数据或函数同名的成员,例如,上例中的 pay()和
displayStatus()。如果派生类声明了一个和某个基类成员同名的新成员 (如果是成员函数,则参数表也要相同,
参数不同的情况属于重载 ),派生的新成员就覆盖了外层同名成员。
第 7章 继承与派生这时,在派生类中或者通过派生类的对象直接使用成员名就只能访问到派生类中声明的同名成员,这称为同名覆盖 。 在上例的程序中,派生类 technician中的
pay()和 displayStatus()函数就覆盖了基类 employee中的同名函数 。
第 7章 继承与派生
3,添加新的成员派生类新成员的加入是继承与派生机制的核心,
是保证派生类在功能上有所发展的关键。我们可以根据实际情况的需要,给派生类添加适当的数据和函数成员,以实现必要的新增功能。这里派生类 technician
中就添加了数据成员 hourlyRate和 workHours。
第 7章 继承与派生
7.2 多继承
7.2.1多继承的声明在派生类的声明中,基类名可以有一个,也可以有多个 。 如果只有一个基类名,则这种继承方式称为单继承;如果基类名有多个,则这种继承方式称为多继承,
这时的派生类同时得到了多个已有类的特征 。 在多继承中,各个基类名之间用逗号隔开 。 多继承的声明语法如下:
class<派生类名 >:[继承方式 ]基类名 1,[继承方式 ]基类名 2,...,[继承方式 ]基类名 n
第 7章 继承与派生
{
派生类成员声明;
};
例如,假设基类 Basel,Base2是已经声明的类,下面的语句声明了一个名为 MultiDerived的派生类,该类从基类 Basel,Base2派生而来 。
classBase1
{
//...
};
classBase2
第 7章 继承与派生
{
//...
};
classMultiDerived:publicBase1,privateBase2
{
public:
MultiDerived();
~MultiDerived();
//...
};
第 7章 继承与派生声明中的,基类名,( 如 Basel,Base2) 是已有类的名称,,派生类名,是继承原有类的特性而生成的新类的名称 ( 如 MultiDerived) 。
多继承和单继承时,基类和派生类之间的关系可以用图 7-2描述 。 单继承可以看作是多继承的一个最简单的特例,多继承可以看作是多个单继承的组合,它们之间的很多特性是相同的 。
第 7章 继承与派生图 7-2 多继承和单继承
(a)多继承 (b)单继承第 7章 继承与派生
7.2.2 类族在派生过程中,派生出来的新类也同样可以作为基类再继续派生新的类。此外,一个基类可以同时派生出多个派生类。也就是说,一个类从父类继承来的特征也可以被其它新的类所继承,一个父类的特征,
可以同时被多个子类继承。这样就形成了一个相互关联的类的家族,称为类族。在类族中,直接参与派生出某类的基类称为直接基类;基类的基类甚至更高层的基类称为间接基类。
第 7章 继承与派生图 7-3所示为一个单继承的多层类族,其中 A类派生出 B类,B类又派生出 E类,则 B类是 E类的直接基类,
A类是 B类的直接基类,而 A类可以称为 E类的间接基类 。
在使用多继承时,对于具有相同名字成员的访问要注意二义性 。 这部分内容将在本章 7.5节中详细介绍 。
第 7章 继承与派生图 7-3 单继承类族示意图第 7章 继承与派生
7.3 类的继承方式在面向对象程序中,基类的成员可以有 public( 公有 ),protected( 保护 ) 和 private( 私有 ) 三种访问属性 。 在基类内部,自身成员可以对任何一个其它成员进行访问,但是通过基类的对象,就只能访问基类的公有成员 。
第 7章 继承与派生派生类继承了基类的全部数据成员和除了构造,
析构函数之外的全部函数成员,但是这些成员的访问属性在派生的过程中是可以调整的 。 从基类继承的成员,其访问属性由继承方式控制 。
类的继承方式有 public( 公有 ) 继承,protected
( 保护 ) 继承和 private( 私有 ) 继承三种 。 对于不同的继承方式,会导致基类成员原来的访问属性在派生类中有所变化 。 表 7-1列出了不同继承方式下基类成员各访问属性的变化情况 。
第 7章 继承与派生表 7-1 访问属性与继承的关系第 7章 继承与派生
7.3.1 公有继承当类的继承方式为 public( 公有 ) 继承时,基类的
public( 公有 ) 和 protected( 保护 ) 成员的访问属性在派生类中不变,而基类的 private( 私有 ) 成员仍保持私有属性 。 也就是说,派生类的其它成员可以直接访问基类的公有成员和保护成员 。 其它外部使用者只能通过派生类的对象访问继承来的公有成员 。 而无论是派生类的成员还是派生类的对象,都无法访问基类的私有成员 。
第 7章 继承与派生
【 例 7-1】 公有继承例题 。
从基类 vehicle( 汽车 ) 公有派生 car( 小汽车 ) 类,
car类继承了 vehicle类的全部特征,同时,car类自身也有一些特点,这就需要在继承 vehicle类的同时添加新的成员 。
程序代码如下:
#include<iostream.h>
classvehicle //基类 vehicle类的声明
{
private,//私有数据成员第 7章 继承与派生
intwheels;
floatweight;
public,//公有函数成员
vehicle(intin_wheels,floatin_weight)
{wheels=in_wheels;weight=in_weight;}
intget_wheels(){returnwheels;}
floatget_weight(){returnweight;}
};
第 7章 继承与派生
classcar:publicvehicle //派生类 car类的声明
{
private,//新增私有数据成员
intpassenger_load;
public,//新增公有函数成员
car(intin_wheels,floatin_weight,intpeople=5):vehicle(in_w
heels,in_weight)
{passenger_load=people;}
第 7章 继承与派生
intget_passengers(){returnpassenger_load;}
};
voidmain()
{
carbluebird(4,1000); //声明 car类的对象
cout<<"Themessageofbluebird(wheels,weight,passengers):"
<<endl;
cout<<bluebird.get_wheels()<<"," //输出小汽车的信息
<<bluebird.get_weight()<<","
<<bluebird.get_passengers()<<endl;
}
第 7章 继承与派生这里首先声明了基类 vehicle。派生类 car继承了 vehicle
类的全部成员(构造和析构函数除外)。因此,在派生类中,实际所拥有的成员就是从基类继承过来的成员与派生类新声明的成员的总和。继承方式为公有继承。这时基类中的公有成员在派生类中的访问属性保持原样,派生类的成员函数及对象可以访问到基类的公有成员,但是无法访问基类的私有数据(例如基类的 wheels和 weight)。
第 7章 继承与派生基类原有的外部接口(如基类的 get_wheels()和
get_weight()函数)变成了派生类外部接口的一部分。
当然,派生类自己新增的成员之间都是可以互相访问的。
car类继承了 vehicle类的成员,也就实现了代码的重用 。 同时,通过新增成员,加入了自身的独有特征,
实现了程序的扩充 。
第 7章 继承与派生在主函数 main()中首先声明了一个派生类的对象
bluebird。 对象生成时,调用构造函数 ( 关于派生类的构造函数在 7.4节中说明 ) 实现初始化 。 然后通过派生类的对象,访问派生类的公有函数 get_passengers(),也访问了派生类从基类继承来的公有函数 get_wheels()和
get_weight()。
通过此例题,我们可以比较直观地看到,一个基类以公有方式产生了派生类之后,派生类的成员函数以及派生类的对象是如何访问从基类继承的公有成员的 。 程序运行结果为
Themessageofbluebird(wheels,weight,passengers):
4,1000,5
第 7章 继承与派生
7.3.2 私有继承当类的继承方式为 private(私有)继承时,基类中的 public(公有)成员和 protected(保护)成员都以私有成员身份出现在派生类中,而基类的 private(私有)
成员在派生类中不可访问。也就是说,基类的 public成员和 protected成员被继承后作为派生类的 private成员,
派生类的其它成员可以直接访问它们,但是在类外部通过派生类的对象无法访问。特别要注意,基类的
private成员仍保持 private属性,这样,无论是派生类的成员还是通过派生类的对象,都无法访问从基类继承的私有成员。
第 7章 继承与派生
【 例 7-2】 私有继承例题 。
采用私有继承方式重作例 7-1,程序如下:
#include<iostream.h>
classvehicle //基类 vehicle类的声明
{
private,//私有数据成员
intwheels;
floatweight;
public,//公有函数成员第 7章 继承与派生
vehicle(intin_wheels,floatin_weight)
{wheels=in_wheels;weight=in_weight;}
intget_wheels(){returnwheels;}
floatget_weight(){returnweight;}
};
classcar:privatevehicle //派生类 car类的声明
{
private,//新增私有数据成员第 7章 继承与派生
intpassenger_load;
public,//新增公有函数成员
car(intin_wheels,floatin_weight,intpeople=5):vehicle(in_w
heels,in_weight)
{passenger_load=people;}
intget_wheels(){returnvehicle::get_wheels();} //重新定义 get_wheels()
floatget_weight(){returnvehicle::get_weight();} //重新定义 get_weight()
intget_passengers(){returnpassenger_load;}
};
voidmain()
第 7章 继承与派生
{
carbluebird(4,1000); //声明 car类的对象
cout<<"Themessageofbluebird(wheels,weight,passengers):
"<<endl;
cout<<bluebird.get_wheels()<<"," //输出小汽车的信息
<<bluebird.get_weight()<<","
<<bluebird.get_passengers()<<endl;
}
第 7章 继承与派生同例 7-1,派生类 car类继承了 vehicle类的成员。因此,
在派生类中,实际所拥有的成员就是从基类继承来的成员与派生类新声明成员的总和。继承方式为私有继承。这时,基类中的公有和保护成员在派生类中都以私有成员的身份出现。派生类的成员函数及对象无法访问基类的私有数据 (例如基类的 wheels和 weight)。
第 7章 继承与派生派生类的成员仍然可以访问到从基类继承过来的公有和保护成员,但是在类外部通过派生类的对象根本无法访问到基类的任何成员,基类原有的外部接口
( 例如基类的 get_wheels()和 get_weight()函数 ) 被派生类封装和隐蔽起来 。 当然,派生类新增的成员之间仍然可以自由地互相访问 。
在私有继承情况下,为了保证基类的部分外部接口特征能够在派生类中也存在,就必须在派生类中重新定义同名的成员函数。
第 7章 继承与派生例 7-2程序的主函数部分和例 7-1完全相同,但执行过程却有所不同。在例 7-1中,car类对象 bluebird调用的 get_wheels()和 get_weight()函数是从基类继承的公有成员函数;而本例的 car类对象 bluebird调用的函数都是派生类自身的公有成员函数。因为是私有继承,它不可能访问到任何一个基类的成员。
第 7章 继承与派生同例 7-1相比较,本例对程序修改的只是派生类的内容,
基类和主函数部分没有做任何改动 。 读者也可以由此看到面向对象程序设计封装性的优越性,car类的外部接口不变,内部成员的实现做了改造,根本没有影响到程序的其它部分,这正是面向对象程序设计可重用与可扩充性的一个实际体现 。
第 7章 继承与派生
7.3.3 保护继承保护继承中,基类的 public( 公有 ) 和 protected
( 保护 ) 成员都以保护成员的身份出现在派生类中,
而基类的 private( 私有 ) 成员不可访问 。 具体说,基类中的保护成员只能被基类的成员函数或派生类的成员函数访问,不能被派生类以外的成员函数访问 。
第 7章 继承与派生
【 例 7-3】 保护继承例题 。
采用保护继承方式重作例 7-2,程序如下:
#include<iostream.h>
classvehicle //基类 vehicle类的声明
{
private,//私有数据成员
intwheels;
protected,//保护数据成员第 7章 继承与派生
floatweight;
public,//公有函数成员
vehicle(intin_wheels,floatin_weight)
{wheels=in_wheels;weight=in_weight;}
intget_wheels(){returnwheels;}
floatget_weight(){returnweight;}
};
classcar:protectedvehicle //派生类 car类的声明第 7章 继承与派生
{
private,//新增私有数据成员
intpassenger_load;
public,//新增公有函数成员
car(intin_wheels,floatin_weight,intpeople=5):vehicle(in_whe
els,in_weight)
{passenger_load=people;}
intget_wheels(){returnvehicle::get_wheels();} //重新定义
get_wheels()
floatget_weight(){returnvehicle::get_weight();} //重新定义
get_weight()
第 7章 继承与派生
intget_passengers(){returnpassenger_load;}
};
voidmain()
{
carbluebird(4,1000); //声明 car类的对象
cout<<"Themessageofbluebird(wheels,weight,passengers):
"<<endl;
cout<<bluebird.get_wheels()<<"," //输出小汽车的信息
<<bluebird.get_weight()<<","
<<bluebird.get_passengers()<<endl;
}
第 7章 继承与派生在例 7-3中,我们将例 7-2中私有数据成员 weight改为保护数据成员,类定义的其它部分没改变,继承方式改为保护继承。这时,基类中的公有和保护成员在派生类中都以保护成员的身份出现。派生类的成员函数及对象无法访问基类的私有数据和保护数据(例如基类的 wheels和 weight)。派生类的成员仍然可以访问到从基类继承过来的公有和保护成员。
第 7章 继承与派生同私有继承一样,在保护继承情况下,为了保证基类的部分外部接口特征能够在派生类中也存在,就必须在派生类中重新定义同名的成员函数 。 这里在派生类
car中,重新定义了 get_wheels()和 get_weight()函数 。 根据同名覆盖的原则,在主函数中自然调用的是派生类的函数 。
第 7章 继承与派生例 7-3程序的主函数部分和例 7-2完全相同,执行过程也是一样的 。 程序的运行结果自然也相同 。
从以上例题我们可以看到类中保护成员的特征,
如果某 A类中含有保护成员,对于建立 A类对象的模块来讲,保护成员和该类的私有成员一样是不可访问的。
如果 A类派生出子类,则对于该子类来讲,保护成员与公有成员具有相同的访问特性。
第 7章 继承与派生换句话来说,就是 A类中的保护成员有可能被它的派生类访问,但是决不可能被其它外部使用者 ( 比如程序中的普通函数,与 A类平行的其它类等 ) 访问 。 这样,
如果合理地利用保护成员,就可以在类的复杂层次关系中为共享访问与成员隐蔽之间找到一个平衡点,既能实现成员隐蔽,又能方便继承,实现代码的高效重用和扩充 。
第 7章 继承与派生可以看出在直接派生类中,所有成员的访问属性都是完全相同的 。 但是,如果派生类作为新的基类继续派生时,二者的区别就出现了 。 如图 7-4所示 。
图中 7-4(a)说明 B类以私有方式继承了 A类后,又派生出 C类,则 C类的成员和对象都不能访问间接从 A类中继承来的成员。图 7-4(b)说明 B类以保护方式继承了
A类,那么 A类中的公有和保护成员在 B类中都是保护成员。
第 7章 继承与派生图 7-4 类的保护成员的访问规则
(a)私有继承 (b)保护继承第 7章 继承与派生总之,在派生中,通过继承方式可以改变成员的访问属性 。 按访问属性的不同,成员可以归纳为以下四种 。
①不可访问的成员。
②私有成员。
③保护成员。
④公有成员。
第 7章 继承与派生
7.4 派生类的构造和析构函数
7.4.1 构造函数派生类对象的初始化也是通过派生类的构造函数实现的。具体来说,就是对该类的数据成员赋初值。
派生类的数据成员由所有基类的数据成员与派生类新增的数据成员共同组成,如果派生类新增成员中包括有内嵌的其它类对象,派生类的数据成员中实际上还间接包括了这些对象的数据成员。
第 7章 继承与派生因此,初始化派生类的对象时,就要对基类数据成员,
新增数据成员和成员对象的数据成员进行初始化 。 因此,派生类的构造函数需要以合适的初值作为参数,
隐含调用基类和新增的内嵌对象成员的构造函数来初始化它们各自的数据成员,然后再加入新的语句对新增普通数据成员进行初始化 。
第 7章 继承与派生派生类构造函数声明的一般语法形式如下:
<派生类名 >::<派生类名 >(参数总表 ):基类名 1(参数表 1),...,基类名 n(参数表 n),
内嵌对象名 1(内嵌对象参数表 1),...,内嵌对象名
m(内嵌对象参数表 m)
{
派生类新增成员的初始化语句;
}
第 7章 继承与派生其中:
① 派生类的构造函数名与派生类名相同 。
② 参数总表需要列出初始化基类数据,新增内嵌对象数据及新增一般成员数据所需要的全部参数 。
③冒号之后,列出需要使用参数进行初始化的基类名和内嵌成员名及各自的参数表,各项之间用逗号分隔。
第 7章 继承与派生特别需要注意,当一个派生类同时有多个基类时,
对于所有需要给予参数进行初始化的基类,都要显式给出基类名和参数表 。 对于使用默认构造函数的基类,
可以不给出类名 。 同样,对于对象成员,如果是使用默认构造函数,也不需要写出对象名和参数表 。 而对于单继承,就只需要写一个基类名就可以了 。
第 7章 继承与派生派生类构造函数的执行顺序一般是,先祖先 (基类 ),
再客人 (内嵌对象 ),后自己 (派生类本身 )。
现在我们再来看上一节的例 7-1中构造函数的声明及调用情况 。 程序简化如下:
#include<iostream.h>
classvehicle //基类 vehicle的声明
{
private:
intwheels;
第 7章 继承与派生
floatweight;
public,//基类 vehicle的构造函数
vehicle(intin_wheels,floatin_weight)
{wheels=in_wheels;weight=in_weight;}
//...
};
classcar:publicvehicle //派生类 car的声明
{
private,//新增私有数据成员第 7章 继承与派生
intpassenger_load;
public,//派生类 car的构造函数
car(intin_wheels,floatin_weight,intpeople=4):vehicle(in_w
heels,in_weight)
{passenger_load=people;}
//...
};
voidmain()
{
carbluebird(4,3); //声明派生类 car的对象第 7章 继承与派生
//...
}
主函数中,声明了派生类 car的对象 bluebird(4,3),
生成对象 bluebird时调用了派生类的构造函数:
car(intin_wheels,floatin_weight,intpeople=4):vehicle(in_w
heels,in_weight)
{passenger_load=people;}
第 7章 继承与派生
7.4.2 析构函数派生类析构函数的功能与没有继承关系的类中析构函数的功能一样,也是在对象消亡之前进行一些必要的清理工作。在派生过程中,基类的析构函数不能继承,如果需要析构函数的话,就要在派生类中重新定义。析构函数没有类型,也没有参数,和构造函数相比,情况略为简单。在本章前面的例子中,我们都没有显式定义过某个类的析构函数。这种情况下,系统会自动为每个类都生成一个默认的析构函数,清理工作就是靠它们完成的。
第 7章 继承与派生派生类析构函数的定义方法与没有继承关系的类中析构函数的定义方法完全相同,只要在函数体中负责把派生类新增的非对象成员的清理工作做好就够了,系统会自己调用基类及成员对象的析构函数来对基类及对象成员进行清理。但它的执行顺序和构造函数正好严格相反 —— 先自己 (派生类本身 ),再客人 (内嵌对象 ),
后祖先 (基类 )。
第 7章 继承与派生
【 例 7-4】 派生类的构造函数和析构函数 ( 多继承,
含有内嵌对象 ) 例题 。
有三个基类 Base1,Base2和 Base3,它们都有自己的构造函数和析构函数。其中 Base3有一个默认的构造函数,即不带参数的构造函数,其余两个的构造函数都带有参数。类 Derive由这三个基类经过公有派生而来。
派生类新增加了三个私有对象成员,memberBase1、
memberBase2 和 memberBase3,它们分别是 Base1,
Base2和 Base3类的对象 。 另外,派生类定义了自己的构造函数,而没有定义析构函数,即采用默认的析构函数 。
第 7章 继承与派生程序代码如下:
#include<iostream.h>
classBase1 //基类 Base1,构造函数有参数
{
public:
Base1(inti){cout<<"constructingBase1"<<i<<endl;}
~Base1(){cout<<"destructingBase1"<<endl;} //Base1
的析构函数
};
第 7章 继承与派生
classBase2 //基类 Base2,构造函数有参数
{
public:
Base2(intj){cout<<"constructingBase2"<<j<<endl;}
~Base2(){cout<<"destructingBase2"<<endl;} //Base2
的析构函数
};
classBase3 //基类 Base3,构造函数无参数第 7章 继承与派生
{
public:
Base3(){cout<<"constructingBase3"<<endl;}
~Base3(){cout<<"destructingBase3"<<endl;}
//Base3的析构函数
};
classDerive:publicBase2,publicBase1,publicBase3
//派生新类
{
private:
//派生类新增私有对象成员第 7章 继承与派生
Base1memberBase1;
Base2memberBase2;
Base3memberBase3;
public:
//派生类的构造函数
Derive(inta,intb,intc,intd):Base2(b),memberBase2(d),membe
rBase1(c),Base1(a){}
};
voidmain()
{
Deriveobject(2,4,6,8);
}
第 7章 继承与派生现在我们来仔细分析派生类构造函数和析构函数的特点 。 因为基类及内嵌对象成员有带参数的构造函数,
所以,派生类中需要定义一个带参数的构造函数 。 这个派生类构造函数的主要功能就是初始化基类及内嵌对象成员 。 按照我们前面所讲过的规则,派生类的构造函数定义为
Derive(inta,intb,intc,intd):Base1(a),memberBase2(d),member
Base1(c),Base2(b){}
构造函数的参数表中给出了基类及内嵌成员对象所需的全部参数。
第 7章 继承与派生程序的主函数中只声明了一个派生类 Derive的对象
object,生成对象 object时调用了派生类的构造函数 。
我们来考虑 Derive类的构造函数的执行情况 。 它应该是先调用基类的构造函数,然后调用内嵌对象的构造函数 。 基类构造函数的调用顺序是按照派生类声明时的顺序,因此,应该是先 Base2,再 Base1,再 Base3。 而内嵌对象的构造函数的调用顺序应该是按照成员在类中声明的顺序,应该是先 Base1,再 Base2,再 Base3。
程序运行的结果也完全证实了这种分析 。
第 7章 继承与派生派生类构造函数声明中,并没有显式列出 Base3的类名和 Base3类的对象 memberBase3,这时系统就会自动调用该类的默认构造函数 。 如果一个基类同时声明了默认构造函数和带有参数的构造函数,那么在派生类构造函数声明中,既可以显式列出基类名和相应的参数,
也可以不列出,程序员可以根据实际情况的需要来自行安排 。
第 7章 继承与派生程序中,我们给三个基类分别加入了析构函数,
派生类使用的是由系统提供的默认析构函数 。 程序执行时,首先执行派生类的构造函数,然后执行派生类的析构函数,派生类默认的析构函数又分别调用了成员对象及基类的析构函数 。 这时,析构函数的执行次序刚好和构造函数的执行次序相反 。
程序运行结果为
constructingBase24
constructingBase12
constructingBase3
第 7章 继承与派生
constructingBase16
constructingBase28
constructingBase3
destructingBase3
destructingBase2
destructingBase1
destructingBase3
destructingBase1
destructingBase2
第 7章 继承与派生
7.5 派生中成员的标识与访问
7.5.1 作用域分辨在派生类的访问中,有两个问题需要解决:第一是唯一标识问题;第二是可见性问题。对于在不同的作用域声明的标识符,其可见性原则是:如果存在两个或多个具有包含关系的作用域,并且外层声明的标识符如果在内层没有声明同名的标识符,那么它在内层仍可见;
如果内层声明了同名的标识符,则外层标识符在内层不可见,这时称内层变量覆盖了外层同名变量,这种现象称为同名覆盖。
第 7章 继承与派生
1,作用域分辨符作用域分辨符就是我们经常见到的,,:”,它可以用来限定要访问的成员归属哪个类,其一般的使用形式如下:
<类名 >::<成员名 >
<类名 >::<成员名 >(参数表 )
在类的派生层次结构中,基类的成员和派生类新增的成员都具有类作用域。二者的作用范围不同,是相互包含的两个层,派生类在内层。
第 7章 继承与派生
【 例 7-5】 继承中使用作用域分辨符例题 。
这里声明了基类 Base,由基类 Base公有派生产生了新类 Derive。 基类中定义了成员数据 n和成员函数 fun(),
在派生类中新增了同名的两个成员 。 这时的 Derive类共含有四个成员,而这四个成员只有两个名字 。
程序代码如下:
#include<iostream.h>
classBase //声明基类 Base
{
public:
intn;
第 7章 继承与派生
voidfun(){cout<<"ThisisBase,n="<<n<<endl;}
};
classDerive:publicBase //声明派生类 Derive
{
public:
intn; //同名数据成员
voidfun(){cout<<"ThisisDerive,n="<<n<<endl;}
//同名函数成员
};
voidmain()
第 7章 继承与派生
{
Deriveobj;
obj.n=1; //对象名,成员名标识
obj.fun(); //对象名,成员函数名标识
obj.Base::n=2; //作用域分辨符标识
obj.Base::fun(); //访问 Base基类成员
}
第 7章 继承与派生程序运行结果为
ThisisDerive,n=1
ThisisBase,n=2
第 7章 继承与派生
2.多继承中作用域分辨符的使用在多继承中,如果某个派生类的部分或全部直接基类是从另一个共同的基类派生而来,在这些直接基类中,从上一级基类继承来的成员就拥有相同的名称 。
因此,派生类中也就会产生同名现象 。 对这种类型的同名成员也要使用作用域分辨符来唯一标识,而且必须用直接基类来进行限定 。 我们再来看一个例题 。
第 7章 继承与派生
【 例 7-6】 多继承中使用作用域分辨符例题 。
有一个基类 Level1,声明了成员数据 n1和成员函数
fun1(),由 Level1公有派生 Level21和 Level22两个类,
再以 Level21和 Level22作为基类共同公有派生产生了新类 Level3,在 Level3类中,没有添加新的同名成员 (如果有同名成员,同样遵循同名覆盖规则 )。因此,
Level3类就含有通过 Level21和 Level22继承来的基类
Level1中的同名成员 n1和 fun1()。类的派生关系及派生类的结构见图 7-5。
第 7章 继承与派生图 7-5多重继承情况下派生类 Level3继承关系,成员构成图
(a)继承关系 (b)Level3类结构第 7章 继承与派生现在我们来讨论同名成员 n1和 fun1()的标识与访问问题 。 间接基类 Level1的成员经过两次派生之后,通过不同的派生路径以相同的名字出现在派生类 Level3中 。
这时,如果使用基类名 Level1来限定,同样无法表明成员到底是从 Level21还是 Level22继承过来的,因此,必须使用直接基类 Level21或者 Level22的名称来限定,才能够唯一标识和访问成员 。
程序代码如下:
#include<iostream.h>
classLevel1 //声明基类 Level1
第 7章 继承与派生
{
public:
intn1;
voidfun1(){cout<<"ThisisLevel1,n1="<<n1<<endl;}
};
classLevel21:publicLevel1 //声明派生类 Level21
{
public:
intn21;
};
第 7章 继承与派生
classLevel22:publicLevel1 //声明派生类 Level22
{
public:
intn22;
};
classLevel3:publicLevel21,publicLevel22 //声明派生类
Level3
{
public:
intn3;
voidfun3(){cout<<"ThisisLevel3,n3="<<n3<<endl;}
};
第 7章 继承与派生
voidmain()
{
Level3obj;
obj.n3=1;
obj.fun3();
obj.Level21::n1=2; //使用直接基类
obj.Level21::fun1(); //使用直接基类
obj.Level22::n1=3; //使用直接基类
obj.Level22::fun1(); //使用直接基类
}
第 7章 继承与派生在程序主函数中,创建了一个派生类的对象 obj。
如果只通过成员名称来访问该类的成员 n1和 fun1(),系统就无法唯一确定要引用的成员 。 这时,必须使用作用域分辨符,通过直接基类名来确定要访问的从基类继承来的成员 。
程序运行结果为
ThisisLevel3,n3=1
ThisisLevel1,n1=2
ThisisLevel1,n1=3
第 7章 继承与派生这种情况下,派生类对象在内存中就同时拥有成员 n1及 fun1()的两份同名拷贝 。 对于数据成员来讲,两个 n1可以分别通过 Level21和 Level22调用 Level1的构造函数进行初始化,可以存放不同的数值,也可以使用作用域分辨符通过直接基类名的限定来分别进行访问 。
但是在很多情况下,我们只需要一个这样的数据拷贝,
同一成员的多份拷贝增加了内存的开销 。 C++提供了虚基类技术来解决这一问题,这部分内容将在 7.6节中介绍 。
第 7章 继承与派生
7.5.2 基类私有成员的访问前面我们介绍过,不管是私有派生还是公有派生,
派生类都无权访问基类的私有成员 。 派生类想要使用基类的私有成员,只能通过调用基类的成员函数来实现,也就是使用基类所提供的接口 。 对于需要频繁访问基类私有成员的派生类来说,这种方式使用起来非常不方便 。 因此,需要寻求直接访问基类私有成员的方式 。 有两种方式可供选择,下面分别介绍 。
第 7章 继承与派生
1.在类定义体中增加保护段为了便于派生类的访问,可以将基类的私有成员中需提供给派生类访问的部分定义为保护段成员 。 保护段成员可以被它的派生类访问,但是对于外界是隐藏的 。 这样,即方便了派生类的访问,又禁止了外界对它的操作 。 下面我们来看一道例题 。
【 例 7-7】 在类定义体中增加保护段直接访问基类私有成员例题 。
第 7章 继承与派生有一个汽车类 vehicle,它具有一个需传递参数的构造函数,类中的数据成员 wheels和 weight放在保护段
protected中;小车类 car和卡车类 truck均是 vehicle类的私有派生类,在 car类和 truck类中都可以直接访问
vehicle类中的数据成员 wheels和 weight。
程序代码如下:
#include<iostream.h>
classvehicle //基类 vehicle类的声明
{
protected,//保护数据成员
intwheels;
第 7章 继承与派生
floatweight;
public,//公有函数成员
vehicle(intin_wheels,floatin_weight)
{wheels=in_wheels;weight=in_weight;}
intget_wheels(){returnwheels;}
floatget_weight(){returnweight;}
floatwheel_load(){returnweight/wheels;}
voidprint();
第 7章 继承与派生
};
classcar:vehicle //派生类 car类的声明
{
private,//新增私有数据成员
intpassenger_load;
public,//新增公有函数成员
car(intin_wheels,floatin_weight,intpeople=4):vehicle(in_w
heels,in_weight)
{passenger_load=people;}
第 7章 继承与派生
intget_passengers(){returnpassenger_load;}
voidprint();
};
classtruck:vehicle //派生类 truck类的声明
{
private,//新增私有数据成员
intpassenger_load;
floatpayload;
public,//新增公有函数成员第 7章 继承与派生
truck(intin_wheels,floatin_weight,intpeople=2,
floatmax_load=24000.00):vehicle(in_wheels,in_weight)
{passenger_load=people;payload=max_load;}
intget_passengers(){returnpassenger_load;}
floatefficiency(){returnpayload/(payload+weight);}
voidprint();
};
voidvehicle::print() //输出汽车类 vehicle的数据第 7章 继承与派生
{
cout<<"thewheelsofvehicleis"<<wheels<<endl;
cout<<"theweightofvehicleis"<<weight<<endl;
cout<<endl;
}
voidcar::print()
//输出小车类 car的数据
{
cout<<"thewheelsofcaris"<<wheels<<endl;
cout<<"theweightofcaris"<<weight<<endl;
第 7章 继承与派生
cout<<"thepassenger_loadofcaris"<<passenger_load<<endl;
cout<<endl;
}
voidtruck::print() //输出卡车类 truck的数据
{
cout<<"thewheelsoftruckis"<<wheels<<endl;
cout<<"theweightoftruckis"<<weight<<endl;
cout<<"thepassenger_loadoftruckis"<<passenger_load<<endl;
cout<<"theefficencyoftruckis"<<efficiency()<<endl;
第 7章 继承与派生
cout<<endl;
}
voidmain()
{
carbluebird(4,1000,5); //声明 car类的对象
truckdongfeng(10,5000,3,34000); //声明 truck类的对象
bluebird.print();
dongfeng.print();
}
第 7章 继承与派生程序运行结果为
thewheelsofcaris4
theweightofcaris1000
thepassenger_loadofcaris5
thewheelsoftruckis10
theweightoftruckis5000
thepassenger_loadoftruckis3
theefficiencyoftruckis0.871795
第 7章 继承与派生
2,将需访问基类私有成员的派生类成员函数声明为基类的友元另一种访问基类私有成员的方法是将待访问的成员函数声明为基类的友元,这样,派生类中的其它成员函数都无权访问它,外界更不可能通过派生新类来访问基类的私有成员 。 关于友元函数在前面第 5章已介绍过,这里就不再赘述 。
第 7章 继承与派生
7.5.3 引入派生类后的对象指针前面章节中介绍了一般对象的指针,它们各自独立,之间没有什么联系,相互不能混用。引入派生类后,由于派生类是由基类派生出来的,派生类和基类之间息息相关,因此,指向派生类和基类的指针也是相关的。在引入了派生的概念后,任何被说明为指向基类对象的指针都可以指向它的公有派生类。下面看一个例子。
第 7章 继承与派生
【 例 7-8】 引入派生类后的对象指针例题 。
#include<iostream.h>
#include<string.h>
classstring
{
char*name;
intlength;
public:
string(char*str)
{
第 7章 继承与派生
length=strlen(str);
name=newchar[length+1];
strcpy(name,str);
}
voidshow(){cout<<name<<endl;}
};
classde_string:publicstring
{
intage;
public:
第 7章 继承与派生
de_string(char*str,intage):string(str)
{de_string::age=age;}
voidshow()
{
string::show();
cout<<"theageis:"<<age<<endl;
}
};
main()
{
第 7章 继承与派生
strings1("Smith"),*ptr1; //定义 string类对象 s1及指针 ptr1
de_strings2("Jean",20),*ptr2;
//定义 de_string类对象 s2及指针 ptr2
ptr1=&s1; //将 ptr1指向 s1对象
ptr1->show(); //调用 string类的成员函数
ptr1=&s2; //将 ptr1指向 string类的派生类 de_string的对象 s2
ptr1->show(); //调用 s2对象所属的基类的成员函数 show()
ptr2=&s2; //将 ptr2指向 de_string类对象 s2
ptr2->show(); //调用 de_string类的成员函数 show()
return1;
第 7章 继承与派生
}
程序运行结果为
Smith
Jean
Jean
theageis:20
从本例可以看出,虽然 ptr1指针已经指向了 s2对象
(ptr1=&s2),但是它所调用的函数 (ptr1->show())仍然是其基类对象的成员函数 。 这是使用时要注意的问题 。
在使用引入派生类之后的对象指针时,要特别注意下面几点 。
第 7章 继承与派生
① 可以用一个声明指向基类对象的指针指向它的公有派生的对象,若试图指向它的私有派生的对象则是被禁止的 。 例如:
classBase //定义基类 Base
{
//...
};
classDerive:Base
//定义基类 Base的私有派生类 Derive
{
//...
};
第 7章 继承与派生
main()
{
Baseobj1,*ptr1;
Deriveobj2;
ptr1=&obj1;
ptr1=&obj2;
//错误 ! 试图将 Base类指针 ptr1指向它的私有派生类对象
//...
return1;
}
第 7章 继承与派生
② 不能将一个声明为指向派生类对象的指针指向其基类的对象 。 例如:
classBase //定义基类 Base
{
//...
};
classDerive:publicBase
//定义基类 Base的公有派生类 Derive
{
第 7章 继承与派生
{
//...
};
main()
{
Baseobj1;
Deriveobj2,*ptr;
ptr=&obj2;
ptr=&obj1;
//错误 ! 试图将派生类指针 ptr指向基类对象
//...
return1;
}
第 7章 继承与派生
③ 声明为指向基类对象的指针,当其指向派生类对象时,只能利用它来直接访问派生类中从基类继承来的成员,不能直接访问公有派生类中特定的成员 。
例如:
classBase //定义基类 Base
{
public:
voidshow1();
};
classDerive:publicBase
//定义基类 Base的公有派生类 Derive
第 7章 继承与派生
{
voidshow2();
};
main()
{
Baseobj1,*ptr;
Deriveobj2;
ptr=&obj1;
ptr->show1();
ptr=&obj2;
ptr->show1();
第 7章 继承与派生
ptr->show2();
//错误 ! 试图调用派生类的特定成员
//...
return1;
}
若想访问其公有派生类的特定成员,可以将基类指针显式类型转换为派生类指针来实现 。
第 7章 继承与派生在公有继承前提下,除了派生类对象的地址可以赋给指向基类的指针,派生类的对象也可以赋值给基类对象,还可用派生类的对象初始化基类的引用 。 也就是说,在需要基类对象的任何地方都可以使用公有派生类的对象来替代,这就是所谓的赋值兼容规则 。 在替代之后,派生类对象就可以作为基类的对象使用,但只能使用从基类继承的成员 。
第 7章 继承与派生例如,如果 Base类为基类,Derive为 Base类的公有派生类,则 Derive类中包含了基类 Base中除构造、析构函数之外的所有成员。这时,根据赋值兼容规则,在基类 Base的对象可以使用的任何地方,都可以用派生类的对象来替代。在下面的程序中,objBase为 Base类的对象,objDerive为 Derive类的对象。
classBase
{//…};
classDerive:publicBase
{//…};
第 7章 继承与派生
voidmain()
{
BaseobjBase,*pb;
DeriveobjDerive;
}
这时:
① 派生类对象可以赋值给基类对象,即用派生类对象中从基类继承来的成员赋值给基类对象的成员
objBase:
objBase=objDerive;
第 7章 继承与派生
② 派生类的对象也可以初始化基类对象的引用
objBase1:
Base& objBase1=objDerive;
③ 派生类对象的地址也可以赋给指向基类的指针:
pb=& objDerive;
通过上述例子看到,根据赋值兼容规则,我们可以在基类出现的场合使用派生类进行替代,但是替代之后派生类仅仅发挥出基类的作用。
第 7章 继承与派生
7.6 虚基类
7.6.1虚基类的声明虚基类的声明是在派生类的声明过程中声明的,其语法形式如下:
class<派生类 >:virtual[继承方式 ]<基类名 >
上述语句声明基类为派生类的虚基类 。 在多继承情况下,虚基类关键字的作用范围和继承方式与一般派生类的声明一样,只对紧跟其后的基类起作用 。 声明了虚基类之后,虚基类的成员在进一步派生过程中和派生类一起维护同一个内存数据拷贝 。
第 7章 继承与派生
【 例 7-9】 虚基类例题 。
用虚基类的方法重作例 7-6。 类的派生关系及派生类的结构见图 7-6。
程序代码如下:
#include<iostream.h>
classLevel1
//声明基类 Level1
{
第 7章 继承与派生图 7-6 虚基类派生类 Level3继承关系,成员构成图
(a)继承关系 (b)Level3类结构第 7章 继承与派生
public:
intn1;
voidfun1(){cout<<"ThisisLevel1,n1="<<n1<<endl;}
};
classLevel21:virtualpublicLevel1
//Level1为虚基类,派生类 Level21
{
public:
intn21;
};
classLevel22:virtualpublicLevel1
//Level1为虚基类,派生类 Level22
第 7章 继承与派生
{
public:
intn22;
};
classLevel3:publicLevel21,publicLevel22
//声明派生类 Level3
{
public:
intn3;
voidfun3(){cout<<"ThisisLevel3,n3="<<n3<<endl;}
};
voidmain()
第 7章 继承与派生
{
Level3obj;
obj.n3=1;
obj.fun3();
obj.n1=2; //采用虚基类后,直接使用
obj.fun1(); //“对象名,成员名,方式
}
第 7章 继承与派生这里与例 7-6不同的是,派生时声明 Level1为虚基类,
再以 Level21和 Level22作为基类共同公有派生产生了新类 Level3。在派生类中,我们没有添加新的同名成员
(如果有同名成员,同样遵循同名覆盖规则 )。这时的
Level3类中,通过 Level21和 Level22两条派生路径继承来的基类 Level1中的成员 n1和 fun1()只有一份拷贝。
第 7章 继承与派生使用虚基类之后,在 Level3派生类中只有唯一的数据成员 n1和函数成员 fun1()。 在建立 Level3类对象的模块中,直接使用,对象名,成员名,方式就可以唯一标识和访问这些成员 。
程序运行结果为
ThisisLevel3,n3=1
ThisisLevel1,n1=2
第 7章 继承与派生
7.6.2 虚基类及其派生类的构造函数在例 7-9中,虚基类的使用显得非常方便,简单,
这是由于该程序中的所有类使用的都是编译器自动生成的默认构造函数 。 如果虚基类定义有非默认形式 (即带形参 )的构造函数,事情就比较麻烦了 。 这时,在整个继承结构中,直接或间接继承虚基类的所有派生类,
都必须在构造函数的成员初始化表中列出对虚基类的初始化 。
第 7章 继承与派生
【 例 7-10】 虚基类及其派生类的构造函数例题 。
如果例 7-9中的虚基类定义了带参数的构造函数,
则程序修改为如下形式:
#include<iostream.h>
classLevel1 //声明基类 Level1
{
public:
intn1;
Level1(intin_n1){n1=in_n1;cout<<"ThisisLevel1,n1="<<n
1<<endl;}
第 7章 继承与派生
};
classLevel21:virtualpublicLevel1
//Level1为虚基类,派生类 Level21
{
public:
intn21;
Level21(inta):Level1(a)
{n21=a;cout<<"ThisisLevel21,n21="<<n21<<endl;}
};
第 7章 继承与派生
classLevel22:virtualpublicLevel1
//Level1为虚基类,派生类 Level22
{
public:
intn22;
Level22(inta):Level1(a)
{n22=a;cout<<"ThisisLevel22,n22="<<n22<<endl;}
};
classLevel3:publicLevel21,publicLevel22
//声明派生类 Level3
第 7章 继承与派生
{
public:
intn3;
Level3(inta):Level1(a),Level21(a),Level22(a)
{n3=a;cout<<"ThisisLevel3,n3="<<n3<<endl;}
};
voidmain()
{
Level3obj(3); //定义 Level3类对象 obj
}
第 7章 继承与派生建立 Level3类对象 obj时,通过 Level3类的构造函数的初始化列表,不仅直接调用了虚基类构造函数 Level1,
对从 Level1继承的成员 n1进行了初始化,而且还调用了基类 Level21和 Level22的构造函数 Level21()和 Level22(),
而 Level21()和 Level22()的初始化列表中也都有对基类
Level1的初始化。这样看起来,对于从虚基类继承来的成员 n1岂不是初始化了三次?对于这个问题,C++编译器有很好的解决办法,我们完全不必担心。
第 7章 继承与派生程序运行结果为
ThisisLevel1,n1=3
ThisisLevel21,n21=3
ThisisLevel22,n22=3
ThisisLevel3,n3=3
第 7章 继承与派生虚基类的初始化与一般的多继承的初始化在语法上是一样的,但构造函数的调用次序不同 。 它的调用次序如下 。
① 虚基类的构造函数在非虚基类之前调用 。
② 若同一层次中包含多个虚基类,这些虚基类的构造函数按它们说明的次序调用 。
③若虚基类由非虚基类派生而来,则仍然先调用基类构造函数,再调用派生类的构造函数。