C++ 面向对象 - -派生与继承(二)

目录

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

1、简单的派生类的构造函数

2、有子对象的派生类的构造函数

3、多层派生时的构造函数

4、派生类构造函数的特殊形式

5、派生类的析构函数

多重继承

1、声明多重继承的方法

2、多重继承派生类的构造函数

3、多重继承引起的二义性问题

4、虚基类

基类与派生类的转换

继承与组合

 


 

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

前面已提到,基类的构造函数和析构函数是不能被继承的,在声明派生类时,派生类并未没有把基类的构造函数继承过来,构造函数的主要作用是对数据成员初始化,那么对继承过来的基类成员和新增的成员初始化的工作都需要由派生类的构造函数承担,也就是说,在执行派生类的构造函数时,使派生类的数据成员和基类中的数据成员同时都被初始化

 

1、简单的派生类的构造函数

我们说的 “简单” 是指一个基类直接派生出一个子类(只有一个基类,且只有一级派生),派生类的数据成员中不包含基类的对象(即子对象)。

简单派生类的构造函数一般形式:

          派生类构造函数名 (总参数列表) : 基类构造函数名(参数列表)

                     {

                           派生类中新增数据成员初始化语句

                    }

注意:只在定义构造函数时列出调用基类构造函数的部分,声明时不需要,调用基类构造函数时,可以直接使用常量或全局变量。

举个简单的例子:

#include 
using namespace std;
class Person{
		int age ;
		string name ;
		char sex ;
	public:
		Person(int a=0,string n="xu",char s='b'):age(a),name(n),sex(s){}
		void show_1(){
			cout<<"age=	"<上述派生类构造函数中有五个参数,其中前三个是用来传递给基类的构造函数,后面两个(sc和 p)是用来对派生类所增加的数据成员初始化的。

需要注意的是,在定义派生类的构造函数的时候,我们可以看到派生类构造函数名后面括号内的参数表包括参数的类型和参数名(如 int a),而基类构造函数名后面括号内的参数表列只有参数名而不包括参数类型(如 a ,n,s)这是因为在这里不是定义基类构造函数,而是调用基类构造函数,因此这些参数是实参而不是形参它们可以是常量、全局变量和派生类构造函数总参数表中的参数。另外调用基类构造函数时传的实参和定义基类构造函数时指定的参数是相匹配的。

在派生类类外定义构造函数和前面定义普通类的构造函数一样,

Student :: Student(int a=0,string n="mu",char s='g',int sc=100,int r=1):Person(a,n,s),score(sc),ranking(r){}

定义时传给基类构造函数一个常量,

Student(int s='g',int sc=100,int r=1):Person(18,"song",s),score(sc),ranking(r){}

由于是调用基类的构造函数,所以派生类总参数表中对基类构造函数的初始化顺序不作要求,如:

Student(int s=1,int sc=100,int r=1):Person(s,"song",'g'),score(sc),ranking(r){}

最后需要注意的是,在建立一个对象时,执行构造函数的顺序是:派生类构造函数先调用基类构造函数,再执行派生类构造函数本身(即派生类构造函数的函数体)。对于上个例子来说,就是先初始化 name、age和sex,然后再初始化 score 和 ranking 。在派生类对象释放时,先执行派生类析构函数,再执行基类析构函数,这个后面再说。

 

2、有子对象的派生类的构造函数

以前所介绍过的类,其数据成员都是标准类型(如 int ,char )或系统提供的类型(如 string ),实际上,我们在类中的数据成员还可以包含类对象。我们在声明一个类的数据成员时定义了另一个类的对象,这个对象就是该类的子对象,也就是该类对象的内嵌对象,即对象中的对象。如同上个例子:

#include 
using namespace std;
class Person{
		int age ;
		string name ;
		char sex ;
	public:
		Person(int a=0,string n="xu",char s='b'):age(a),name(n),sex(s){}
		void show_1(){
			cout<<"age=	"<

派生类构造函数的任务应包括三个部分:1)对基类数据成员的初始化;2)对子对象数据成员初始化;3)对派生类数据成员初始化。即派生类构造函数的一般形式:

派生类构造函数名(总参数表):基类构造函数名(参数列表),子对象名(参数列表),

{

          派生类中新增数据数据成员初始化语句;

}

当然子对象和基类的初始化都是调用其他的构造函数,所以在派生类中对其进行初始化时,只需传进实参即可。另外基类构造函数和子对象的调用次序可以是任意的,编译系统是根据相同的参数名来确定传递关系的。但是一般先写基类构造函数,与调用顺序一致。

最后说一下执行派生类构造函数的顺序:调用基类构造函数,初始化基类数据成员,再调用子对象构造函数,初始化子对象数据成员,最后执行派生类自身的构造函数,初始化自己新增数据成员。

 

3、多层派生时的构造函数

当出现多层派生时,不能列出每一层的构造函数,只须写出其上一层派生类(即它的直接基类)的构造函数。如:

基类 student 构造函数首部为:
student ( int n, string nam )

一级派生类student1 构造函数首部为
student1 ( int n, string nam, int a ):student (n,nam)

二级派生类student2 构造函数首部为
student2 ( int n, string nam, int a, int s ):student1 (n,nam,a)

注意,二级派生类student2 构造函数首部不要写成:
student2 ( int n, string nam, int a, int s ):student (n, nam), student1(n, m, a) //错误!!

不要列出每一层构造函数,只须列出其上一层的构造函数!!

多级派生类初始化的顺序是:先初始化基类的数据成员 num 和 name,再初始化 Student1 的数据成员 age,最后再初始化 Student2的数据成员 score 

 

4、派生类构造函数的特殊形式

  • 在当不需要对派生类新增的成员进行任何初始操作时,派生类构造函数的函数体可以为空,即构造函数是空函数,同参数初始化表类似。如:
Student_1 (int n,string nam , int n1, string name1):Student(n,nam),s(n1,nam1){}
// s 是 Student的类对象

此派生类构造函数的作用只是为了将参数传递给基类构造函数和子对象,并在执行派生类构造函数时调用基类构造函数和子对象构造函数。

  • 如果基类和子对象的类型声明中都没有定义构造函数或者定义了没有参数的构造函数,在定义派生类构造函数时,可以不写基类构造函数或者子对象,因为此时派生类构造函数没有向基类构造函数和子对象传递参数的任务。在调用派生类构造函数时,系统会自动调用基类和子对象的默认构造函数。但如果基类或者子对象类型的声明中定义了带参数的构造函数,那么就必须显式地定义派生类构造函数,并在其中写出基类或子对象的构造函数及其参数表。

 

5、派生类的析构函数

析构函数的作用:在撤消对象之前,进行一些清理工作。当对象被删除时,系统会自动调用析构函数。和构造函数一样,析构函数也是不能继承给派生类的,在派生类中必须编写自己的析构函数,来做一些派生类自己的清理工作,并调用基类的析构函数。

调用析构函数的顺序:与调用构造函数的顺序正好相反:先执行派生类自己的析构函数,清理派生自己新增数据成员;再调用子对象的析构函数,清理子对象;最后调用基类的析构函数,对基类进行清理。

 

多重继承

一个子类由两个或多个基类派生而来,子类从两个或多个基类中继承所需的属性。比如,在职研究生既有在职职工的属性,又有研究生的属性。这就是多重继承。

多重继承的优点是:自然地做到对单继承的扩展,可以继承多个类的功能。但其结构复杂,优先顺序比较模糊,有的功能会产生冲突。

1、声明多重继承的方法

声明多重继承的方法:设已声明了类 A,B,C,从它们派生出子类 D,其声明的一般形式为:

 

class D : public A, private B, protected C

{
    D():A(),B(),C(){} 
    类D新增加的成员;
}

  可看出子类 是按照不同的继承方式从不同的基类多重继承而来的派生类。

 

2、多重继承派生类的构造函数

多重继承派生类的构造函数与单继承派生类的构造函数基本相同,只是在初始表中包含多个基类构造函数。其一般形式是:

派生类构造函数名(总参数表):基类1构造函数 (参数列表1),基类2构造函数 (参数列表2),基类3构造函数 (参数列表3)
{
    派生类中新增数据成员初始化语句;
}

注意:各基类构造函数的排列顺序任意,派生类构造函数的执行顺序是先按照声明派生类时基类出现的顺序调用基类的构造函数,再执行派生类构造函数

下面举一个在职研究生继承教师和学生的例子:

#include 
#include 
using namespace std;
class Teacher{
	protected:
		string name ;
		int age ;
		string title ;	//职称
	public:
		Teacher(string n,int a,string t):name(n),age(a),title(t){}
		void display(){
			cout<<"name : "<

在写的时候会发现 Teacher 和 Student 中都有名字 name 这一项,写一样的话会出错,提示说 name 是模糊不清的,如果在同一个派生类中存在两个同名的数据成员,在用派生类的成员函数 show 中引用 name 时就会出现二义性,编译系统无法判定应该选择哪一个基类中的 name ,这部分后面再具体说明。虽然这段程序是用了两个 name 和 name_1 来分别代表两个基类中的姓名,这样做虽然解决了问题,但并不是很合适的,比较好的方法是:在两个基类中可以用同一个数据成员名 name ,在 show 函数中引用数据成员时指明其作用域,即:

cout << "name :" << Teacher::name<

这样就是唯一的,不会引起二义性。另外会发现多重继承中也出现了前边所说的问题,数据冗余。这是很常见的,因为一般情况下使用的是现成的基类,如果有多个基类,这个问题会更突出,所以在设计派生类时要仔细考虑其数据成员,尽量减少数据冗余。

 

3、多重继承引起的二义性问题

前边已涉及到了这个问题,虽然多重继承给写程序带来了灵活性,但出现了一个问题就是 二义性问题。这一般分为三种情况:

1)两个基类有同名成员,如:

class A{
	public:
		int a;
		void display();
};
class B{
	public:
		int a;
		void display();
};
class C:public A,public B{
	public:
		int c;
		void show();
};

只是为了说明,简化的一个框架。如果我们在 main 函数中定义 C 类对象 c ,并调用数据成员 a 和成员函数 dispaly :

C c ;
c.a=1 ;    //调用数据成员 a
c.display() ;    //调用成员函数 display 

//但都不知道访问的哪一个基类的成员,程序编译出错

//可以用基类名限定符来解决:
c.A::a=1 ;
c.A::display() ;

而如果在 show 函数中去访问其中一个基类的成员,只需要加上类限定符,指定是哪一个基类即可。

 

2)两个基类和派生类三者都有同名成员,将上面的 C 类改为:

class C:public A,public B{
    int a ;
    void display() ;
}

此时如果再在 main 函数中访问数据成员 a 和成员函数 display :

C c ;
c.a=1 ;
c.display() ;

我们会发现这段程序是能通过编译的,也可以正常运行,那问题是我现在访问的哪一个类中的成员?访问的是 C 类中的成员,因为根据同名覆盖规则基类的同名成员在派生类中被屏蔽,成为 “不可见” 的,或者说派生类新增的同名成员覆盖了基类中的同名成员。因此如果在定义派生类对象的模块中通过对象名访问同名的成员,则访问的是派生类中的成员。此时如果想访问基类中的成员,同样加上类限定符即可。再说明一次:不同的成员函数,只有在函数名和参数个数相同,类型相匹配的情况下才发生同名覆盖,如果只有函数名相同而参数不同,不会发生同名覆盖,而属于函数重载。

3)类 A 和类 B 是从同一个基类派生的,如:

class N{
	int a;
	void display(){
		cout<<"A::a="<在类 A 和类 B 中虽然没有定义数据成员 a 和成员函数 display ,但是它们分别从类 N 中继承了数据成员 a 和成员函数 display ,这样在类 A 和类 B 中同时存在着两个同名的数据成员 a 和成员函数 display ,它们是类 N 成员的拷贝。类 A 和类 B 中的数据成员 a 代表两个不同的存储单元,可以分别存放不同的数据,在程序中可以通过类 A 和类 B 的构造函数去调用基类 N 的构造函数,分别对类 A 和类 B 的数据成员初始化。

如果想访问 A 类中从基类 N 中继承下来的成员,我们该怎么去实现呢?显然不能用

C c;
c.a=1 ;
c.N::a=1 ;

这样也不知道是类 A 从基类 N 中继承下来的成员还是类 B 中从基类 N 继承下来的成员。应当通过类 N 的直接派生类名来指明要访问是类 N 的哪一个派生类中的基类成员。如:

c.A::a=1;
c.A::display();    //都是访问的类 N 的派生类 A 中的基类成员

 

4、虚基类

前面提到,在多重继承过程中容易出现二义性的问题,在引用这些同名的成员时,必须在派生类对象名后增加直接基类名,以避免产生二义性,使其唯一地标识一个成员(如 c . A :: display())。在一个类中保留间接共同基类的多份同名成员,虽然有时是有必要的,可以再不同的数据成员中存放不同的数据,也可以通过构造函数分别对它们进行初始化;但大多数情况下,这种现象是人们不愿意看到的,因为保留多份数据成员的拷贝,不仅占用较多的存储空间,还增加了访问这些成员时的困难,容易出错,而且实际上也并不需要有多份拷贝。

C++提供虚基类的方法,使得在继承间接共同基类时只保留一份成员。其解决思路是:在继承间接共同基类时,内存里只存储一份成员,即使用虚基类的技术。注意虚基类并不是在声明基类时声明的,而是在声明派生类时指定继承方式时声明的,虚基类定义的一般形式:

              class 派生类名:virtual  继承方式 基类名

经过这样的声明后,当基类通过多条派生路径被一个派生类继承时,该派生类只继承该基类一次,即基类成员只保留一次。为了保证虚基类在派生类中只继承一次,应当在该基类的所有直接派生类中声明为虚基类,否则仍然会出现对基类的多次继承。如:

C++ 面向对象 - -派生与继承(二)_第1张图片

只在 A,B 中将 N 声明为虚基类,而 C 中没有将 N 声明为虚基类,则派生类 D 中仍然有冗余的基类拷贝。

虚基类的初始化:

在最后的派生类中,不仅要初始化其直接基类,而且还要初始化虚基类。如果虚基类中定义了带参数的构造函数,而且没有定义默认构造函数,则在所有直接派生和间接派生类中通过构造函数的初始化表对虚基类进行初始化。例如:

class A
{ A (int i) { }
};
class B : virtual public A
{ B (int n) : A(n) { }        //对虚基类初始化
};
class C: virtual public A
{ C(int n) : A(n) { }
};
class D: public B, public C        //在初始化表中对所有基类进行初始化
{ D(int n): A(n), B(n), C(n) { }
};

在以前,在派生类的构造函数中只需负责对其直接基类初始化,再由其直接基类负责对间接基类初始化。现在,由于虚基类在派生类中只有一份数据成员,所以这份数据成员的初始化必须由派生类直接给出。如果在最后的派生类 D 中不对虚基类进行初始化,而是由虚基类的直接派生类 B 和 C 对虚基类初始化,就有可能由于在类 B 和 类 C 的构造函数中对虚基类给出不同的初始化参数而产生矛盾。所以就规定:在最后的派生类中不仅负责对直接基类进行初始化,还要对虚基类初始化。另外编译系统只执行最后的派生类对虚基类的构造函数的调用,而忽略了虚基类的其他派生类(如类 B 和 类 C)对虚基类的构造函数的调用,这就保证了虚基类的数据成员不会被多次初始化。

下面写个例子:

#include 
#include 
using namespace std;
class Person{
	protected:
		string name ;
		char sex ;
		int age ;
	public:
		Person(string nam,char s,int a):name(nam),sex(s),age(a){}
}; 
class Teacher:virtual public Person{
	protected:
		string title ;	//职称
	public:
		Teacher(string nam,char s,int a,string t):Person(nam,s,a),title(t){}
};
class Student:virtual public Person{
	protected:
		float score ;
	public:
		Student(string n,char s,int a,float sc):Person(n,s,a),score(sc){}
};
class Graduate:public Teacher,public Student{
		float wage ;	//津贴
	public:
		Graduate(string nam,int a,char s,string t,float sco,float w):
			Person(nam,s,a),Teacher(nam,s,a,t),Student(nam,s,a,sco),wage(w){}
			//注意多了对 虚基类 Person 的初始化,还有 Teacher 和 Student 的初始化参数个数是四个。 
		void show(){
			cout<<" name: "<

和上边的例子最后实现功能是一样的,只是中间在初始化过程中有不一样的地方,读者可以比较一下。在最后派生类 Graduate 的构造函数中,既包括对虚基类构造函数的调用,也包括对其直接基类的初始化。

需要说明的是:

在 Teacher 类和 Student 类的构造函数中仍需保留对基类的初始化,尽管编译系统不会在此执行,而是在派生类 Graduate 的构造函数中,调用虚基类和直接基类的构造函数。由于虚基类只保留一份基类的成员,因此可以用类 Graduate 中的 show 函数引用公共基类 Person 中的数据成员name,sex,age,不需要加基类名和 域限定符(::),不会产生二义性。这就是用虚基类的好处,但不要忘了在最后的派生类中对虚基类的初始化

 


最后总结一下派生类的构造函数和析构函数调用原则

  1. 先基类,再对象,后自己;
  2. 在同一层上如有多个基类,则先虚基类,后非虚基类,并按派生时定义的先后次序执行
  3. 若虚基类是由非虚基类派生而来,则仍然先调用基类的构造函数,再调用派生类的构造函数;
  4. 对于一个派生类的某个虚基类的构造函数一旦被执行过,就不再被多次执行;
  5. 析构函数与构造函数的调用顺序相反

另外使用多重继承时要十分小心,经常会出现二义性问题。许多专业人员认为: 不要提倡在程序中使用多重继承,只有在比较简单和不易出现二义性的情况或实在必要时才使用多重继承,能用单一继承解决的问题就不要使用多重继承。因此有些面向对象的程序设计语言(如Java,Smalltalk)并不支持多重继承。

 

基类与派生类的转换

从前面的介绍中可以看出:三种继承方式中,只有公有继承能较好地保留基类的特征,它保留了除了构造函数和析构函数以外的基类所有成员,基类中成员的各种访问权限也全部都按原样保留下来了,公用派生类具有基类的全部功能,所有基类能够实现的功能,公用派生类也都能实现,因此,只有公有派生类才是基类真正的子类型,它完整地继承了基类的功能。

我们知道不同数据之间在一定条件下可以进行类型的转换,例如整型数据可以赋值给双精度型变量,在赋值之前,先把整型数据转换成为双精度型数据,但是不能把一个整型数据赋给指针变量,这种不同类型数据之间的自动转换和赋值,称为 赋值兼容 。同样,基类与派生类之间也有赋值兼容关系,由于派生类中包含从基类继承的成员,因此可以将派生类对象的值赋给基类对象,在用到基类对象的时候可以用其子类对象代替。具体表现在以下几个方面:

派生类对象可以向基类对象赋值

可以通过子类(即公用派生类)对象对其基类对象赋值,如:

A a ;    //定义基类 A 的对象 a 
B b ;    //定义 A 类的公用派生类 B 的对象 b 
a=b ;

在赋值时派生类舍弃掉自己的成员,也就是 “大材小用”,应当知道的是子类型关系是单向的,不可逆的。 B 是 A 的子类型,而不能说 A 是  B 的子类型,也只能用子类对象对其基类对象赋值,而不能用基类对象对其子类对象赋值,因为基类对象不包含派生类新增的成员,故无法对派生类的成员赋值。同理,同一基类的不同派生类对象之间也不能赋值

 

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

A a ;    //定义基类 A 的对象 a 
B b ;    //定义 A 类的公用派生类 B 的对象 b 
A &c=a ;    // c 是 a的引用(别名),共享同一段存储单元
A &c=b ;    // 用派生类对象对 a 的引用 c 赋值

此时的赋值应注意,c 不是 b 的别名,也不是与 b 共享一段存储单元,它只是 b 中基类部分的别名,c 与 b 中基类部分共享同一段存储单元,c 与 b 有相同的起始地址

 

如果函数的参数是基类对象或基类对象的引用,相应的实参可以用子类对象

若有一函数 fun:

void fun(A &a){    //定义形参是类 A 的对象的引用
    cout << a.num <

同前边一样,fun 函数中只能输出派生类中基类成员的值。

 

派生类对象的地址可以赋给指向基类对象的指针变量,也就是说,指向基类对象的指针变量也可以用来指向派生类对象

举个栗子:

#include 
using namespace std;
class Student{
		int num ;
		string name ;
		float score ;
	public:
		Student(int n,string nam,float s):num(n),name(nam),score(s){}
		void display();
};
void Student :: display(){
	cout<<"  num= "<display();
	
	cout<display(); 
}

很多读者会认为:在派生类中有两个同名的 成员函数,根据同名覆盖原则,被调用的应当是派生类对象的成员函数,在执行其成员函数过程中调用基类的成员函数。但我们看过这段代码的运行结果后会发现和我们所想的并不一样,前三行是基类的三个数据,后三行是派生类的数据,并没有输出 wage 的值。我最初读这段代码的时候,就是这样想的,我刚写的时候还是这样想的 ......,那么问题在什么地方呢?问题在于 p 是指向 Student 类对象的指针变量,即使让它指向了 grad ,但实际上 p 指向的是 grad 中从基类继承的部分。通过指向基类对象的指针,只能访问派生类中的基类成员,而不能访问派生类增加的成员。所以 p->display() 调用的不是派生类对象所增加的函数,而是基类的函数,所以只输出了基类中的三个数据成员。如果想通过指针输出研究生 的 wage,可另设一个指向派生类对象的指针变量pt,使它指向 grad,再执行 pt->display() 去调用派生类对象的 display 函数。只是不大方便。

从上面的例子来看,需要注意的是当基类指针或引用指向公有派生类对象时,只能访问派生类中从基类继承过来的成员,不能访问派生类定义的成员。在成员函数覆写的情况下,基类本来可以被继承的其他同名重载函数都会被屏蔽而不会被继承。

 

继承与组合

一个类可以用基类对象作为数据成员,即子对象。现在我们说,在一个类中,可以用另一个类的对象作为数据成员,叫类的组合。

继承与组合的区别:

继承:派生类与基类的关系是 “是”(is a)的关系,即派生类是基类。例如:白猫是猫。

组合:指一个类包含另一个类,它们是 “有”(has a)的关系。例如:教授“有”一个生日。

继承是纵向的,组合是横向的。

如果修改了成员类的部分内容,只要成员类的公用接口(如头文件名)不变,组合类可以不修改,但需要重新编译。

 


 

你可能感兴趣的:(C++面向对象)