C++继承与派生
遗传与变异是生物进化的基础,面向对象的程序设计借鉴了这种机制,为类代码提供一种新的,也是最为重要的一种代码重用形式,这就是类的继承与派生。设计新类的时候可以继承已有的类。这个已有的类被称为基类或父类,设计出来的新类是对基类继承来的功能进行升级改造,这被称为对基类进行派生。通过继承与派生得到的新类称为派生类或子类。
一、派生类的定义
按照来源不同,派生类的成员可分为两种:一是从基类继承来的成员,称为基类成员,二是定义时新增的成员,称为新增成员。
1、定义派生类额语法形式
class 派生类名:继承方式1 基类1,继承方式2 基类2 ... ... //派生类声明部分
{
public:
新增公有成员
protected:
新增保护成员
private:
新增私有成员
};
各新增函数成员的完整定义 //派生类实现部分
派生类定义语法细则:
1)定义派生类时,派生类名后添加继承列表,在声明部分的大括号里面声明新增成员,在实现部分编写新增成员的完整定义代码。新增成员的目的时扩充或修改基类的功能。
2)继承列表指定派生类从那些基类继承。派生类可以只从一个基类中继承(单继承),也可以从多个基类继承(多继承)。每个基类以“继承方式 基类名”的形式声明,多个基类之间用“,”隔开。
3)派生类将继承基类中除构造函数、析构函数之外的所有数据成员和函数成员。基类的构造函数和析构函数不能被继承,派生类需要重新编写自己的构造和析构函数。
4)继承后,派生类对其基类成员按照继承方式进行封装。继承方式有三种:public(公有继承)、protected(保护继承)和private(私有继承)。
public(公有继承):派生类对其基类成员不做任何封装,它们在派生类中的访问权限与原来在基类中的权限相同(相对于派生类以外额函数所言)。
private(私有继承):在派生类中的访问权限统统被改为private,使用是私有继承,实际上是派生类要将其基类成员全部隐藏起来。
protected(保护继承):派生类对其基类成员做半封装,基类中的public成员被降为protected保护权限,protected和private成员权限不变。
为了更好的理解继承和派生的语法细节,下面以圆形类和圆环类举例说明:
圆形类代码:
//圆形类声明 class Circle { private: double r; //半径 public: void Input(); //输入半径 double Radius(); //读取半径 double CArea(); //求圆形面积 double CLen(); //求圆形周长 Circle() //无参构造函数 { r = 0; } Circle(double x) //有参构造函数 { if(x<0) r = 0; else r = x; } Circle(Circle &x) //拷贝构造函数 { r = x.r; } }; //类实现 void Circle::Input() { cin >> r; while( r < 0 ) cin >> r; } double Circle::Radius() { return r; } double Circle::CArea() { return (3.14*r*r); } double Circle::CLen() { return (3.14*2*r); }
圆环类代码:
//派生类BorderCircle(圆环类)声明 class BorderCircle:public Circle //公有继承Circle类 { public: double w; //新增数据成员:边框宽度 //以下为新增的函数成员 double InnerArea(); //求圆环内圆面积 double BorderArea(); //求边框面积,即圆环面积 void Input(); //输入外圆半径和边框宽度 }; //类实现 double BorderCircle::InnerArea() { double x = Radius(); //读取外圆半径 return (3.14*(x-w)*(x-w)); } double BorderCircle::BorderArea() { return (CArea()-InnerArea()); } void BorderCircle::Input() { Circle::Input(); //输入外圆半径 cin >> w; //输入宽度 }
对象上面两段代码进行分析如下:
1)派生类BorderCircle中的成员
基类成员:
数据成员:半径r
函数成员:输入半径函数Input、读取半径函数Radius、求圆形面积函数Area和求周长函数CLen。
新增成员:
数据成员:边框宽度w
函数成员:求内圆面积函数InnerArea、求边框面积函数BorderArea和一个重写额Input函数。
派生类BorderCircle一共有9个成员,其中5个基类成员和4个新增成员
2)在派生类中访问基类成员
派生类的新增成员可以访问继承来的基类成员,但受到基类访问控制权限的限制,只能访问基类的公有和保护成员(只有在派生类中才可以通过派生类对象访问基类的protected成员)。如在派生类BorderCircle的InnerArea函数中,无法直接读取外圆半径,只能通过Raius读取半径。
3)派生类中新增成员对基类成员的同名覆盖
派生类新增函数成员可以与基类成员重名,但它们不是重载函数。如BorderCircle继承了基类的输入半径函数Input,然后又新增了一个函数成员Input。同名时新增成员将覆盖基类成员。派生类对象通过成员名访问到的时新增成员Input,这就是新增成员对基类成员的同名覆盖。可以通过基类名::基类成员名的语法形式访问被覆盖的基类成员。如BorderCircle类在新增成员Input函数中输入圆形半径时的使用:Circle::Input();
二、派生类对象的定义和访问
定义一个派生类BorderCircle的对象obj:BorderCircle obj; 计算机执行该对象定义语句时将为对象分配内存空间,一个派生类所占用的内存空间等于类中全部数据成员所需内存空间的总和。
1、派生类对象的访问
派生类对象中的成员,无论是基类成员还是新增成员,只要是公有成员就都可以访问,是非公有权限(保护权限、私有权限)就都不能访问。换句话说就是派生类对象只能访问公有继承下的基类公有成员。
1)访问基类成员:obj.Circle::Input()、obj.Radius()、obj.CArea()、obj.CLen()
2) 访问新增成员:obj.w、obj.InnerArea()、obj.BorderArea()、obj.Input()
可以看到无论时基类还是新增成员访问方式都一样,只有在同名覆盖的情况下,访问被覆盖的基类成员时需要在成员名前加前缀"基类名::",显式指明基类成员,否则默认都是新增成员。
2、多级派生和多种派生
派生类也可以任意多级。用基类定义派生类,派生类可以继续作为基类去定义更下级的派生类,这就是多级派生。多级派生会对所继承的基类进行再次封装,即多级封装。如:A 派生出->B派生出-> C 派生出->D
多个不同的派生类可以继承同一个基类。换句话说,同一个基类可以派生出多个不同的派生类,这就是多种派生。如:A 派生出->B、C、D
同一基类可以经过多种派生和多级派生,派生出很多不同的派生类,这些派生类和基类共同组成一个类的家族,即类族。类族中的类都继承自一个基类。
三、保护权限与保护继承
1、类的保护权限
被赋予公有权限的类成员是开放的、赋予私有权限的类成员是被隐藏的,然而被赋予保护权限的类成员是半开放的。何为半开放?
在派生类新增函数成员中访问基类成员,基类的保护成员和公有成员一样(但派生类对象不能访问基类保护成员),都可以被访问。保护成员对派生类中的新增函数成员是开放的,但对类外的所有其他函数都是隐藏的,这就是所谓的半开放。类的保护权限是向其派生类定向开放的一种权限。
2、类的保护继承
派生类通过继承方式对基类继承来的成员进行二次封装。公有继承就保持基类成员权限原样(相当于没有封装),而私有继承就是将所有基类成员统统隐藏起来(即全封装),保护继承则是一种半封装。
派生类保护继承基类,对下级派生类的新增函数成员来说,派生类中的基类成员没被封装(派生类将从上级基类中继承来的公有和保护成员继续向下级派生类开发);但对派生类外部的所有其他函数来说,这些基类成员却被封装起来了,这就是所谓的半封装。派生类的保护继承是向下级派生类定向开发基类成员的一种半封装形式(开放的是其基类的保护和公有成员)。
四、派生类对象的构造与析构
1、派生类的构造函数
构造函数通过形参传递初始值,实现对新建对象数据成员的初始化。派生类构造函数不能直接初始化类中的基类成员,因为基类的数据成员可能是私有的,不能访问赋值。要想初始化这些基类成员,必须通过基类的构造函数才能完成。调用基类构造函数,其语法形式是在派生类构造函数头后面添加初始化列表:
派生类构造函数名(形参列表):基类名1(形参1),基类名2(形参2) ...
{
... //函数体中初始化新增成
}
为派生类BorderCircle添加如下3个重载构造函数
1)有参构造函数
BorderCircle::BorderCircle(double p1.double p2):Circle(p1)
{
w = p2;
}
使用示例: BorerCircle obj(5,2);
2) 无参构造函数
BorderCircle::BorderCircle() {w = 0;}
使用示例: BorerCircle obj1;
3) 拷贝构造函数
BorderCircle::BorderCircle(BorderCircle &rObj):Circle(rObj) {w = rObj.w; }
使用示例: BorerCircle obj2(obj);
派生类对象中各数据成员的初始化顺序是:先调用基类构造函数,初始化基类成员;在执行派生类构造函数的函数体,初始化新增成员。如果派生类继承自多个基类,初始化顺序由其在派生类继承列表中的声明顺序决定,声明在前的基类成员先初始化。
2、派生类的析构函数
派生类对象的数据成员的析构顺序:先执行派生类析构函数的函数体,清理新增成员;再自动调用基类析构函数,清理基类成员。简单说,派生类对象的析构顺序与构造顺序相反,即先析构新增成员,再析构基类成员。
3、组合派生类的构造和析构
数据成员中包含对象成员的类称为组合类。如果派生类的新增成员也包含对象成员,则该派生类就是组合派生类。组合派生类的成员可分为三种:基类成员、对象成员和非对象成员。
为初始化对象,组合派生类的构造函数需依次初始化基类成员、新增对象成员和新增非对象成员。其中初始化基类成员和对象成员均通过初始化列表,初始化非对象成员通过函数体直接赋值。
这里需要注意:组合派生类构造函数的初始化列表中,通过类名调用基类构造函数,通过对象名调用对象成员构造函数。
组合派生类对象的初始化顺序:先调用基类构造函数初始化基类成员,在调用对象成员所属类的构造函数初始化新增对象成员,最后执行构造函数体初始化新增非对象成员。组合派生类的析构顺序与其构造顺序相反,即先析构新增非对象成员、再析构新增对象成员,最后才析构基类成员。
4、组合和继承的选择
假设已有的类A,新类B。如果类B包含A,则B可认为由A组合而成,应选择组合方法(例如圆柱类和圆类)。如果类B是一种A,即类B是类A的一个特例,此时应选择继承方法(如圆环类和圆类)。更多继承和组合的区别和适应场景还有待学习。