第十章继承
继承是面向对象程序设计的基础的概念之一,是解决软件重用(reuse)的一种方法。本章将先介绍与继承相关的一些基础概念,然后介绍C++中实现继承的方法。根据不同的继承方式以及基类成员的访问控制权限,讨论如何访问基类的各类成员。接着对从多个基类派生子类的多继承进行讨论。由于经过继承与派生之后,派生类对象的成分既有自己特有的成分,也有通过继承而获得的成分,因此派生类的构造函数与析构函数要承担这两种成分的构造任务,如何完成两种成分的构造任务将会被介绍。同时,要讨论什么是二义性,以及解决二义性的方法,支配规则就是解决二义性的方法之一。在多继承情况下,每个基类都会在派生类对象中产生自己的基类子对象,派生类在继承路径上如果遇到共同基类,其派生类对象中就会产生该基类的多个基类子对象,使基类子对象惟一就是虚基类要解决的问题。由于派生类对象成分的多样性,派生类的赋值以及初始化语义值得认真研究。
10.1继承
继承机制规定:派生类可以继承基类的数据和操作,同时派生类也可以根据自身的特点新增自己的数据和操作。从共性与个性的关系上看,基类抽象出了其所有派生类的共同特征,而派生类则通过新增自己的数据和操作来体现其个性特点。通过继承可以在两个方面实现软件重用。其一,通过继承允许派生类共享基类的数据和操作,实现软件重用。此时,被复用的部分是基类的数据和操作。另外,通过公有继承,可以使派生类成为基类的子类型。从而使原本作用于基类对象的代码变成可以重用于其派生类对象,达到基类对象和派生类对象共享这段代码,或一段代码可以被多种类型对象共享的目的。
根据从一个基类还是从多个基类派生,继承分为单继承与多继承。声明单继承的的一般形式是:
class 派生类名 : 继承方式基类名
{
派生类新增成员的声明
};
其中,class是派生类的类说明符,类说明符也可以用struct。以class关键字说明时,派生类中没有被访问控制修饰符修饰的成员(缺省修饰的成员)均为类的私有成员,而以struct关键字说明时,派生类中缺省修饰的成员均为类的公有成员。在继承中,类说明符不能用union,因为用union声明的类既不能作为基类,也不能作为派生类。派生类名应该是C++的合法标识符。
继承方式可以是:public、protected、或private。继承方式为public的继承称为公有继承,继承方式为protected的继承称为保护继承,继承方式为private的继承称为私有继承。基类名应该是一个非union声明类的类名。
派生类新增成员的声明用于声明派生类新增的数据成员和成员函数。它们又可以进一步被类中的访问控制修饰符public、protected和private修饰。从而形成派生类中的公有数据成员、保护数据成员和私有数据成员三类数据成员,以及派生类中的公有成员函数、保护成员函数和私有成员函数三类成员函数。
下面以三维空间中的点、线为例说明如何由点类通过继承得到线类。
例10-1 由三维空间中的点类通过继承得到派生的三维空间中的线类。
#include "iostream.h"
#include "math.h"
struct D3Point
{
D3Point(int x=0,int y=0,int z=0){X0=x;Y0=y;Z0=z;}
D3Point(D3Point& p){X0=p.X0;Y0=p.Y0;Z0=p.Z0;}
~D3Point(){}
void Set(int x,int y,int z){X0=x;Y0=y;Z0=z;}
void Move(int dx,int dy,int dz){X0+=dx;Y0+=dy;Z0+=dz;}
void Show(){cout<<"X0="< int GetY0(){return Y0;}; int Z0; protected: int Y0; private: int X0; }; struct D3Line : public D3Point { D3Line(){X1=Y1=Z1=0;} D3Line(int x,int y,int z,int x1,int y1,int z1){ Set(x,y,z); X1=x1;Y1=y1;Z1=z1; } D3Line(D3Line& r); ~D3Line(){}; void Show(); int GetX1(){return X1;}; int GetY1(){return Y1;}; double Length(); int Z1; protected: int Y1; private: int X1; }; D3Line::D3Line(D3Line& r) : D3Point(r) { X1=r.X1;Y1=r.Y1;Z1=r.Z1; } double D3Line::Length() { int dx2,dy2,dz2; dx2=(GetX1()-GetX0())*(GetX1()-GetX0()); dy2=(Y1-Y0)*(Y1-Y0); dz2=(Z1-Z0)*(Z1-Z0); return sqrt(dx2+dy2+dz2); } void D3Line::Show() { D3Point::Show(); cout<<"X1="< } void main(void) { D3Point P1(10,20,30),P2(100,200,300); D3Line L(10,20,30,100,200,300); L.Show(); cout<<"L.Z0="< cout<<"the length of the line is "< cout<<"size of D3Point is "< cout<<"size of D3Line is "< } 程序的运行结果为: X0=10,Y0=20,Z0=30 X1=100,Y1=200,Z1=300 L.Z0=30 the length of the line is 336.749 size of D3Point is 12 size of D3Line is 24 程序中通过:struct D3Line : public D3Point {…};说明D3Line类是从D3Point类继承而得。此时,D3Point类的数据成员X0、Y0、Z0自动成为D3Line类的数据成员,它们称为派生类中的基类子对象(subobject),它们构成空间直线的一个端点。同时,D3Line 类又新增了数据成员X1、Y1、Z1,它们构成了空间直线的另一个端点。程序运行结果中size of D3Point is 12和size of D3Line is 24可以说明这一点。 基类的成员函数也可以被派生类对象使用。当然,对基类数据成员和成员函数的访问与操作要受到继承方式和基类访问控制权限的约束,这个在下一节详细讨论。另外,派生类中还新增了自身的成员函数。在派生类成员函数Show中,通过D3Point::Show();调用,显示派生类的数据成员X0、Y0、Z0,而派生类的数据成员X1、Y1、Z1则必须由D3Line类Show函数中的cout输出。此时,D3Point::Show()称为通过成员名限定方式调用D3Point 类的Show操作。D3Point::是必须的,因为基类和派生类中都有Show操作。加成员名限定D3Point::将调用基类的Show操作;不加成员名限定,则调用派生类的Show,从而形成无限循环调用。 另外,派生类的拷贝构造函数通过D3Line::D3Line(D3Line& r) : D3Point(r)列出了对基类拷贝构造函数的调用,用于初始化派生类中的基类子对象X0、Y0、Z0。这方面的内容将在本章第4节进行。这里要强调的是:调用基类的拷贝构造函数D3Point(D3Point& p),初始化的却是派生类对象的数据成员X0、Y0、Z0。同时,D3Point(r)中实参是关于派生类D3Line的引用,潜在的引入了子类型的概念。公有继承产生的派生类对象可以用于基类对象适用的场合。子类型将在第11章介绍。 将X0、Y0、Z0,以及X1、Y1、Z1分别声明为基类和派生类的私有数据成员、保护数据成员和公有数据成员。目的是为了在后面能够解释不同的继承方式,以及基类中的不同访问控制对类中成员访问产生的影响。 将上面的内容和例子概括起来有如下几点; 1.在派生类声明中通过“: public D3Point”可以得到从基类的公有继承。 2.派生类中的数据结构由新增数据成员和派生得到的基类子对象组成。 3.对基类成员的访问要受到继承方式和基类访问控制的约束。 4.设计派生类的构造函数、析构函数、拷贝构造函数、赋值操作等都必须考虑到基类子对象。 5.本例引入了成员名限定的概念。 6.本例引入了子类型的概念。 10.2 对基类成员的访问 10.2.1 继承方式和访问控制 继承的引入使C++程序的结构发生了很大变化。此时,假设要讨论的类为当前类,则考虑继承和派生时一个完整的C++应用程序往往由这样一些模块组成: 在第5章中,把其它类和其它类的各个派生类、mian函数、以及不属于任何类的各个函数称为以当前类(即基类)为参考点的水平模块部分。现在考虑继承与派生情况,把当前类的直接基类和间接基类,称为以当前类为参考点的垂直向上模块部分。把当前类的各个派生类,包括当前类的直接和间接派生类,称为以当前类为参考点的垂直向下模块部分。在下面的讨论中,如果不做说明,简称垂直向下模块部分为垂直模块。在这种划分情况下,首先要明确的是基类成员通过继承会派生到派生类中,成为派生类中的基类子对象。并且要着重指出的是:这里所指的派生类既包括直接派生类,也包括间接派生类。特别是对于间接派生类对象,其数据结构中既包含其直接基类的基类子对象,也包括间接基类的基类子对象,同时还包括自身新增的数据成员。至于间接派生类对象中的直接基类和间接基类中的非静态成员函数则还是按照统一存储,共享使用的原则处理。 例10-2 基类对象,直接派生类对象和间接派生类对象的数据结构演示的例子。 #include "iostream.h" struct A{ int X;}; struct B : public A { int Y;}; struct C : public B { int Z; }; void main(void) { A a; B b; C c; a.X = 10; b.X = 20; b.Y = 30; c.X = 40; c.Y = 50; c.Z = 60; cout<<"a.X="< cout<<"b.X="< struct D3Point { D3Point(int x=0,int y=0,int z=0){X0=x;Y0=y;Z0=z;} int GetX0(){return X0;}; int GetY0(){return Y0;}; void Show(){cout<<"X0="< int Z0; protected: int Y0; private: int X0; }; struct D3Line : public D3Point { D3Line(){X1=Y1=Z1=0;} D3Line(D3Point& p1,D3Point& p2); void Show(); int GetX1(){return X1;}; int GetY1(){return Y1;}; int Z1; protected: int Y1; private: int X1; }; D3Line::D3Line(D3Point& r1,D3Point& r2) : D3Point(r1) { X1=r2.GetX0();Y1=r2.GetY0();Z1=r2.Z0; } void D3Line::Show() { cout<<"X0="< cout<<"X1="< } struct D3Plane : public D3Line { D3Plane(){X2=Y2=Z2=0;} D3Plane(D3Line& r1,D3Point& r2); void Show(); int GetX2(){return X2;}; int GetY2(){return Y2;}; int Z2; protected: int Y2; private: int X2; }; D3Plane::D3Plane(D3Line& r1,D3Point& r2) : D3Line(r1) { X2=r2.GetX0();Y2=r2.GetY0();Z2=r2.Z0; } void D3Plane::Show() { cout<<"X0="< cout<<"X1="< cout<<"X2="< } void main(void) { D3Point P1(10,20,30),P2(100,200,300),P3(400,500,600); P1.Z0=P2.Z0=P3.Z0=50; D3Line L(P1,P2); L.Show(); L.Z0=L.Z1=250; D3Plane PL(L,P3); PL.Z2=800; PL.Show(); cout<<"size of D3Point is "< cout<<"size of D3Line is "< cout<<"size of D3Plane is "< 程序的运行结果为: X0=10,Y0=20,Z0=50 X1=100,Y1=200,Z1=50 X0=10,Y0=20,Z0=250 X1=100,Y1=200,Z1=250 X2=400,Y2=500,Z2=800 size of D3Point is 12 size of D3Line is 24 size of D3Plane is 36 在水平的main函数部分,P1.Z0=P2.Z0=P3.Z0=50;语句是访问三个空间点对象的公有数据成员;L.Z0=L.Z1=250;语句中L.Z1访问的是空间直线对象的公有数据成员,而L.Z0访问的是空间直线对象的通过继承得到的基类子对象中的公有数据成员;而PL.Z2=800;语句访问的是空间平面对象的公有数据成员,如果使用PL.Z0,或PL.Z1,这种使用也是合法的。但是绝对不能访问各个类中的保护成员或私有成员。例如:在D3Line类的构造函数中,引用对象r2来自水平部分,因此通过X1=r2.GetX0();Y1=r2.GetY0();语句来实现用r2的X0和Y0数据成员的值来初始化当前对象的数据成员X1和Y1。因此,在水平部分只能调用各个类提供的接口,即各个类的公有成员函数。 在公有继承情况下,D3Point类中私有数据成员X0通过继承成为D3Line类对象中的私有数据成员,对它的访问必须通过接口函数GetX0()来访问,如D3Line::Show函数中的GetX0();而其它成员都可以直接进行访问。同理,D3Point类中私有数据成员X0和D3Line 类中私有数据成员X1通过继承成为D3Plane类对象中的私有数据成员,在D3Plane类中,对X0和X1必须通过接口函数GetX0(),GetX1()来访问,而其它成员都可以直接进行访问。 size of D3Point is 12,size of D3Line is 24和size of D3Plane is 36分别说明了D3Point类、D3Line类、以及D3Plane类数据成员所占内存的大小分别是12个字节、24个字节和36个字节。 另外,D3Line::D3Line(D3Point& r1,D3Point& r2) : D3Point(r1)中的D3Point(r1)将调用D3Point的缺省拷贝构造函数去初始化D3Line类中的D3Point类的基类子对象的数据成员X0、YO、以及Z0。 同理,D3Plane::D3Plane(D3Line& r1,D3Point& r2) : D3Line(r1)中的D3Line(r1)也将调用D3Line的缺省拷贝构造函数去初始化D3Plane类中的D3Line类的基类子对象的数据成员X0、YO、、Z0以及X1、Y1、Z1。 10.2.3 私有继承 如果将例10-3中的公有继承改为私有继承: struct D3Point { … //同例10-3 }; struct D3Line : private D3Point { … //同例10-3 }; struct D3Plane : private D3Line { … //同例10-3 }; 则程序在编译时会报告4个出错信息。如:不能在D3Plane中的Show函数中调用GetX0函数,也不能直接访问Y0、Z0,以及在main函数中不能用L.Z0的表达式直接访问对象L 的数据成员Z0。 原因何在呢?在D3Plane中的Show函数中有下面语句: cout<<"X0="< GetX0是D3Point类中的公有成员,但通过私有继承之后已经成为D3Line中的私有成员,此时在D3Line中尚可直接调用该函数,但是在D3Line的派生类中已经不能直接访问该函数,即便是:D3Plane从D3Line类公有继承也不行。类似的,D3Point类中的保护成员Y0、公有成员Z0通过私有继承已经成为D3Line类中的私有成员,在D3Line类的派生类中不能直接访问他们。由此可见,私有继承就象一堵墙,阻止了在间接派生类中直接访问间接基类中的任何成员。间接派生类唯一的办法是通过自己的基类(即直接派生类)提供的接口来间接访问它间接基类中的成员。 另外,由于私有继承,Z0在D3Line类访问属性已经变为私有属性,水平部分当然不能直接访问它。此时要在水平部分的main函数中去掉对L.Z0的使用: 例10-4 将例10-3中的公有继承改为私有继承后修改程序的例子。 void main(void) { D3Point P1(10,20,30),P2(100,200,300),P3(400,500,600); P1.Z0=P2.Z0=P3.Z0=50; D3Line L(P1,P2); L.Show(); L.Z1=250;// 去掉了对L.Z0的使用 D3Plane PL(L,P3); PL.Z2=800; PL.Show(); cout<<"size of D3Point is "< cout<<"size of D3Line is "< cout<<"size of D3Plane is "< } 同时对D3Plane类的Show函数做如下修改: void D3Plane::Show() { D3Line::Show(); cout<<"X2="< } 在上面的Show函数中,通过成员名限定表达式D3Line::Show()对D3Line类的Show函数进行了调用。D3Line::Show()应该理解为this->D3Line::Show(),调用的结果是显示D3Line 类对象中的X0、Y0、Z0和X1、Y1、Z1。这就是通过D3Plane类的直接基类D3Line中的操作去访问D3Plane类的间接基类D3Point中的成员的方法。如果D3Line类不提供相应的操作,则在D3Plane类将无法访问D3Point类中的任何成员。 10.2.4 保护继承 如果将例10-3中的公有继承改为保护继承,则程序在编译时会报告1个出错信息。即 在main函数中的L.Z0=L.Z1=250;语句中不能用L.Z0的表达式直接访问对象L的数据成员Z0。 道理很简单,D3Point类中公有数据成员通过保护继承已经成为D3Line中的保护成员,此时在D3Line类和D3Line类的派生类中可直接调用该函数,但是在main函数这个水平模块部分不能直接访问它,因此出错。由于D3Point类中没有提供设置Z0的值的公有成员函数,因此唯一的解决办法是取消使用L.Z0表达式。 基类中由protected修饰的成员称为类的保护成员。另外,基类中的公有成员和保护成员在保护继承的作用下会在派生类中产生保护的基类子对象,即保护的基类子对象的各个成员的访问属性是保护属性。总的来讲,在保护成员所在类中可以直接访问类中的保护成员;在保护的基类子对象所在类中可以直接访问保护的基类子对象的各个成员。在水平模块中则既不能访问类的保护成员也不能访问派生类中产生保护基类子对象中的成员。 例10-5访问类的保护成员以及派生类的保护基类子对象的各个成员的应用程序举例。 #include "iostream.h" class A { public: int Y; protected: int X; }; class B : public A { public: void f1(); }; void B::f1() { X=10; cout<<"X="< } class C : protected A { public: void f2(); }; void C::f2() { X=20; cout<<"X="< } class D : protected C { public: void f3(); void f4(); // void f5(); }; void D::f3() { A a; a.Y=300; X=30; cout<<"X="< cout<<"a.Y="< } void D::f4() { B b; b.Y=400; cout<<"b.Y="< } /*void D::f5() { C c; c.Y=500; cout<<"c.Y="< }*/ void main(void) { A a; B b; C c; D d; //a.X=100; //b.X=200; //c.X=300; b.f1(); c.f2(); d.f3(); d.f4(); //d.f5(); } 程序中注释掉的语句和函数都是非法的。首先,main函数中的a.X=100; b.X=200;以及c.X=300;都是企图在水平模块访问类中保护成员,因此非法。其次,f1函数中的X=10; f2函数中的X=20;以及f3函数中的X=30;都是分别对B、C、D类中通过继承得到的保护的基类子对象的数据成员X的赋值操作。如:X=10应该理解为this->X=10。 函数f3中的a.Y=300;是对类A对象公有数据成员的赋值操作,合法。函数f4中的b.Y=400;是对类B对象公有的基类子对象的数据成员X的赋值操作,因此也合法。而函数f5中的c.Y=500;为非法赋值操作的原因是:类C从类A中保护继承,类A中的公有数据成员Y继承到类C中后,成为类C的保护基类子对象成员,其访问属性是保护属性,对它的 直接访问只能在类C中进行,在类C的派生类D中对类C对象保护成员的访问C++不支持。正确的使用方法应该对程序进行如下修改: class C : protected A { public: void f2(); void f5(); }; void C::f5() { C c; c.Y=500; X=50; cout<<"X="< cout<<"c.Y="< } 此时对类C对象的成员c.Y的访问是在类C中进行,因此合法,可以编译通过并且得到预期的运行结果。 10.3多继承 上面讨论的继承中,派生类的基类都只有一个,因此称为单继承。如果派生类从多个基类派生,则称为多继承。声明多继承的的一般形式是: class 派生类名 : 继承方式基类名1,…,继承方式基类名n { 派生类新增成员的声明 }; 其中,关于派生类的类说明符可以是class,也可以是struct,但不能是union的解释与单继承中解释相同。派生类名也应该是C++的合法标识符。继承方式同样可以是:public、protected、或private,它们的含义也与单继承相同。基类名1,…,基类名n 是派生类的n个基类的类名。关于派生类新增成员的声明的解释也可以参看单继承部分的解释。 多继承可以看成是单继承的扩充。在多继承中,每个基类与派生类的继承关系可以通过将这个基类与派生类看成是一个单继承来进行讨论。此时,以n个基类中某一个基类为当前类,则它的直接派生类和间接派生类将构成程序模块的垂直部分,而另外的n-1个基类和其余部分都属于程序模块中的水平部分。对当前类各种访问属性的成员的访问,对当前类在直接派生类和间接派生类产生的基类子对象中各种访问属性的成员的访问,都可以参照单继承中规则进行处理。 例10-6 多继承的程序举例 #include "iostream.h" struct A { public: int pub_a; void SetA(int a1,int a2){pub_a=a1;pro_a=a2;} int pro_a; }; struct B { public: int pub_b; void SetB(int b1,int b2){pub_b=b1;pro_b=b2;} void fb(void); protected: int pro_b; }; void B::fb(void) { A a; a.SetA(10,20); cout<<"a.pub_a="< //cout<<"a.pro_a="< struct C :public A,public B { public: int pub_c; void SetC(int c1,int c2,int c3,int c4,int c5,int c6); void ShowC(void); protected: int pro_c; }; void C::SetC(int c1,int c2,int c3,int c4,int c5,int c6) { SetA(c1,c2); SetB(c3,c4); pub_c=c5;pro_c=c6; } void C::ShowC(void) {