2.2 继承与派生
1.?继承与派生的一般形式
继承与派生在C++中也是经常使用的,比如设计一个箱子类可以用以下代码实现:
Class CBox{
public:
int volume(){
return height*width*length;
}
void display(){
cout<<height<<endl;
cout<<width<<endl;
cout<<length<<endl;
}
private:
int height,width,length;
};
但假设现在有一批箱子比较特殊,有不同的颜色,且重量也都不一样,此时可以重新声明一个新的箱子的类,如下所示:
Class CBox_new{
public:
int volume(){
return height*width*length;
}
void display(){
cout<<height<<endl;
cout<<width<<endl;
cout<<length<<endl;
cout<<color<<endl;
cout<<weight<<endl;
}
private:
int height,width,length;
int color,weight;
};
可以看到上面的程序中有相当一部分是原来就已经有的,其实可以利用原来的CBox类作为基础,再加上新的内容即可,如下所示:
Class CBox_new::public CBox{ // 类CBox_new继承于类CBox
public:
void display1(){ // 新增加的成员函数
cout<<color<<endl;
cout<<weight<<endl;
}
private:
int color,weight; // 新增加的成员变量
};
这里,Box_new就是一个派生类。可见,声明派生类的一般形式为:
class 派生类名:[继承方式] 基类名{
派生类新增加的成员
};
其中的继承方式包括public(公用的)、private(私有的)和protected(受保护的),此项是可选的,如果不写此项,则默认为private(私有的)。
派生类里有两大部分内容:从基类继承而来的和在声明派生类时增加的部分。派生类中接受了基类的全部内容,这样可能出现有些基类的成员,在派生类中是用不到的,但是也必须继承过来的情况。这就会造成数据的冗余,尤其多次派生后,会在许多派生类对象中存在大量无用的数据,不仅浪费了大量的空间,而且在对象的建立、赋值、复制和参数的传递中,花费许多无谓的时间,从而降低了效率。因此,实际开发中要根据派生类的需要慎重选择基类,使冗余量最小。
2.?派生类的访问属性
(1)派生类中包含基类成员和派生类自己增加的成员,就产生了这两部分成员的关系和访问属性的问题。在建立派生类的时候,并不是简单地把基类的私有成员直接作为派生类的私有成员,把基类的公用成员直接作为派生类的公用成员;实际上,对基类成员和派生类自己增加的成员是按不同的原则处理的。具体来说,在讨论访问属性时,要考虑以下几种情况。
1)基类的成员函数只能访问基类的成员,而不能访问派生类的成员。
2)派生类的成员函数可以访问基类的成员,具体见后面详细描述;派生类的成员函数也可以访问派生类成员。
3)在派生类外可以访问基类的成员,具体见后面详细描述;在派生类外也可以访问派生类的公用成员,而不能访问派生类的私有成员。
(2)派生类的成员函数访问基类的成员和在派生类外访问基类的成员涉及如何确定基类的成员在派生类中的访问属性的问题,不仅要考虑对基类成员所声明的访问属性,还要考虑派生类所声明的对基类的继承方式,根据这两个因素共同决定基类成员在派生类中的访问属性。在派生类中,对基类的继承方式可以有public(公用的)、private(私有的)和protected(保护的)3种。不同的继承方式决定了基类成员在派生类中的访问属性。简单地说可以总结为以下几点。
1)公用继承(public inheritance):基类的公用成员和保护成员在派生类中保持原有访问属性,其私有成员仍为基类私有。
2)私有继承(private inheritance):基类的公用成员和保护成员在派生类中成了私有成员,其私有成员仍为基类私有。
3)受保护的继承(protected inheritance):基类的公用成员和保护成员在派生类中成了保护成员,其私有成员仍为基类私有。保护成员的意思是,不能被外界引用,但可以被派生类的成员引用。
在多级派生的情况下,各成员的访问属性仍按以上原则确定。假设类A为基类,类B是类A的派生类,类C是类B的派生类,则类C也是类A的派生类;类B称为类A的直接派生类,类C称为类A的间接派生类;类A是类B的直接基类,是类C的间接基类。派生关系如图2-3所示。
如果声明了以下的类:
class A{
public:
int var_A_pub;
protected:
void func_A_pro();
int var_A_pro;
private:
int var_A_pri;
};
class B:public A{
public:
void func_B_pub();
protect:
void func_B_pro();
private:
int var_B_pri;
};
class C:protected B{
public:
void func_C_pub();
private:
int var_C_pri;
};
则类A是类B的公用基类,类B是类C的保护基类。各成员在不同类中的访问属性如表2-1所示。
表2-1 派生的各个成员的访问属性
var_A_pub func_A_pro var_A_pro var_A_pri func_B_pub func_B_pro var_B_pri func_C_pub var_C_pri
基类A 公用 保护 保护 私有
公用派生类B 公用 保护 保护 不可访问 公用 保护 私有
保护派生类C 保护 保护 保护 不可访问 保护 保护 不可访问 公用 私有
通过以上分析,可以看到:无论哪一种继承方式,在派生类中是不能访问基类的私有成员的,私有成员只能被本类的成员函数所访问,毕竟派生类与基类不是同一个类。如果在多级派生时都采用公用继承方式,那么直到最后一级派生类都能访问基类的公用成员和保护成员。如果采用私有继承方式,经过若干次派生之后,基类的所有成员就会变成不可访问的了。如果采用保护继承方式,在派生类外是无法访问派生类中的任何成员的;而且经过多次派生后,人们很难清楚地记住哪些成员可以访问,哪些成员不能访问,很容易出错。因此,在实际中,常用的是公用继承。
3.?派生类的构造函数与析构函数
派生类的数据成员由所有基类的数据成员与派生类新增的数据成员共同组成,如果派生类新增成员中包括其他类的对象(子对象),派生类的数据成员中实际上还间接地包括了这些对象的数据成员。因此,构造派生类的对象时,必须对基类数据成员、新增数据成员和成员对象的数据成员进行初始化。派生类的构造函数必须要以合适的初值作为参数,隐含调用基类和新增对象成员的构造函数,来初始化它们各自的数据成员,然后再加入新的语句对新增普通数据成员进行初始化。
派生类构造函数的一般格式如下:
<派生类名>::<派生类名>(<参数表>) : <基类名1>(<参数表1>),
……,
<基类名n>(<参数表n>),
<子对象名1>(<参数表n+1>),
……,
<子对象名m>(<参数表n+m>) {
<派生类构造函数体> // 派生类新增成员的初始化
}
对派生类的构造函数有以下几点说明:
(1)对基类成员和子对象成员的初始化必须在成员初始化列表中进行,新增成员的初始化既可以在成员初始化列表中进行,也可以在构造函数体中进行。
(2)派生类构造函数必须对这3类成员进行初始化,其执行顺序是这样的:①先调用基类构造函数;②再调用子对象的构造函数;③最后调用派生类的构造函数体。
(3)当派生类有多个基类时,处于同一层次的各个基类的构造函数的调用顺序取决于定义派生类时声明的顺序(自左向右),而与在派生类构造函数的成员初始化列表中给出的顺序无关。
(4)如果派生类的基类也是一个派生类,则每个派生类只需负责其直接基类的构造,依次上溯。
(5)当派生类中有多个子对象时,各个子对象构造函数的调用顺序也取决于在派生类中定义的顺序(自前至后),而与在派生类构造函数的成员初始化列表中给出的顺序无关。
(6)派生类构造函数提供了将参数传递给基类构造函数的途径,以保证在基类进行初始化时能够获得必要的数据。因此,如果基类的构造函数定义了一个或多个参数时,派生类必须定义构造函数。
(7)如果基类中定义了默认构造函数或根本没有定义任何一个构造函数(此时由编译器自动生成默认构造函数)时,在派生类构造函数的定义中可以省略对基类构造函数的调用,即省略"<基类名>(<参数表>)"这个语句。
(8)子对象的情况与基类相同。
(9)当所有的基类和子对象的构造函数都可以省略时,可以省略派生类构造函数的成员初始化列表。
(10)如果所有的基类和子对象构造函数都不需要参数,派生类也不需要参数时,派生类构造函数可以不定义。派生类构造函数的使用可以参考例2.26的程序。
【例2.26】 派生类构造函数的使用举例。
#include<iostream>
#include<string>
using namespace std;
class CStudent{ // 声明基类Student
public:
CStudent(int n,string nam,char s){ // 基类构造函数
num=n;
name=nam;
sex=s;
}
~CStudent(){} // 基类析构函数
protected: // 保护部分
int num;
string name;
char sex ;
};
class CStudent1: public CStudent{ // 声明派生类Student1
public : // 派生类的公用部分
CStudent1 (int n,string nam,char s,int a,string ad): CStudent (n,nam,s){
// 派生类构造函数
age=a; // 在函数体中只对派生类新增的数据成员初始化
addr=ad;
}
void show(){
cout<<"num: "<<num<<endl;
cout<<"name: "<<name<<endl;
cout<<"sex: "<<sex<<endl;
cout<<"age: "<<age<<endl;
cout<<"address: "<<addr<<endl<<endl;
}
~CStudent1(){ } // 派生类析构函数
private : // 派生类的私有部分
int age;
string addr;
};
int main(){
CStudent1 stud1(10010,"Wang-li",'f',19,"115 Beijing Road,Shanghai");
CStudent1 stud2(10011,"Zhang-fun",'m',21,"213 Shanghai Road,Beijing");
stud1.show(); // 输出第一个学生的数据
stud2.show(); // 输出第二个学生的数据
return 0;
}
程序的执行结果是:
num: 10010
name: Wang-li
sex: f
age: 19
address: 115 Beijing Road,Shanghai
num: 10011
name: Zhang-fun
sex: m
age: 21
address: 213 Shanghai Road,Beijing
例2.26中定义了类CStudent、类CStudent1,其中类CStudent1继承了类CStudent。类CStudent1比基类新增了两个数据成员。基类CStudent有自己的带参构造函数,而派生类的构造函数,则只须对派生类新增的数据成员初始化,不过需要把基类的参数也给带进去。
析构函数的作用是在对象撤销之前,进行必要的清理工作。当对象被删除时,系统会自动调用析构函数。
析构函数比构造函数简单,没有类型,也没有参数。在派生时,派生类是不能继承基类的析构函数的,也需要通过派生类的析构函数去调用基类的析构函数。在派生类中可以根据需要定义自己的析构函数,用来对派生类中所增加的成员进行清理工作;基类的清理工作仍然由基类的析构函数负责。在执行派生类的析构函数时,系统会自动调用基类的析构函数和子对象的析构函数,对基类和子对象进行清理。
4.?派生类的构造函数与析构函数的调用顺序
前面已经提到,构造函数和析构函数的调用顺序是先构造的后析构,后构造的先析构。那么基类和派生类中的构造函数和析构函数的调用顺序是否也是如此呢?
构造函数的调用顺序规则如下所述。
1)基类构造函数。如果有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序,而不是它们在成员初始化表中的顺序。
2)成员类对象构造函数。如果有多个成员类对象,则构造函数的调用顺序是对象在类中被声明的顺序,而不是它们出现在成员初始化表中的顺序。
3)派生类构造函数。
而析构函数的调用顺序与构造函数的调用顺序正好相反,将上面3点内容中的顺序反过来用就可以了,即:首先调用派生类的析构函数;其次再调用成员类对象的析构函数;最后调用基类的析构函数。析构函数在下面3种情况时被调用。
1)对象生命周期结束被销毁时(一般类成员的指针变量与引用都不自动调用析构函数)。
2)delete指向对象的指针时,或delete指向对象的基类类型指针,而其基类虚构函数是虚函数时。
3)对象i是对象o的成员,o的析构函数被调用时,对象i的析构函数也被调用。
下面用例2.27来说明构造函数的调用顺序。
【例2.27】 构造函数的调用顺序。
#include<iostream>
using namespace std;
class CBase{
public:
CBase (){ std::cout<<"CBase::CBase()"<<std::endl; }
~ CBase (){ std::cout<<"CBase::~CBase()"<<std::endl; }
};
class CBase1:public CBase {
public:
CBase1 (){ std::cout<<"CBase::Base1()"<<std::endl; }
~ CBase1 (){ std::cout<<"CBase::~Base1()"<<std::endl; }
};
class CDerive{
public:
CDerive (){ std::cout<<"CDerive::CDerive()"<<std::endl; }
~ CDerive (){ std::cout<<"CDerive::~CDerive()"<<std::endl; }
};
class CDerive1:public CBase1{
private:
CDerive m_derive;
public:
CDerive1(){ std::cout<<"CDerive1::CDerive1()"<<std::endl; }
~CDerive1(){ std::cout<<"CDerive1::~CDerive1()"<<std::endl; }
};
int main(){
CDerive1 derive;
return 0;
}
程序的执行结果是:
CBase::CBase()
CBase::Base1()
CDerive::CDerive()
CDerive1::CDerive1()
CDerive1::~CDerive1()
CDerive::~CDerive()
CBase::~Base1()
CBase::~CBase()
例2.27中声明了4个类,CBase1继承于CBase、CDerive1继承于CBase1、CDerive1中有CDerive的成员变量,最后定义一个CDerive1对象,用来确定各种基类、派生类的构造函数和析构函数的执行顺序。执行结果与上文中描述的调用顺序相符。
总的来说,构造函数的调用顺序是:①如果存在基类,那么先调用基类的构造函数,如果基类的构造函数中仍然存在基类,那么程序会继续进行向上查找,直到找到它最早的基类进行初始化(如上例中类Derive1,继承于类Base与Base1);②如果所调用的类中定义的时候存在对象被声明,那么在基类的构造函数调用完成以后,再调用对象的构造函数(如上例在类Derive1中声明的对象Derive m_derive);③调用派生类的构造函数(如上例最后调用的是Derive1类的构造函数)。
相关资源:敏捷开发V1.0.pptx