声明与继承关系
继承派生概念
- 类的继承就是新类由已经存在的类获得已有特性。
- 类的派生则是由已经存在的类产生新类的过程。
由已有类产生新类时,新类会拥有已有类的所有特性,然后又加入了自己独有的新特性。**已有类叫做基类或者父类,产生的新类叫做派生类或者子类。**派生类同样又可以作为基类派生新的子类,这样就形成了类的层次结构。
类是对现实中事物的抽象,类的继承和派生的层次结构则是对自然界中事物分类、分析的过程在程序设计中的体现。
某个公司雇员的派生关系。位于最高层的雇员其抽象程度最高,是最具一般性的概念。最下层抽象程度最低,最具体。从上层到下层是具体化的过程,从下层到上层是抽象话的过程。面向对象设计中上层与下层是基类与派生类的关系。
此公司的雇员有三类:兼职技术人员、管理人员和销售人员。每个雇员都有姓名、级别和薪水等信息。每种雇员都可以升级,但升级方式不同。他们的月薪计算方式也不同,兼职技术人员应按实际工作小时数领取月薪,管理人员领取固定月薪,而销售人员是根据当月销售额领取提成。
这三类雇员的升级方式和月薪的计算方法等不同,所以不能用同一个类来描述,需要有三个类来分别抽象三类雇员。但这三个类中又有很多数据成员是一样的,例如姓名、级别和薪水等,函数成员也有很多相同的,只是可能实现方法不同,例如升级函数和计算月薪函数等。
我们应该先描述所有雇员的共性,再分别描述每类雇员。分别描述时应先说明他是雇员,然后描述他特有的属性和处理方法。这种描述方法在面向对象设计中就是类的继承与派生。对雇员共性进行描述就形成了基类,而对每类雇员的特性的描述可以通过从基类派生出子类来实现。
派生类声明
派生类声明的语法形式为:
class 派生类名 : 继承方式1 基类名1, 继承方式2 基类名2, ... 继承方式n 基类名n
{
派生类成员的声明;
}
class Child : public Parent1, private Parent2
{
public:
Child();
~Child();
}
- “基类名”(Parent1和Parent2)是已有类的名称,“派生类名”(Child)是从已有类产生的新类的名称。
- 多继承:一个派生类可以有多个基类。这种情况下派生类就同时具有多个基类的特性。同样,一个基类也可以产生多个派生类,这比多继承更常见。
- 单继承:一个派生类如果只有一个基类。
- 类族: 基类产生派生类,派生类又可以作为基类再派生它自己的派生类,任何基类又可以产生多个派生类,这样就形成了一个类族。
- 直接基类:直接派生出某个类的基类叫做这个类的直接基类。
- 间接基类:基类的基类或者更高层的基类叫做派生类的间接基类。
类A派生出类B,类B派生出类C,则类A是类B的直接基类,类B是类C的直接基类,而类A是类C的间接基类。
- 继承方式:继承方式限定了派生类访问从基类继承来的成员的方式,指出了派生类成员或类外的对象对从基类继承来的成员的访问权限。每个“继承方式”只限定紧随其后的基类。如果没有显式指定继承方式,则默认为私有继承。
- 公有继承public
- 保护继承protect
- 私有继承private
- 基类的扩展:派生类声明语法形式中的派生类成员指除了原封不动从基类中继承来的成员以外,修改的基类成员或者新增加的成员。派生类成员是对基类的扩展。
从基类继承产生派生类实现了对代码的复用,派生类中修改的基类成员或新增加的成员实现了对代码的扩展。这样继承与派生使得我们减少了重复性劳动,提高了软件开发效率,维护和扩展程序更容易。
派生类从基类继承的过程
派生类从基类继承的过程可以分为三个步骤:
- 吸收基类成员
- 修改基类成员
- 添加新成员。
吸收基类成员就是代码复用的过程,修改基类成员和添加新成员实现的是对原有代码的扩展,而代码的复用和扩展是继承与派生的主要目的。
class employee // 雇员类
{
public:
employee(); // 构造函数
~employee(); // 析构函数
void promote(int); // 升级函数
void getSalary(); // 计算工资
protected:
char *m_szName; // 雇员姓名
int m_nGrade; // 级别
float m_fSalary; // 工资
};
class salesman : public employee
{
public:
salesman();
~salesman();
void getSalary(); // 计算工资
private:
float m_fProportion; // 提成比例
float m_fSalesSum; // 当月总销售额
};
吸收基类成员
派生类从基类继承时首先就是吸收基类成员,将基类成员中除了构造函数和析构函数外的所有其他成员全部接收。这里要注意,基类的构造函数和析构函数都不能被派生类继承。
派生类不能从基类继承构造函数和析构函数。但是派生类同样需要有初始化和清理,所以我们需要为派生类添加新的构造函数和析构函数,就像上例中,派生类salesman中就添加了新的构造函数salesman()和新的析构函数~salesman()。
上例中,employee类除构造函数和析构函数外的所有成员:promote(int)函数、getSalary()函数,以及数据成员m_szName、m_nGrade和m_fSalary,都被派生类salesman继承过来。
修改基类成员
派生类修改基类成员的方式有两种:
- 通过设置派生类声明中的继承方式,来改变从基类继承的成员的访问属性。
- 通过在派生类中声明和基类中数据或函数同名的成员,覆盖基类的相应数据或函数。
一旦我们在派生类中声明了一个和基类某个成员同名的成员,那么派生类这个成员就会覆盖外层的同名成员。这叫做同名覆盖。 需要注意的是,要实现函数覆盖不只要函数同名,函数形参表也要相同,如果函数形参表不同只有名字相同则属于前面所说的重载。
上例中,salesman类的getSalary函数覆盖了employee类的同名函数。比如,我们定义了一个salesman类的对象A,则A.getSalary()调用的是salesman类中的getSalary函数而不是基类employee中的。
添加新成员
代码的扩展是继承与派生的主要目的之一,而添加新成员是实现派生类在基类基础上扩展的关键。
上例中,派生类salesman就添加了两个新数据成员m_fProportion和m_fSalesSum。可见,能够添加新成员还是很方便的,我们在软件开发中可以根据实际需要为派生类添加新的数据成员或函数成员。
继承关系
- 对象仅能访问类的公有成员,不能访问保护成员和私有成员。而类的内部成员使用类内部成员时没有公私之分。通过对象访问类的成员属于外部访问,只能访问类的公有成员。
- 派生类的继承方式为public,即公有继承时,对基类中的公有成员和保护成员的访问属性都不变,而对基类的私有成员则不能访问。
- 在保护继承方式中,基类的公有成员和保护成员被派生类继承后变成派生类的保护成员,而基类的私有成员在派生类中不能访问。
- 在私有继承方式中,基类的公有成员和保护成员被派生类继承后变成派生类的私有成员,而基类的私有成员在派生类中不能访问。
派生类对基类成员的访问主要有两种:
- 派生类的新增成员对继承的基类成员的访问;
- 派生类的对象对继承的基类成员的访问。通过对象访问类的成员属于外部访问,只能访问类的公有成员。
公有继承
基类的公有成员和保护成员被继承到派生类中以后同样成为派生类的公有成员和保护成员,派生类中新增成员对他们可以直接访问,派生类的对象只能访问继承的基类公有成员。但是派生类的新增成员和派生类的对象都不能访问基类的私有成员。
#include <iostream>
using namespace std;
class Base // 基类Base的声明
{
public: // 公有成员函数
void SetTwo(int a, int b) { x=a; y=b; }
int GetX() { return x; }
int GetY() { return y; }
private: // 私有数据成员
int x;
int y;
};
class Child : public Base // 派生类的声明,继承方式为公有继承
{
public: // 新增公有成员函数
void SetThree(int a, int b, int c) { SetTwo(a, b); z=c; }
int GetZ() { return z; }
private: // 新增私有数据成员
int z;
};
int main()
{
Child child; // 声明Child类的对象
child.SetThree(1, 2, 3); // 设置派生类的数据
cout << "The data of child:"<<endl;
cout << child.GetX() << "," << child.GetY() << "," << child.GetZ() << endl;
return 0;
}
- 上面的程序声明了一个基类Base,又声明了Base类的派生类Child,最后是主函数部分。派生类Child从基类Base中继承了除构造函数和析构函数外的所有数据成员和函数成员,这些再加上派生类Child的新增成员就组成了Child类的全部。类Child的继承方式为公有继承,基类Base的所有公有成员在派生类Child中的访问属性不变,都可以直接访问,所以Child类的SetThree函数可以直接调用Base类的SetTwo函数。基类公有成员SetTwo、GetX和GetY都变成了Child类外部接口的一部分。但是上面说过,派生类不能访问基类的私有成员,所以Child类不能访问Base类的x和y。
- 主函数中首先定义了派生类Child的对象child,然后通过对象child调用了派生类Child的新增公有函数SetThree和GetZ,还调用了从基类Base继承的公有成员函数GetX和GetY。
保护继承
基类的公有成员和保护成员在派生类中都成了保护成员,所以派生类的新增成员可以直接访问基类的公有成员和保护成员,而派生类的对象不能访问它们.
类的对象也是处于类外的,不能访问类的保护成员。对基类的私有成员,派生类的新增成员函数和派生类对象都不能访问。
假设A类是基类,B类是从A类继承的派生类,A类中有保护成员,则对派生类B来说,A类中的保护成员和公有成员的访问权限是一样的。而对A类的对象的使用者来说,A类中的保护成员和私有成员都一样不能访问。可见类中的保护成员可以被派生类访问,但是不能被类的外部对象(包括该类的对象、一般函数、其他类等)访问。我们可以利用保护成员的这个特性,在软件开发中充分考虑数据隐藏和共享的结合,很好的实现代码的复用性和扩展性。
class Base
{
protected:
int x; // 基类的保护成员
};
int main()
{
Base base;
base.x = 0; // 编译报错
return 0;
}
- 这段代码在编译的时候会报错,错误就出在通过对象base访问保护成员x时,就像上面讲的,对Base类的对象base的使用者来说,Base类中的保护成员x和私有成员的访问特性是一样的,所以对象base不能访问x,这样跟使用私有成员一样通过保护成员实现了数据的隐藏。
class Base
{
protected:
int x; // 基类的保护成员
};
class Child : public Base
{
public:
void InitX();
};
void Child::InitX()
{
x = 0;
}
- 对上面的派生类Child来说,基类Base中的保护成员x和公有成员的访问权限一样,所以Child类的成员函数InitX可以访问Base类的保护成员x。
私有继承
在私有继承方式中,基类的公有成员和保护成员被派生类继承后变成派生类的私有成员,而基类的私有成员在派生类中不能访问。派生类的新增成员可以直接访问基类的公有成员和保护成员,但是在类的外部通过派生类的对象不能访问它们。而派生类的成员和派生类的对象都不能访问基类的私有成员。
***不管是保护继承还是私有继承,在派生类中成员的访问特性都是一样的,都是基类的公有和保护成员可以访问,私有成员不能访问。***但是派生类作为基类继续派生新类时,两种继承方式就有差别了。例如,A类派生出B类,B类又派生出C类,如果B类是以保护继承方式从A类继承的,则A类的公有成员和保护成员都成为B类的保护成员,再由B类派生出C类时,原来A类的公有成员和保护成员间接继承到C类中,成为C类的保护成员或者私有成员(C类从B类公有继承或保护继承时为前者,私有继承时为后者),所以C类的成员可以间接访问A类的公有成员和保护成员。但是如果B类是以私有继承方式从A类继承的,则A类的公有成员和保护成员都成为B类的私有成员,A类的私有成员不能在B类中访问,B类再派生出C类时,原来A类的所有成员都不能在C类中访问。
由以上分析得出,私有继承使得基类的成员在其派生类后续的派生中不能再被访问,中止了基类成员继续向下派生,这对代码的复用性没有好处,所以一般很少使用私有继承方式。
#include <iostream>
using namespace std;
class Base // 基类Base的声明
{
public: // 公有成员函数
void SetTwo(int a, int b) { x=a; y=b; }
int GetX() { return x; }
int GetY() { return y; }
private: // 私有数据成员
int x;
int y;
};
class Child : private Base // 派生类的声明,继承方式为公有继承
{
public: // 新增公有成员函数
void SetThree(int a, int b, int c) { SetTwo(a, b); z=c; }
int GetX() { return Base::GetX(); }
int GetY() { return Base::Gety(); }
int GetZ() { return z; }
private: // 新增私有数据成员
int z;
};
int main()
{
Child child; // 声明Child类的对象
child.SetThree(1, 2, 3); // 设置派生类的数据
cout << "The data of child:"<<endl;
cout << child.GetX() << "," << child.GetY() << "," << child.GetZ() << endl;
return 0;
}
- hild类从Base类中私有继承,Base类中的公有成员SetTwo()、GetX()和GetY()成为Child类的私有成员,在Child类中可以直接访问它们,例如Child类的成员函数SetThree()中直接调用了Base类的公有成员函数SetTwo()。Base类的私有成员x和y在Child类中不能访问。在外部通过Child类的对象不能访问Base类的任何成员,因为Base类的公有成员成为Child类的私有成员,Base类的私有成员在Child类中不能访问。那么Base类的作为外部接口的公有成员SetTwo()、GetX()和GetY()都被派生类Child隐藏起来,外部不能通过Child类的对象直接调用。
- 这里调用的函数GetX()和GetY()都是派生类Child的函数,由于是私有继承,基类Base中的同名函数都不能通过Child类的对象访问。
如果我们希望派生类也提供跟基类中一样的外部接口怎么办呢?
我们可以在派生类中重新定义重名的成员。上面的Child类就重新定义了公有成员函数GetX()和GetY(),函数体则只有一个调用基类函数的语句,照搬了基类函数的功能。因为派生类中重新定义的成员函数的作用域位于基类中同名函数的作用域范围的内部,根据前面可见性中讲的同名覆盖原则,调用时会调用派生类的函数。通过这种方式可以对继承的函数进行修改和扩展,在软件开发中经常会用到这种方法。