您的位置:首页 > 其它

继承与派生

2015-11-12 15:27 267 查看
通过继承机制,可以方便地利用一个已有的类建立新类,重用已有软件中的部分甚至很大的部分,例如微软基础类就是通过类的继承来体现类的可重用性和可扩充性。

1.继承和派生的概念

所谓“继承”就是在一个或多个已存在的类的基础上建立一个新的类。已存在的类称为“基类”、“父类”或“一般类”。新建立的类称为“派生类”、“子类”或“特殊类”。

一个新类从已有的类那里获得其已有特性,这种现象称为类的继承。通过继承,一个新建子类从已有的父类那里获得父类的特性。
从另一角度说,从已有的父类产生一个新的子类,称为类的派生。类的继承是用已有的类来建立专用类的编程技术。

派生类继承了基类的所有数据成员和成员函数(不包括基类的构造函数和析构函数),并可以增加自己的新成员,同时也可以调整来自基类的数据成员和成员函数。

基类和派生类是相对而言的。一个基类可以派生出多个派生类,每一个派生类又可以作为基类再派生出新的派生类。一代一代地派生下去,就形成了类的继承层次结构,

一个派生类只从一个基类派生,这称为单继承(singleinheritance),这种继承关系所形成的层次是一个树形结构

一个派生类不仅可以从一个基类派生,也可以从多个基类派生。一个派生类有两个或多个基类的称为多重继承(multiple inheritance),如下图所示

派生类是基类的具体化,而基类是派生类的抽象。





派生类中的成员包括从基类继承过来的成员和自己新增加的成员两大部分,从基类继承过来的成员体现了派生类从基类继承而获得的共性,而新增加的成员体现了派生类的个性,体现了派生类与基类的不同,体现了不同派生类的区别。



#include<iostream>
using namespace std;
class Circle
{
public:
void SetRadious(const int &r){radious = r;}
int GetRadious(){return radious;}
void ShowRadious()
{
cout<<"Base class Circle:radious="<<radious<<endl;
}
private:
int radious;
};

class Cylinder: public Circle
{
public:
void SetHeight(const int &h){height = h;}
int GetHeight(){return heigh;}
void ShowHeight()
{
cout<<"Derived class Cylinder:height="<<height<<endl;
}
private:
int height;
};


实际上,并不是把基类的成员和派生类自己新增加的成员简单地加在一起就成为派生类。构造一个派生类一般经历3个步骤:从基类接收成员、调整从基类接收的成员、增加新成员

派生类中基类成员的访问属性不仅与在声明基类时所声明的访问属性有关,而且与在声明派生类时所指定的对基类的继承方式有关,这两个因素共同决定基类成员在派生类中的访问属性。

派生类对基类的继承方式有public,private和protected 3种。不同的继承方式决定了基类成员在派生类中的访问属性。

1)公用继承(publicinheritance)
基类的公用成员和保护成员在派生类中保持原有访问属性,其私有成员仍为基类私有。

2)私有继承(private inheritance)
基类的公用成员和保护成员在派生类中成了私有成员。其私有成员仍为基类私有。
3)受保护的继承(protectedinheritance)

基类的公用成员和保护成员在派生类中成了保护成员,其私有成员仍为基类私有。

基类中的成员

在公用派生类中的访问属性

在私有派生类中的访问属性

在保护派生类中的访问属性

私有成员

不可访问

不可访问

不可访问

公用成员

公用

私有

保护

保护成员

保护

私有

保护

在派生类中,成员有4种不同的访问属性:

①公用的:派生类内和派生类外都可以访问。
②受保护的:派生类内可以访问,派生类外不能访问,其下一层的派生类可以访问。
③私有的:派生类内可以访问,派生类外不能访问。
④不可访问的:派生类内和派生类外都不能访问

成员的同名问题(隐藏,重载,覆盖之隐藏)

可以在派生类中声明一个与基类成员同名的成员,则派生类的新成员会屏蔽与其同名的基类成员,使同名的基类成员成为“不可见”的,即基类成员的名字被隐藏。

(1)如果是在派生类中声明了一个与基类成员函数名字相同,参数也相同的成员函数,则基类中的成员函数将被隐藏。
(2)如果是在派生类中声明了一个与基类成员函数名字相同,但参数不同的成员函数,则基类中的成员函数也将被隐藏。

隐藏可以通过作用域解析运算符”::“让其可见。隐藏不代表消失,成员依然存在,可以使其现身,现身的法宝就是作用域解析运算符

#include<iostream>
using namespace std;
class Circle
{
public:
void Set(const int &r){radius = r;}
void Show()
{
cout<<"Base class Circle:radius="<<radius<<endl;
}
private:
int radius;
};
class Cylinder:public Circle
{
public:
void Set(const int &r,const int &h)
{Circle::Set(r);height = h;}
void Show()
{
Circle::Show();
cout<<"Derived class Cylinder:height="<<height<<endl;
}
private:
int height;
};
int main()
{
Cylinder obj;
obj.Set(10,20);
obj.Show();
//	obj.Set(10); 被隐藏了,导致参数错误。
obj.Circle::Set(30);
obj.Circle::Show();
return 0;
}
派生类重定义基类数据成员的例子:

#include<iostream>
using namespace std;
class Base
{
public:
float y;
void set(const int &n){x=n;}
void show(){cout<<"Base class:x="<<x<<endl;}
private:
int x;
};

class Derived:public Base
{
public:
void set(int i,int j,int k){Base::set(i);y=j;z=k;}
void show(){
Base::show();
cout<<"Base class:y="<<Base::y<<endl;
cout<<"Derived class:y="<<y<<endl;
cout<<"Derived class:z="<<z<<endl;
};
private:
int y,z;
};
int main()
{
Derived obj;
obj.set(1,2,3);
obj.Base::y=12.3;
obj.show();
return 0;
}

2.派生类的构造函数和析构函数

基类的构造函数和析构函数派生类是不能继承的。在声明派生类时,一般还应当自己定义派生类的构造函数和析构函数。
不仅要考虑派生类新增数据成员的初始化,还应当考虑对其从基类继承过来的数据成员的初始化。采取的方法是在执行派生类的构造函数时,调用基类的构造函数



派生类派生类Student的构造函数有6个形参,前3个形参作为调用基类构造函数的实参,后3个形参为对派生类新增数据成员初始化所需要的参数。

class Student: public Person
{public:
Student(char *Name, char Sex, int Age, char *Id,
char *Date, float Score): Person(Name, Sex, Age)
{  /*在构造函数的函数体中只对派生类
新增的数据成员初始化*/
strcpy(id, Id); strcpy(date, Date); score = Score;
cout << " The constructor of derived class
Student is called." << endl;
}
~Student( )       //派生类析构函数
{ cout << " The destructor of derived class
Student is called." << endl; }


定义简单派生类构造函数的一般形式为:
<派生类构造函数名>(<总参数列表>): <基类构造函数名>(<参数表>)
{
<派生类新增数据成员初始化>
};
在建立一个对象时,执行构造函数的顺序是:
(1)最先调用基类的构造函数,对基类数据成员初始化。
(2)再执行派生类构造函数的函数体,对派生类新增数据成员初始化。
在声明派生类时,可以根据需要定义自己的析构函数,用来对派生类中新增加的成员进行清理工作。在执行派生类的析构函数时,系统会自动调用基类的析构函数,对基类进行清理。派生类析构函数的执行顺序与构造函数正好相反:
(1)最先执行派生类的析构函数的函数体,对派生类新增加的成员进行清理。
(2)再调用基类的析构函数,对基类进行清理。

3.多重继承(派生类由多个基类派生)

多重继承派生类的声明格式如下:
class<派生类名>: <继承方式><基类名1>,…,<继承方式><基类名n>
{
<派生类新增加成员>
};
其中不同的基类可以选择不同的继承方式。

在多重继承方式下,定义派生类构造函数的一般形式如下:

<派生类名>(<总参数列表>): <基类名1>(<参数表1>),…,<基类名n>(<参数表n>)

{

<派生类新增数据成员的初始化>

};

其中,<总参数表>必须包含完成所有基类数据成员初始化所需的参数。

先调用所有基类的构造函数,再执行派生类构造函数的函数体。所有基类构造函数的调用顺序将按照它们在继承方式中的声明次序调用,而不是按派生类构造函数参数初始化列表中的次序调用。

派生类析构函数的执行顺序与其构造函数执行顺序正好相反,首先执行派生类析构函数的函数体,对派生类新增的数据成员进行清理;然后调用基类的析构函数,对从基类继承来的成员进行清理。所有基类的析构函数将按照它们在继承方式中的声明次序的逆序、从右到左调用。
4.虚基类
从上节的介绍可知,如果一个派生类有多个直接基类,而这些直接基类又有一个共同的基类,则在最终的派生类中会保留该间接共同基类成员的多份同名成员。这种情况有时是必要的,但是由于保留间接共同基类的多份成员,不仅占用较多的存储空间,还增加了访问这些成员时的困难,容易出错。为了解决这个问题,C++提供了虚基类(virtual
base class)的方法,使得在继承间接共同基类时只保留其一份成员

class Base

{…};

class Base1: virtual publicBase

{…};

class Base2: virtual publicBase

{…};

虚基类并不是在声明基类时声明的,而是在声明派生类时,指定继承方式时声明的。因为一个基类可以在派生一个派生类时作为虚基类,而在派生另一个派生类时不作为虚基类。
声明虚基类的一般形式为:
class
派生类名: virtual
继承方式 基类名



为了保证虚基类在派生类中只继承一次,应当在该基类的所有直接派生类中都把基类声明为虚基类。否则仍然会出现对基类的多次继承。

如果在虚基类中定义了带参数的构造函数,而且没有定义默认构造函数,则在其所有派生类(包括直接派生或间接派生的派生类)中,都要通过构造函数的初始化表对虚基类进行初始化。

#include<iostream>
using namespace std;
class Base                                      //声明基类Base
{public:                                           //公用部分
Base(int m,int n):x(m),y(n){}     //基类构造函数
protected:                                      //保护部分
int x, y;
};


class Base1: virtual public Base
{public:
Base1(int m,int n,int k): Base(m,n) {  z1=k;  }
protected:
int z1;
};
class Base2: virtual public Base
{public:
Base2(int m,int n,int p):Base(m,n) {  z2=p;  }
protected:
int z2;
};
class Derived:public Base1, public Base2
{public:
Derived(int m,int n,int o,int p,int q):
Base(m,n),Base1(m,n,o),Base2(m,n,p) {  z=q;  }
void show ()
{     cout<<"x="<<x<<endl;
cout<<"y="<<y<<endl;
cout<<"z1="<<z1<<endl;
cout<<"z2="<<z2<<endl;
cout<<"z="<<z<<endl;
}
protected:
int z;
};
int main()
{
Derived obj(12,13,14,15,16);
obj.show();
return 0;
}


派生类构造函数调用的次序有3个原则:

(1)同一层中对虚基类构造函数的调用优先于对非虚基类构造函数的调用。

(2)若同一层次中包含多个虚基类,则这些虚基类的构造函数按照它们在继承方式中的声明次序调用。

(3)若虚基类由非虚基类派生出来,则仍然先调用基类构造函数,再按派生类中构造函数的执行顺序调用。

派生类的析构函数调用的次序与构造函数的正好相反。如果存在虚基类时,在析构函数的调用过程中,同一层对普通基类析构函数的调用总是优先

基类对象与公用派生类对象之间的赋值兼容关系具体表现在以下3个方面:

(1)公用派生类对象可以向基类对象赋值

(2)公用派生类对象可以代替基类对象向基类对象的引用进行赋值或初始化

(3)如果函数的参数是基类对象或基类对象的引用,相应的实参可以使用公用派生类对象

(4)公用派生类对象的地址可以赋给指向基类对象的指针变量,指向基类对象的指针也可以指向公用派生类对象。但是通过指向基类对象的指针只能访问公用派生类对象中的基类成员,而不能访问公用派生类对象新增加的成员。



5.聚合与组合

前面学习的继承描述的是类与类之间的一般与特殊的关系,是“is-a”关系。如果A是B的一种,则允许A继承B的功能和属性。如研究生是学生的一种,那么研究生类可从学生类派生;汽车是交通工具的一种,小汽车是汽车的一种,那么汽车类可从交通工具类派生,小汽车类可以从汽车类派生。

而这里所说的聚合与组合描述的是类与类之间的整体与部分的关系。聚合关系中成员对象可以脱离整体对象独立存在,而组合关系中的部分和整体具有统一的生命周期。一旦整体对象不存在,部分对象也将不存在。组合关系也被称为是一种强聚合关系。

聚合关系是“has-a”关系,组合关系是“contains-a”关系。例如,计算机和CPU的关系、公司和员工的关系是一种聚合关系,而人和大脑的关系、窗口和其中的按钮的关系是一种组合关系。表现在代码层,这两种关系是一致的,为部分类对象(也称为子对象)以类属性的形式出现在整体类的定义中。区分它们只能从语义级别来区分。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: