类的继承性
1. 基类和派生类
1) 继承的几个特性
若类间具有继承关系,它们应该有下列几个特性:
(1)类间具有共享特征。
(2)类间具有细微的差别。
(3)类间具有层次结构。
2) 派生类的概念
引入继承的目的,是为了能使代码重用。如果可以利用已编制的程序段,将可以提高编程的效率及程序的可读性。下面通过类CPoint和类CCircle之间的关系来说明C++中类的继承性,通过继承实现了代码的重用。
class CPoint
{
private:
int color;
protected:
int x,y;
public:
void show();
void SetPosition();
};
class CCircle
{
private:
int radius;
protected:
int x,y;
int width;
public:
void show();
void SetPosition();
double Circumfrence();
};
类CPoint和类CCircle中的数据成员有许多是相同的,成员函数或者是完全相同的,或者是功能相同的。例如成员变量x、y在类CPoint中表示点的位置,在类CCircle中也如此。又如成员函数SetPosition()的功能是设置成员变量x和y的值,因而在两个类中代码是完全相同的,而成员函数show()在类CPoint中显示一个点,而在类CCirlce中是显示一个圆,因而show()在两个类中的功能是不一样的,其代码显然也是不一样的。由以上分析可以看到类CCircle可以由类CPoint作适当修改和扩充得到,也就是说类CCircle可以从类CPoint继承而来,因而类CCircle可以写成如下形式,图8.2表示了此两个类的继承关系。
图8.2 类的继承性 |
class CCirlce:public CPoint
{
private:
int radius;
protected:
int width;
public:
double Circumfrence();
void show();
};
上面的写法中在类名CCirlce后增加了“:public CPoint”,说明类CCircle是从类CPoint继承而来的。这样,凡是在类CPoint中已定义的成员都可以被类CCirlce继承,因而在类CCirlce中只定义了扩充的或将修改的成员数据或函数。CCircle的成员除了新定义的之外,还包含了从类CPoint中继承来的数据成员和成员函数。通常称类CPoint是类CCirlce的基类,或父类、上级类;称类CCircle是类CPoint的派生类或子类、下级类、导出类。
3) 派生类的定义
派生类的定义格式为:
class派生类名:继承方式基类名
{
定义派生类成员;
};
如果是多重继承,派生类的定义格式在后面叙述。
上述定义中继承方式可以是三个关键字之一:public、private、protected,缺省值为private。派生类有以下几个特点:
(1)在基类基础上提供新的成员。
(2)隐藏基类的成员。
(3)重新定义基类中的成员函数。
2. 单继承
1) 派生类对基类的继承关系
在派生类的定义中都必须指明对基类的继承方式,这将决定派生类对基类的访问性质。下面叙述派生类在三种继承方式下是如何继承基类成员的。
(1) 公有继承方式(public)
基类中的私有成员不可访问(invisible),所谓不可访问就是派生类无法直接使用基类中的私有成员,基类中其它成员访问权限不变。
(2) 保护继承方式(protected)
基类中的私有成员不可访问,其它成员的访问权限都变成保护的(protected)。
(3) 私有继承方式(private)
基类中的私有成员不可访问,其它成员的访问权限都变成私有的(private)。
派生类对基类的访问权限归结起来如表8.1所示。
表8.1:不同继承方式下派生类对基类的访问权限
继承方式 | 基类访问权限 | 派生类访问权限 |
public (公有继承) | public protected private | public protected 不可访问 |
protected (保护继承) | public protected private | protected protected 不可访问 |
private (私有继承) | public protected private | private private 不可访问 |
例8.1 公有继承示例
//ex8_1.cpp
#include "iostream.h"
class CBass
{
int x;
public:
void SetX(int i){x=i;}
int GetX(){return x;}
};
class CDerivate:public CBass
{
int y;
public:
void SetY(int i){y=i;}
int GetY(){return y;}
int GetXY(){return y*GetX();}
};
void main()
{
CBass a;
CDerivate b;
a.SetX(5);
b.SetX(6);
b.SetY(7);
cout<<a.GetX()<<endl;
cout<<b.GetX()<<","<<b.GetY()<<","<<b.GetXY()<<endl;
}
程序运行结果为:
5
6,7,42
程序中类CDerivate是类CBass的派生类,是从类CBass公有继承而来,因而在类CDerivate中除新定义的成员外,还继承了基类CBass中的所有成员。图8.5说明了派生类CDerivate是如何继承基类CBass中的成员的,以及继承后类CDerivate中各成员的访问权限。
图8.5 基类与派生类 |
由图8.5可知,首先派生类全盘继承了基类中的成员,它们在类中构成了一个独立的块,它们的全体称为派生类的基类子块,简称为基类子块。显然,基类子块内的各成员保持了原先的关系,同时它们还要参与派生类的相关操作。基类子块在派生类中的访问权限可由继承方式及基类子块原先的访问权限共同确定。根据表8.1可以重新确定基类子块成员在派生类中的访问权限。在本例中,由于是公有继承,基类子块中除私有成员外,其它成员的访问权限不变,因而由继承而得到的成员函数SetX()、GetX()仍是公有的,而私有数据成员x是不可访问的。注意:不可访问的意思是指成员变量x虽然存在于派生类CDerivate中,但在派生类中是不可见的,若要显示或更改成员变量x的值,必须通过基类子块中的接口函数才行。本例程序中定义了基类CBass的对象a和派生类CDerivate的对象b,语句:“b.SetX(6);”说明将对对象b的数据成员x赋值,在对象b中没有直接定义成员函数SetX(),但它已从基类中继承而得到成员函数SetX(),该函数是公有的,因而可在主函数中直接调用,不会发生语法错误。进一步分析可以看到,成员函数b.SetX()将对不可访问的成员变量x赋值,对于基类子块而言,SetX()就是一个公有的接口函数,因而它可以访问私有成员x。同理b.GetX()也是个公有接口函数,因而它也可以访问私有成员x。
2) protected的含义
例8.3 指出程序中的语法错误
//ex8_3
#include "iostream.h"
class CBass
{
int x;
protected:
int y;
public:
int z;
void SetX(int i){x=i;}
void SetY(int i){y=i;}
void SetZ(int i){z=i;}
};
class CDerivate:public CBass
{
public:
void PrintX(){cout<<x<<endl;}//错误:x是不可访问成员
void PrintY(){cout<<y<<endl;}
void PrintZ(){cout<<z<<endl;}
};
void main()
{
CBass A;
CDerivate B;
A.SetX(1);
A.SetY(2);
A.SetZ(3);
cout<<A.x<<endl;//错误:不能访问私有成员x
cout<<A.y<<endl;//错误:不能访问保护成员y
cout<<A.z<<endl;
B.SetX(4);
B.SetY(5);
B.SetZ(6);
B.PrintX();//错误:成员函数Print()出错
B.PrintY();
B.PrintZ();
cout<<B.x<<endl;// 错误: x是不可访问成员
cout<<B.y<<endl;// 错误: 不能访问保护成员y
cout<<B.z<<endl;
图8.7 例8.3中的派生类 |
}
图8.7中标明了例题中基类子块成员在派生类的访问权限。 程序中主要存在以下两种语法错误:
(1) 在类外直接使用了类的私有成员或不可访问成员,例如派生类的成员函数
void PrintX(){Cout<<x<<endl;}使用了不可访问的成员x。主函数中的语句"Cout<<A.x<<endl;"和"Cout<<B.x<<endl;"在类外使用私有成员或不可访问成员x。
(2) 在类外直接使用了类的保护成员,例如程序中语句:"Cout<<A.y<<endl;"和"Cout<<B.y<<endl;"在类外使用了保护数据成员y。
删除出错语句,运行程序所得结果为:
3
5
6
6
本例在基类CBass中定义了protected(保护)成员y,y可以在派生类CDerivate中被使用,但在类外不可使用。在派生类成员函数 “PrintY(){Cout<<y<<endl;}” 中可以直接使用基类子块中的保护成员y,但不可以在类外使用保护成员y,因而主函数中语句“Cout<<B.y<<endl;”将导致语法错误。当希望基类中的成员可以被派生类的成员使用而不可以在类外被使用,这时可以令成员的访问性质为保护的(protected)。
通常讲,保护成员对类内成员相当于公有成员,对类外或对象相当于私有成员。在无继承的情况下,保护成员与私有成员的性质完全一样,在存在继承的情况下,保护成员就具有了二重性,有时呈现私有成员的性质,有时呈现公有成员的性质。当讨论的重心是:派生类对象对由继承而得到的成员的访问特性(亦称水平继承)问题时,保护成员呈现为私有成员性质。当讨论的重心是:派生类成员对由继承而得到的成员的访问特性(亦称垂直水平继承)问题时,保护成员呈现为公有成员性质。上节中提出的基类子块概念能方便地处理保护成员的二重性问题。在继承情况下,通过重新确定基类子块各成员的访问性质,从而化解了因继承方式引起的问题,基类子块内各成员除因同名被屏蔽以外,其它各成员与对象的关系与无继承时的情况完全一样。唯一要注意的是这时增加了一种新的访问性质:不可访问性。
3) 派生类的构造函数和析构函数
每个类中都有显式或隐式的构造函数和析构函数,创建派生类时,派生类将全盘继承基类的数据成员和成员函数,但不继承构造函数和析构函数。因而用户要么在派生类中重新定义构造函数和析构函数,要么使用系统提供的缺省构造函数和析构函数。由于派生类继承了基类的所有成员,在进行初始化时,派生类除了对自身数据成员进行初始化外,还必须对基类子块数据成员进行初始化,因而就涉及到如何在派生类中调用基类的构造函数问题。对于析构函数也有同样的问题。本节主要讨论由于继承引起的这些问题。
派生类的构造函数的一般格式如下:
派生类构造函数名(参数总表):基类构造函数(参数表),对象成员名(参数表)
{ 派生类构造函数体 ; }
由上述格式可以看到,在派生类构造函数的初始化表中将显式调用基类构造函数,对基类子块数据成员进行初始化,如果派生类中存在对象成员,其初始化也在初始化表中进行。
析构函数是不带参数的,不存在初始化表,因而其格式与以前叙述的析构函数完全一样。在执行派生类的析构函数后,系统将自动调用基类析构函数。
派生类中构造函数的调用次序如下:基类的构造函数,成员对象的构造函数,派生类构造函数。析构函数的调用次序与构造函数的调用次序相反,依次为:派生类的析构函数,对象成员的析构函数,基类的析构函数。
例8.4 写出程序运行结果
//ex8_4.cpp
#include "iostream.h"
class CBass
{
int x;
public:
CBass()
{
x=0;
cout<<"Default bass constructor called.\n";
}
CBass(int i)
{
x=i;
cout<<"Bass constructor called.\n";
}
~CBass(){cout<<"Bass destructor called.\n";}
void Print() const {cout<<x<<endl;}
};
class CDerivate:public CBass
{
int y;
public:
CDerivate()
{
y=0;
cout<<"Default derivate constructor called.\n";
}
CDerivate(int i,int j):CBass(j)
{
y=i;
cout<<"Derivate constructor called.\n";
}
~CDerivate(){cout<<"Derivate destructor called.\n";}
void Print()
{
CBass::Print();
cout<<y<<endl;
}
};
void main()
{
CDerivate A,B(5,10);
A.Print();
B.Print();
}
程序运行结果为:
Default bass constructor called.
Default derivate constructor called.
Bass constructor called.
Derivate constructor called.
0
0
10
5
Derivate destructor called.
Bass destructor called.
Derivate destructor called.
Bass destructor called.
程序中派生类CDerivate的构造函数为:
CDerivate(int x,int y):CBass(j)
{
y=i;
Cout<<"Derivate constructor called";
}
构造函数的初始化表中显式调用了基类的构造函数CBass(j),对基类子块中的数据成员赋值,基类构造函数中有一个参数,派生类中也有一个数据成员,因而派生类构造函数有两个参数,它们将分别对基类子块和派生类的数据成员进行初始化。
程序的输出结果说明了构造函数和析构函数的调用次序。程序中定义了派生类对象A,由于没有初始值,因而将调用无参构造函数,在执行派生类的构造函数前,将先调用基类构造函数,再执行派生类构造函数,从而形成程序结果中的头两句:
Default bass constructor called.
Default bass constructor called.
这些运行结果清楚地表明了构造函数和析构函数的调用次序。类似地可以分析对象B建立和撤消时,是如何调用构造函数和析构函数的。
3. 多重继承