C++语言程序设计教程 第8章 继承与派生 第8章 继承与派生 制作人:杨进才
第8章 继承与派生 学习目标 掌握派生与继承的概念与使用方法; 会运用继承机制对现有的类进行重用; C++语言程序设计教程 第8章 继承与派生 第8章 继承与派生 学习目标 掌握派生与继承的概念与使用方法; 会运用继承机制对现有的类进行重用; 掌握继承中的构造函数与析构函数的调用顺序; 会为派生类设计合适的构造函数初始化派生类; 会处理多继承时的二义性问题; 掌握虚基类的概念与使用方法。
C++语言程序设计教程 第8章 继承与派生 8.1 继承与派生 8.1.1 继承的概念 在C++中,可以利用已有的类来定义新的类,新类将拥有原有类的全部特性,原有类被称为基类(Base class)或父类(Super class),新产生的类被称为派生类(Derived class)或子类(Sub class)。派生类拥有基类的特性称作继承,由基类产生派生类的过程称为派生。 每一个派生类都有且仅有一个基类,派生类可以看作是基类的特例,它增加了某些基类所没有的性质。这种继承方式,称为单继承或单向继承。 现实生活中,子女的外貌、血型往往不是仅仅继承自父亲或母亲,而是将父母亲的特点都继承下来。与之相类似,如果一个派生类有两个或两个以上的基类,则称为多继承或多重继承。 派生类又作为基类,继续派生新的类, 这样的派生方式称为多层派生,从继承的角度看称为多层继承。
C++语言程序设计教程 第8章 继承与派生 8.1.1 继承的概念
8.1.2 派生类实现 1. 派生类的定义 派生类定义的语法为: class 派生类名:继承方式1 基类名1, 继承方式2 基类名2,… { 第8章 继承与派生 8.1.2 派生类实现 1. 派生类的定义 派生类定义的语法为: class 派生类名:继承方式1 基类名1, 继承方式2 基类名2,… { private: 派生类的私有数据和函数 public: 派生类的公有数据和函数 protected: 派生类的保护数据和函数 }; “继承方式1 基类名1, 继承方式2 基类名2,…”为基类名表, 表示当前定义的派生类的各个基类。 如果基类名表中只有一个基类,表示定义的是单继承;如果基类名表中有多个基类,表示定义的是多继承。 继承方式指定了派生类成员以及类外对象对于从基类继承来的成员的访问权限。继承方式有三种:public: 公有继承; private: 私有继承; protected:保护继承。
8.1.2 派生类实现 在派生类的定义中,每一种继承方式只限定紧跟其后的那个基类。如果不显式给出继承方式,系统默认为私有继承。 C++语言程序设计教程 第8章 继承与派生 8.1.2 派生类实现 在派生类的定义中,每一种继承方式只限定紧跟其后的那个基类。如果不显式给出继承方式,系统默认为私有继承。 【例如】在普通的时钟类Clock基础上派生出闹钟类AlarmClock: 派生类AlarmClock的成员构成图(表) class Clock { private: int H,M,S; public: void SetTime(int H=0,int M=0,int S=0); void ShowTime(); Clock(int H=0,int M=0,int S=0); ~Clock(); }; class AlarmClock: public Clock int AH,AM; //响铃的时间 bool OpenAlarm; //是否关闭闹钟 SetAlarm(int AH, int AM); //设置响铃时间 SwitchAlarm(bool Open=true); //打开/关闭闹铃 ShowTime(); //显示当前时间与闹铃时间 } 类 名 成 员 名 AlarmClock:: Clock:: H, M, S SetTime() ShowTime() AH, AM, OpenAlarm SetAlarm() SwitchAlarm() AlarmClock()
8.1.2 派生类实现 2.派生类的实现方式 (1) 吸收基类成员 C++语言程序设计教程 第8章 继承与派生 8.1.2 派生类实现 2.派生类的实现方式 (1) 吸收基类成员 基类的全部成员被派生类继承,作为派生类成员的一部分。如:Clock类中的数据成员H、M、S, 成员函数SetTime()、ShowTime()经过派生,成为派生类AlarmClock的成员。 (2) 改造基类成员 派生类根据实际情况对继承自基类的某些成员进行限制和改造。对基类成员的访问限制主要通过继承方式来实现;对基类成员的改造主要通过同名覆盖来实现,即在派生类中定义一个与基类成员同名的新成员(如果是成员函数,则函数参数表也必须相同,否则,C++会认为是函数重载)。当通过派生类对象调用该成员时,C++将自动调用派生类中重新定义的同名成员,而不会调用从基类中继承来的同名成员,这样派生类中的新成员就“覆盖”了基类的同名成员。由此可见,派生类中的成员函数具有比基类中同名成员函数更小的作用域。如:AlarmClock类中的成员函数ShowTime()覆盖了基类Clock中的同名成员函数ShowTime()。 (3) 添加新成员 派生类在继承基类成员的基础之上,根据派生类的实际需要,增加一些新的数据成员和函数成员,以描述某些新的属性和行为。如:AlarmClock添加了数据成员AH、AM、OpenAlarm, 成员函数SetAlarm()、SwitchAlarm()。
8.1.2 派生类实现 3. 继承的性质 (1) 继承关系是可以传递的 C++语言程序设计教程 第8章 继承与派生 8.1.2 派生类实现 3. 继承的性质 (1) 继承关系是可以传递的 在派生过程中,一个基类可以同时派生出多个派生类,派生出来的新类也同样可以作为基类再继续派生新的派生类。这样,就形成了一个相互关联的类的家族,有时也称作类族。在类族中,直接派生出某类的基类称为直接基类,基类的基类甚至更高层的基类称为间接基类,比如类A派生出类B,类B又派生出类C,则类B是类C的直接基类,类A是类B的直接基类,而类A称为类C的间接基类。 (2)继承关系不允许循环 在派生过程中,不允许类A派生出类B,类B又派生出类C,而类C又继承自类A。
C++语言程序设计教程 第8章 继承与派生 8.1.3 继承与组合 继承描述的是一般类与特殊类的关系,类与类之间体现的是“is a kind of”,即如果在逻辑上A是B的一种(is a kind of),则允许A继承B的功能和属性。例如汽车(automobile)是交通工具(vehicle)的一种,小汽车(car)是汽车的一种。那么类automobile可以从类vehicle派生,类car可以从类automobile派生。 组合描述的是整体与部分的关系,类与类之间体现的是“is a part of”,即如果在逻辑上A是B的一部分(is a part of),则允许A和其他数据成员组合为B。例如:发动机、车轮、电池、车门、方向盘、底盘都是小汽车的一部分,它们组合成汽车。而不能说发动机是汽车的一种。 继承和组合既有区别,也有联系,某些比较复杂的类,既需要使用继承,也需要使用组合,二者一起使用。 在某些情况下,继承与组合的实现还可以互换。在多继承时,一个派生类有多个直接基类,派生类实际上是所有基类属性和行为的组合。派生类是对基类的扩充,派生类成员一部分是从基类中来,因此派生类组合了基类。既然这样,派生类也可以通过组合类实现。例如:AlarmClock类可以通过组合Clock类实现,从功能上讲,基本的时钟功能是闹钟功能的一部分。 什么时候使用继承,什么时候使用组合,要根据问题类与类之间的具体关系,顺其自然,权衡考虑。
8.2 继承的方式 8.2.1 公有继承 注意: 公有方式继承的特点: C++语言程序设计教程 第8章 继承与派生 8.2 继承的方式 8.2.1 公有继承 公有方式继承的特点: ① 基类的公有成员在派生类中仍然为公有成员,可以由派生类对象和派生类成员函数直接访问。 ② 基类的私有成员在派生类中,无论是派生类的成员还是派生类的对象都无法直接访问。 ③ 保护成员在派生类中仍是保护成员,可以通过派生类的成员函数访问,但不能由派生类的对象直接访问。 注意: 对基类成员的访问,一定要分清是通过派生类对象访问还是通过派生类成员函数访问。
C++语言程序设计教程 第8章 继承与派生 8.2.1 公有继承 【例8-1】公有继承及其访问 将点理解为半径长度为0的圆,Point(点)类公有派生出新的Circle(圆)类。圆类具备Point类的全部特征,同时自身也有自己的特点:圆有半径。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 //Point.h #include<iostream> using namespace std; class Point { private: int X,Y; public: Point(int X=0,int Y=0) this->X=X,this->Y=Y; } void move(int OffX, int OffY) X+=OffX, Y+=OffY; void ShowXY() cout<<"("<<X<<","<<Y<<")"<<endl; };
8.2.1 公有继承 C++语言程序设计教程 第8章 继承与派生 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 /****************************** * Circle.h * * 从Point类派生出圆类(Circle) * *******************************/ #include"point.h" const double PI=3.14159; class Circle :public Point { private: double radius; //半径 public: Circle(double R, int X, int Y):Point(X,Y) radius=R; } double area() //求面积 return PI*radius*radius; void ShowCircle() cout<<"Centre of circle:"; ShowXY(); cout<<"radius:"<<radius<<endl; }; 类 名 成 员 名 访问权限 Circle Point:: X, Y private 不可访问 move() public ShowXY() radius area() ShowCircle() Circle()
8.2.1 公有继承 C++语言程序设计教程 第8章 继承与派生 运行结果 31 32 33 34 35 36 37 38 39 40 41 42 43 44 /********************** * p8_1.cpp * * Circle 类的使用 * ***********************/ #include "Circle.h" using namespace std; void main() { Circle Cir1(100,200,10); Cir1.ShowCircle(); cout<<"area is:"<<Cir1.area()<<endl; Cir1.move(10,20); Cir1.ShowXY(); } Centre of circle:(100,200) radius:10 area is:31415.9 (110,30)
C++语言程序设计教程 第8章 继承与派生 8.2.1 公有继承 程序解释 派生类Circle继承了Point类的除构造函数外的全部成员,拥有从基类继承过来的成员与派生类新添加的成员的总和。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 /****************************** * Circle.h * * 从Point类派生出圆类(Circle) * *******************************/ #include"point.h" const double PI=3.14159; class Circle :public Point { private: double radius; //半径 public: Circle(double R, int X, int Y):Point(X,Y) radius=R; } double area() //求面积 return PI*radius*radius; void ShowCircle() cout<<"Centre of circle:"; ShowXY(); cout<<"radius:"<<radius<<endl; }; 继承方式为公有继承,这时,基类中的公有成员在派生类中访问属性保持原样,派生类的成员函数及对象可以访问基类派生的的公有成员。 基类原有的外部接口(公有成员函数), 如ShowXY()和move()变成了派生类外部接口的一部分。 在Circle的构造函数中,为了给从基类继承来的数据成员赋初值,使用了初始化列表,其格式与组合类相同
C++语言程序设计教程 第8章 继承与派生 8.2.2 私有继承 私有方式继承的特点: ①基类的公有成员和保护成员被继承后作为派生类的私有成员,即基类的公有成员和保护成员被派生类吸收后,派生类的其他成员函数可以直接访问它们,但是在类外部,不能通过派生类的对象访问它们。 ②基类的私有成员在派生类中不能被直接访问。无论是派生类的成员还是通过派生类的对象,都无法访问从基类继承来的私有成员。 ③经过私有继承之后,所有基类的成员都成为了派生类的私有成员或不可访问的成员,如果进一步派生的,基类的全部成员将无法在新的派生类中被访问。因此,私有继承之后,基类的成员再也无法在以后的派生类中发挥作用,实际是相当于中止了基类的继续派生,出于这种原因,一般情况下私有继承的使用比较少。
8.2.2 私有继承 【例8-2】私有继承派生类的实现及其访问 C++语言程序设计教程 第8章 继承与派生 // Circle2.h #include"point.h" const double PI=3.14159; class Circle :private Point { private: double radius; //半径 public: Circle(double R, int X, int Y); double area(); //求面积 void ShowCircle(); void move(int OffX,int OffY) point::move(OffX,OffY); } }; 类 名 成 员 名 访问权限 Circle Point:: X,Y private 不可访问 move() public ShowXY() radius area() ShowCircle() Circle()
C++语言程序设计教程 第8章 继承与派生 8.2.2 私有继承 #include "Circle2.h" using namespace std; void main() { Circle Cir1(100,200,10); Cir1.ShowCircle(); cout<<"area is:"<<Cir1.area()<<endl; Cir1.move(10,20); //同名覆盖 // Cir1.ShowXY(); //错误,ShowXY()继承为私有成员函数 } 运行结果 Centre of circle:(100,200) radius:10 area is:31415.9 (110,30) 程序解释 对比两个示例程序,可以看出:由于是私有继承,基类中的所有成员在派生类中都成为私有成员,因此派生类对象不能直接访问任何一个基类的成员。类Circle的对象Cir1调用的都是派生类自身的公有成员。 本例仅仅对派生类的实现作了适当的修改,基类和主程序部分没有做任何改动,程序运行的结果同前例。由此可见面向对象程序设计封装性的优越性,这正是面向对象程序设计可重用与可扩充性的一个实际体现。
8.2.3 保护继承 保护继承的特点: ①基类的公有成员和保护成员被继承后作为派生类的保护成员。 ②基类的私有成员在派生类中不能被直接访问。 C++语言程序设计教程 第8章 继承与派生 8.2.3 保护继承 保护继承的特点: ①基类的公有成员和保护成员被继承后作为派生类的保护成员。 ②基类的私有成员在派生类中不能被直接访问。 修改Circle2.h,将派生类的继承方式改为保护继承,其它部分不变: //circle3.h #include “piont.h” class Circle :protected point { //类成员定义 } 类 名 成 员 名 访问权限 Circle Point:: X,Y private 不可访问 move() public protected ShowXY() radius area() ShowCircle() Circle()
C++语言程序设计教程 第8章 继承与派生 8.2.3 保护继承 #include "Circle3.h" using namespace std; void main() { Circle Cir1(100,200,10); Cir1.ShowCircle(); cout<<"area is:"<<Cir1.area()<<endl; Cir1.move(10,20); //同名覆盖 // Cir1.ShowXY(); //错误,ShowXY()继承为保护成员函数 } 运行结果 Centre of circle:(100,200) radius:10 area is:31415.9 (110,30) 程序解释 private、protected两种继承方式下,基类所有成员在派生类中的访问属性都是完全相同的。即在派生类中可以访问基类的公有、保护成员不可访问基类的私有成员。 如果将派生类作为新的基类继续派生时, private、protected两种继承方式区别就出现了。假设类B以私有方式继承自类A,则无论B类以什么方式派生出类C,类C的成员和对象都不能访问间接从A类中继承来的成员。但如果类B是以保护方式继承自类A,那么类A中的公有和保护成员在类B中都是保护成员。类B再派生出类C后,如果是公有派生或保护派生,则类A中的公有和保护成员被类C间接继承后,类C的成员函数可以访问间接从类A中继承来的成员。即类A的成员可以沿继承树继续向下传播。
8.2.3 保护继承 【例8-2 】保护继承与保护成员的访问 C++语言程序设计教程 第8章 继承与派生 修改例8-1,除将基类Point的数据成员X和Y的访问属性改为protected外,又增加了一个派生类:Cylinder(圆柱体)类。Cylinder类保护继承自类circle。程序实现如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 //Point2.h #include<iostream> using namespace std; class Point { protected: int X,Y; public: Point(int X=0,int Y=0) this->X=X,this->Y=Y; } void move(int OffX, int OffY) X+=OffX, Y+=OffY; void ShowXY() cout<<"("<<X<<","<<Y<<")"<<endl; };
8.2.3 保护继承 C++语言程序设计教程 第8章 继承与派生 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 /********************************* * p8_2.cpp * * 从circle类派生出圆柱类(Cylinder) * *********************************/ #include"point2.h" const double PI=3.14159; class Circle :protected Point { protected: double radius; //半径 public: Circle(double R, int X, int Y):Point(X,Y) radius=R; } double area() //求面积 return PI*radius*radius; void ShowCircle() cout<<"Centre of circle:"; ShowXY(); cout<<"radius:"<<radius<<endl; }; class Cylinder: protected Circle private: double height; Cylinder(int X, int Y, double R, double H):Circle(R,X,Y) 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 { height=H; } double area() return 2*Circle::area()+2*PI*radius*height; double volume() return Circle::area()*height; void ShowCylinder() ShowCircle(); cout<<"height of cylinder:"<<height<<endl; }; void main() Cylinder CY(100,200,10,50); CY.ShowCylinder(); cout<<"total area:"<<CY.area()<<endl; cout<<"volume:"<<CY.volume(); 运行结果 Centre of circle:(100,200) radius:10 height of cylinder:50 total area :3769.11 volume : 15707.9
C++语言程序设计教程 第8章 继承与派生 8.2.3 保护继承 Circle保护继承自类Point,因此类Circle为子类,类Point为父类,对于该子类来讲,保护成员与公有成员具有相同的访问特性。所以派生类的成员函数ShowCircle()可以访问基类从基类继承而来的保护成员,当然它也可以调用从基类继承来的公有成员函数ShowXY()。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 /********************************* * p8_2.cpp * * 从circle类派生出圆柱类(Cylinder) * *********************************/ #include"point2.h" const double PI=3.14159; class Circle :protected Point { protected: double radius; //半径 public: Circle(double R, int X, int Y):Point(X,Y) radius=R; } double area() //求面积 return PI*radius*radius; void ShowCircle() cout<<"Centre of circle:"; ShowXY(); cout<<"radius:"<<radius<<endl; }; class Cylinder: protected Circle private: double height; Cylinder(int X, int Y, double R, double H):Circle(R,X,Y) 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 { height=H; } double area() return 2*Circle::area()+2*PI*radius*height; double volume() return Circle::area()*height; void ShowCylinder() ShowCircle(); cout<<"height of cylinder:"<<height<<endl; }; void main() Cylinder CY(100,200,10,50); CY.ShowCylinder(); cout<<"total area:"<<CY.area()<<endl; cout<<"volume:"<<CY.volume(); 当通过类Cylinder的对象CY调用成员函数area()时,由于对象CY拥有两个同名成员函数area(),一个是从其父类Circle继承来的,一个是类Cylinder自己新增的,二者函数体实现完全不同。类Circle的成员函数area()和派生类Cylinder新增的成员函数area()都具有类作用域,二者的作用范围不同,是相互包含的两个层,派生类在内层。由于,派生类Cylinder声明了一个和其父类circle成员同名的新成员area(),派生的新成员函数就覆盖了外层父类的同名成员函数,直接使用成员名只能访问到派生类自己新增的同名成员函数。C++利用同名覆盖原则,自动选择调用类Cylinder新增的成员函数area(),输出圆柱体的总的表面积,这再一次体现了继承机制所产生的程序重用性和可扩充性。 类Circle沿类的继承树继续派生出类Cylinder,继承方式依然为保护继承,因此,在类cylinder中,它间接从类Point中继承了四个保护成员:数据成员X、Y,以及成员函数move()、ShowXY();同时它也直接从其父类Circle中继承了3个类成员:数据成员radius, 成员函数ShowCircle()、area(),它们都以保护成员的身份出现在类Cylinder中。因此,在类Cylinder的成员函数ShowCylinder()中,不仅可以访问从父类Circle中直接继承来的成员函数ShowCircle(),而且可以访问沿继承树从基类Point中间接继承来的数据成员X和Y。
8.2.3 保护继承 三种继承方式下,基类成员在派生类中的访问控制属性总结如图: 基类属性 继承方式 public protected 第8章 继承与派生 8.2.3 保护继承 三种继承方式下,基类成员在派生类中的访问控制属性总结如图: 基类属性 继承方式 public protected private 不可访问
8.3 派生类的构造与析构 1.派生类构造函数的定义 派生类名(参数总表): 基类名1(参数表1),...,基类名m (参数表m), C++语言程序设计教程 第8章 继承与派生 8.3 派生类的构造与析构 1.派生类构造函数的定义 派生类名(参数总表): 基类名1(参数表1),...,基类名m (参数表m), 成员对象名1(成员对象参数表1),...,成员对象名n(成员对象参数表n) { 派生类新增成员的初始化; } 基类名1(参数表1),...,基类名m (参数表m)称为基类成员的初始化表。 成员对象名1(成员对象参数表1),...,成员对象名n(成员对象参数表n) 为成员对象的初始化表。 基类成员的初始化表与成员对象的初始化表构成派生类构造函数的初始化表。 在派生类构造函数的参数总表中,需要给出基类数据成员的初值、成员对象数据成员的初值、新增一般数据成员的初值。 在参数总表之后,列出需要使用参数进行初始化的基类名、成员对象名及各自的参数表,各项之间使用逗号分隔。 基类名、对象名之间的次序无关紧要,它们各自出现的顺序可以是任意的。在生成派生类对象时,程序首先会使用这里列出的参数,调用基类和成员对象的构造函数。。
8.3 派生类的构造与析构 2 单继承的构造与析构 什么时候需要定义派生类的构造函数? C++语言程序设计教程 第8章 继承与派生 8.3 派生类的构造与析构 什么时候需要定义派生类的构造函数? 如果基类定义了带有形参表的构造函数时,派生类就应当定义构造函数,提供一个将参数传递给基类构造函数的途径,保证在基类进行初始化时能够获得必要的数据。 如果基类没有定义构造函数,派生类也可以不定义构造函数,全部采用默认的构造函数,这时新增成员的初始化工作可以用其他公有成员函数来完成。 2 单继承的构造与析构 单继承时,派生类构造函数调用的一般次序如下: ① 调用基类构造函数; ② 调用内嵌成员对象的构造函数,调用顺序按照它们在类中定义的顺序 。 ③ 派生类自己的构造函数。 ④ 当派生类对象析构时,各析构函数的调用顺序正好相反。首先调用派生类析构函数(清理派生类新增成员);然后调用派生类成员对象析构函数(清理派生类新增的成员对象);最后调用基类析构函数(清理从基类继承来的基类子对象)。
8.3 派生类的构造与析构 【例8-3】单继承的构造与析构。 C++语言程序设计教程 第8章 继承与派生 8.3 派生类的构造与析构 【例8-3】单继承的构造与析构。 为了说明单继承的构造,由Point类派生出Circle类,再由两个同心Circle类对象与高度height构成空管Tube类。构成空管的两个同心圆的外圆从Circle类继承,内圆组合Circle类对象InCircle。Tube类的层次结构图如图:
8.3 派生类的构造与析构 当退出主函数之前,程序沿继承树自底向上依次调用各类的析构函数,其顺序与构造函数顺序正好相反。 C++语言程序设计教程 第8章 继承与派生 8.3 派生类的构造与析构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 /****************************** * p8_3.cpp * * 多层继承的构造函数与析构函数 * ******************************/ #include<iostream> using namespace std; class Point { private: int X,Y; public: Point(int X=0,int Y=0) this->X=X,this->Y=Y cout<<"point("<<X<<","<<Y<<") constructing..."<<endl; } ~Point() cout<<"point("<<X<<","<<Y<<") destructing..."<<endl; }; class Circle :protected Point protected: double radius; //半径 Circle(double R=0,int X=0, int Y=0):Point(X,Y) radius=R; cout<<"circle constructing, radius:"<<R<<endl; ~Circle() cout<<"circle destructing, radius:"<<radius<<endl; 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 class tube: protected Circle { private: double height; Circle InCircle; public: tube(double H,double R1, double R2=0, int X=0, int Y=0 ):InCircle(R2,X,Y),Circle(R1,X,Y) height=H; cout<<"tube constructing, height:"<<H<<endl; } ~tube() cout<<"tube destructing, height:"<<height<<endl; }; void main() tube TU(100,20,5); 当退出主函数之前,程序沿继承树自底向上依次调用各类的析构函数,其顺序与构造函数顺序正好相反。 定义了一个派生类Tube的对象TU,首先试图调用类Tube的构造函数; 运行结果 类Tube是派生类,由基类Circle派生,于是试图调用Circle类的构造函数; point(0,0) constructing... circle constructing, radius:20 point(0,0) constructing... circle constructing, radius:5 tube constructing, height:100 tube destructing, height:100 circle destructing, radius:5 point(0,0) destructing... circle destructing, radius:20 point(0,0) destructing... 类Circle的基类是Point, 沿继承树上溯至顶层基类Point,调用Point类的构造函数; Tube同时又是一个组合类,由对象InCircle组合而成,于是,再从顶层基类Point开始,依次调用调用Point类的构造函数、Circle的构造函数。
8.4 类型兼容 类型兼容是指在公有派生的情况下,一个派生类对象可以作为基类的对象来使用。类型兼容又称为类型赋值兼容或类型适应。 C++语言程序设计教程 第8章 继承与派生 8.4 类型兼容 类型兼容是指在公有派生的情况下,一个派生类对象可以作为基类的对象来使用。类型兼容又称为类型赋值兼容或类型适应。 在C++中,类型兼容主要指以下三种情况: ① 派生类对象可以赋值给基类对象。 ② 派生类对象可以初始化基类的引用。 ③ 派生类对象的地址可以赋给指向基类的指针。 【例8-4】演示类的兼容性。 前面我们定义了类Point,它公有派生出类Circle,后者进一步公有派生出类Cylinder。我们可以通过这个单继承的例子来验证类型兼容规则。
Pp调用了Point 类的成员函数ShowXY(), 显示了Point类对象的中心坐标值。 C++语言程序设计教程 第8章 继承与派生 8.4 类型兼容 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 /************************************** * p8_4.cpp * * 从circle类公有派生出圆柱类Cylinder * * 演示类的兼容性 * ****************************************/ #include"Circle.h" class Cylinder: public Circle { private: double height; public: Cylinder(int X, int Y, double R, double H):Circle(X,Y,R) height=H; } void ShowCylinder() ShowCircle(); cout<<"height of cylinder:"<<height<<endl; }; void main() Point P(1,1); //Point类对象 Circle Cir(20,20,15.5); //Circle类对象 Cylinder CY(300,300,15.5,50); //Cylinder类对象 Point *Pp; //point类指针 Pp=&P; //将基类对象地址赋给指向基类的指针 Pp->ShowXY(); Pp=&Cir; //将派生类对象地址赋给指向基类的指针 Pp=&CY; //将派生类对象地址赋给指向基类的指针 Circle & RC=CY; //Circle类引用引用了派生类Cylinder对象 RC.ShowXY(); P=Cir; //Circle类对象赋值给基类Point类对象 P.ShowXY(); 运行结果 (1,1) (20,20) (300,300) (300,300) (20,20) 定义了Point类型的指针Pp 指向了Point类对象 指向了Circle类对象 指向了Cylinder类对象 Pp调用了Point 类的成员函数ShowXY(), 显示了Point类对象的中心坐标值。 调用了Point 类的成员函数ShowXY(), 显示了Circle类对象的中心坐标值。 调用了Point 类的成员函数ShowXY(), 显示了Cylinder类对象的中心坐标值。
8.4 类型兼容 如将上述程序改为: 可将display()的参数改为引用形式: C++语言程序设计教程 第8章 继承与派生 8.4 类型兼容 如将上述程序改为: void display(Point p) { p.ShowXY(); } void main() Point P(1,1); //Point类对象 Circle Cir(20,20,15.5); //Circle类对象 Cylinder CY(300,300,15.5,50); //Cylinder类对象 display(P); //显示对象P的中心坐标 display(Cir); //显示对象Cir的中心坐标 display(CY); //显示对象CY的中心坐标 可将display()的参数改为引用形式: void display(Point& p) { p.ShowXY(); } 根据C++类型兼容规则, p可以引用任何point的公有派生类对象。 还可以将display()形参改为基类指针: void display(Point* p) { p->ShowXY(); } 这样,可以分别把基类对象P、派生类Circle的对象Cir和派生类Cylinder的对象CY的地址作为实参传给基类类型指针,由C++编译器实现隐式的类型转换。
C++语言程序设计教程 第8章 继承与派生 8.5 多继承 多继承(multiple inheritance,MI)是指派生类具有两个或两个以上的直接基类(direct class)。 8.5.1 多继承的构造与析构 多继承时派生类构造函数执行的一般次序如下: ① 调用各基类构造函数;各基类构造函数调用顺序按照基类被继承时声明的顺序,从左向右依次进行。 ② 调用内嵌成员对象的构造函数;成员对象的构造函数调用顺序按照它们在类中定义的顺序依次进行。 ③ 调用派生类的构造函数; 注意: 在继承层次图中,处于同一层次的各基类构造函数的调用顺序取决于定义该派生类时所指定的各基类的先后顺序,与派生类构造函数定义时初始化表中所列的各基类构造函数的先后顺序无关。 对同一个基类,不允许直接继承两次。
C++语言程序设计教程 第8章 继承与派生 8.5.2 二义性问题 一般来说,在派生类中对于基类成员的访问应该是唯一的,但是,由于多继承中派生类拥有多个基类,如果多个基类中拥有同名的成员,那么,派生类在继承各个基类的成员之后,当我们调用该派生类成员时,由于该成员标识符不唯一,出现二义性,编译器无法确定到底应该选择派生类中的哪一个成员,这种由于多继承而引起的对类的某个成员访问出现不唯一的情况就称为二义性问题。 【例8-5】多继承的二义性。 例如:我们可以定义一个小客车类car和一个小货车类Wagon,它们共同派生出一个客货两用车类StationWagon。StationWagon继承了小客车的特征,有座位seat,可以载客;又继承了小货车的特征,有装载车厢load, 可以载货。程序实现如下: 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Car //小客车类 { private: int power; //马力 int seat; //座位 public: Car(int power,int seat) this->power=power,this->seat=seat; } void show() cout<<"car power:"<<power<<" seat:"<<seat<<endl; };
C++语言程序设计教程 第8章 继承与派生 8.5.2 二义性问题 运行结果 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 class Wagon //小货车类 { private: int power; //马力 int load; //装载量 public: Wagon(int power,int load) this->power=power,this->load=load; } void show() cout<<"wagon power:"<<power<<" load:"<<load<<endl; }; class StationWagon :public Car, public Wagon //客货两用车类 StationWagon(int power, int seat,int load) : Wagon(power,load), Car(power,seat) void ShowSW() cout<<"StationWagon:"<<endl; Car::show(); Wagon::show(); void main() StationWagon SW(105,3,8); //SW.show(); //错误,出现二义性 SW.ShowSW(); StationWagon: car power:105 seat:3 wagon power:105 load:8 小客车类Car和小货车类Wagon共同派生出客货两用车类StationWagon,后者继承了前者的属性power和行为show(), 当通过StationWagon类的对象SW访问show ()时,程序出现编译错误。这是因为基类Car和Wagon各有一个成员函数show (),在其共同的派生类StationWagon中就有两个相同的成员函数,而程序在调用时无法决定到底应该选择哪一个成员函数
8.5.2 二义性问题 通常有两种方法可以解决: (1) 成员名限定 通过类的作用域分辨符明确限定出现歧义的成员是继承自哪一个基类。 C++语言程序设计教程 第8章 继承与派生 8.5.2 二义性问题 通常有两种方法可以解决: (1) 成员名限定 通过类的作用域分辨符明确限定出现歧义的成员是继承自哪一个基类。 例如:程序第47、48两行使用了Car::show()与Wagon::show()来表明调用哪个类的show(). (2) 成员重定义 在派生类中新增一个与基类中成员相同的成员,由于同名覆盖,程序将自动选择派生类新增的成员。 可以对派生类StationWagon的ShowSW()改名为show()。这样, 类StationWagon中的show()覆盖了基类中的两个同名的show(),使用SW.show();时不会出现二义性问题。
C++语言程序设计教程 第8章 继承与派生 8.6 虚基类 在多继承中,在派生类的对象中,同名数据成员在内存中同时拥有多个拷贝,同一个成员函数会有多个映射,出现二义性,这种二义性为间接二义性。 【例8-6】多重继承的间接二义性。 假定类Car、Wagon从共同的基类Automobile(汽车)派生出来,程序如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 /************************** * p8_6.cpp * * 多继承的二义性 * ***************************/ #include<iostream> using namespace std; class Automobile //汽车类 { private: int power; //马力 public: Automobile(int power) this->power=power; } void show() cout<<" power:"<<power; };
8.6 虚基类 C++语言程序设计教程 第8章 继承与派生 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 class Car: public Automobile //小客车类 { private: int seat; //座位 public: Car(int power,int seat):Automobile(power) this->seat=seat; } void show() cout<<"car:"; Automobile::show(); cout<<" seat:"<<seat<<endl; }; class Wagon: public Automobile //小货车类 int load; //装载量 Wagon(int power,int load):Automobile(power) this->load=load; cout<<"wagon:"; cout<<" load:"<<load<<endl;
C++语言程序设计教程 第8章 继承与派生 8.6 虚基类 一个StationWagon类对象中,具有多个从不同途径继承来的同名的数据成员power。 一方面占据了内存空间,另一方面由于在内存中有不同的拷贝而可能造成数据不一致。 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 class StationWagon :public Car, public Wagon //客货两用车类 { public: StationWagon(int CPower, int WPower,int seat,int load) :Wagon(WPower,load), Car(CPower,seat) } void show() cout<<"StationWagon:"<<endl; Car::show(); Wagon::show(); }; void main() StationWagon SW(105,108,3,8); SW.show(); 将car::power设成105,Wagon::power设成108,那么StationWagon的power值究竟应为多少? 运行结果 StationWagon: car power:105 seat:3 wagon power:108 load:8
C++语言程序设计教程 第8章 继承与派生 8.6.1 虚基类的定义 为了解决从不同途径继承来的同名的数据成员在内存中有不同的拷贝造成数据不一致问题,将共同基类设置为虚基类。这时从不同的路径继承过来的同名数据成员在内存中就只有一个拷贝,同一个函数名也只有一个映射。这样不仅就解决了二义性问题,也节省了内存,避免了数据不一致的问题。 虚基类的定义是在融合在派生类的定义过程中的,其定义格式如下: class 派生类名:virtual 继承方式 基类名 其中: virtual是关键字,声明该基类为派生类的虚基类。 在多继承情况下,虚基类关键字的作用范围和继承方式关键字相同,只对紧跟其后的基类起作用。 声明了虚基类之后,虚基类在进一步派生过程中始终和派生类一起,维护同一个基类子对象的拷贝。
8.6.1 虚基类的定义 使用虚基类,将程序p8_6.cpp修改如下: 第8章 继承与派生 8.6.1 虚基类的定义 使用虚基类,将程序p8_6.cpp修改如下: class Car: virtual public Automobile //小客车类 class Wagon: virtual public Automobile //小货车类 使用虚基类后的继承层次图与类成员图如下: 这时,从Automobile中不同途径继承来的power、show()在StationWagon中只有一个拷贝。
C++语言程序设计教程 第8章 继承与派生 8.6.2 虚基类的构造与析构 C++将建立对象时所使用的派生类称为最远派生类。对于虚基类而言,由于最远派生类对象中只有一个公共虚基类子对象,为了初始化该公共基类子对象,最远派生类的构造函数要调用该公共基类的构造函数,而且只能被调用一次。 虚基类的构造函数调用分三种情况: (1) 虚基类没有定义构造函数 程序自动调用系统缺省的构造函数来初始化派生类对象中的虚基类子对象。 (2) 虚基类定义了缺省构造函数 程序自动调用自定义的缺省构造函数和析构函数。 (3) 虚基类定义了带参数的构造函数 这种情况下,虚基类的构造函数调用相对比较复杂。因为虚基类定义了带参数的构造函数,所以在整个继承结构中,直接或间接继承虚基类的所有派生类,都必须在构造函数的初始化表中列出对虚基类的初始化。但是,只有用于建立派生类对象的那个最远派生类的构造函数才调用虚基类的构造函数,而派生类的其它非虚基类中所列出的对这个虚基类的构造函数的调用被忽略,从而保证对公共虚基类子对象只初始化一次。 C++同时规定,在初始化列表中同时出现对虚基类和非虚基类构造函数的调用,虚基类的构造函数先于非虚基类的构造函数的执行。 虚基类的析构顺序与构造顺序完全相反,最开始析构的是最远派生类自身,最后析构的是虚基类。尽管从程序上看,虚基类被析构多次,实际上只有在最后一次被执行,中间的全部被忽略。。
8.6.2 虚基类的构造与析构 将程序p8_6.cpp修改后,在编译下列语句时显示编译错误: 第8章 继承与派生 8.6.2 虚基类的构造与析构 将程序p8_6.cpp修改后,在编译下列语句时显示编译错误: StationWagon(int CPower, int WPower, int seat, int load) :Wagon(WPower, load), Car(CPower, seat) 系统在调用StationWagon的构造函数时,首先调用虚基类的构造函数,以便初始化虚基类中的数据成员。由于在StationWagon的构造函数中没有列出基类构造函数的调用形式,系统调用虚基类的默认构造函数Automobile()。但是,在类Automobile中,默认构造函数被Automobile(int)取代,没有Automobile()可调用,故而出错。一个避免出错的方法是将虚基类的构造函数Automobile(int)更改成带默认形参值的形式:Automobile(int=0),但是此时虚基类中的数据成员无法初始化。 为了是初始化虚基类中的数据成员,需要在最远派生类的构造函数中定义对虚基类构造函数调用的初始化列表。 将程序p8_6.cpp修改如下 【例8-7】虚基类的构造函数。 假定类Car、Wagon从共同的基类Automobile(汽车)派生出来,程序如下:
8.6.2 虚基类的构造与析构 C++语言程序设计教程 第8章 继承与派生 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 /************************** * p8_7.cpp * * 虚基类的构造函数 * ***************************/ #include<iostream> using namespace std; class Automobile //汽车类 { private: int power; //马力 public: Automobile(int power) this->power=power; cout<<"Automobile constructing..."<<endl; } void show() cout<<" power:"<<power; };
8.6.2 虚基类的构造与析构 C++语言程序设计教程 第8章 继承与派生 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 class Car: virtual public Automobile //小客车类 { private: int seat; //座位 public: Car(int power,int seat):Automobile(power) this->seat=seat; cout<<"Car constructing..."<<endl; } void show() cout<<"car:"; Automobile::show(); cout<<" seat:"<<seat<<endl; }; class Wagon: virtual public Automobile //小货车类 int load; //装载量 Wagon(int power,int load):Automobile(power) this->load=load; cout<<"Wagon constructing..."<<endl; cout<<"wagon:"; cout<<" load:"<<load<<endl;
8.6.2 虚基类的构造与析构 C++语言程序设计教程 第8章 继承与派生 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 class StationWagon :public Car, public Wagon //客货两用车类 { public: StationWagon(int CPower,int WPower, int seat,int load) :Automobile(CPower),Wagon(WPower,load), Car(CPower,seat) cout<<"StationWagon constructing..."<<endl; } void show() cout<<"StationWagon:"<<endl; Car::show(); Wagon::show(); }; void main() StationWagon SW(105,108,3,8); SW.show(); 运行结果 Automobile constructing... Car constructing... Wagon constructing... StationWagon constructing... StationWagon: car power:105 seat:3 wagon power:105 load:8
C++语言程序设计教程 第8章 继承与派生 8.6.2 虚基类的构造与析构 结果分析: 虚基类Automobile定义了一个带参数的构造函数,所以其派生类StationWagon、 Car和Wagon都必须各自定义带参数的构造函数,当建立最远派生类StationWaggon的对象SW时,通过类StationWagon的构造函数的初始化列表,似乎虚基类构造函数Automobile()、直接基类Car和Wagon的构造函数Car ()和Wagon ()都被调用,而后者有分别嵌套调用了虚基类的构造函数Automobile(),这样,C++编译器岂不是会将从虚基类继承来的数据成员power连续初始化三次?但实际上,只有虚基类的构造函数Automobile()被执行,而最远派生类StationWagon的直接基类Car和Wagon的构造函数对虚基类构造函数的嵌套调用将自动被忽略,这样,power只会被初始化一次。
C++语言程序设计教程 第1章 C++编程简介 8.7 本章小结 ◇ 通过继承,派生类在原有的类的基础上派生出来,它继承原有类的属性和行为,并且可以扩充新的属性和行为,或者对原有类中的成员进行更新,从而实现了软件重用。 ◇ 继承方式有public、protected、private,各种继承方式下,基类的私有成员在派生类中不可存取。public继承方式基类成员的访问控制属性在派生类中不变,protected继承方式基类成员的访问控制属性在派生类中为protected, private继承方式基类成员的访问控制属性在派生类中为private。 ◇ 在派生类建立对象时,会调用派生类的构造函数,在调用派生类的构造函数前,先调用基类的构造函数。派生类对象消失时,先调用派生类的析构函数,然后再调用基类的析构函数。 ◇ 类型兼容是指在公有派生的情况下,一个派生类对象可以作为基类的对象来使用:派生类对象可以赋值给基类对象,派生类对象可以初始化基类的引用,派生类对象的地址可以赋给指向基类的指针。 ◇ 多继承时,多个基类中的同名的成员在派生类中由于标识符不唯一而出现二义性。在派生类中采用成员名限定或重定义具有二义性的成员来消除二义性。 ◇ 在多继承中,当派生类的部分或全部直接基类又是从另一个共同基类派生而来时,可能会出现间接二义性。消除间接二义性除了采用消除二义性的两种方法外,可以采用虚基类的方法。