12继承与派生

继承与派生


面向对象程序设计有4个主要特点: 抽象、封装、继承和多态性。

要较好地进行面向对象程序设计,还必须了解面向对象程序设计另外两个重要特征——继承性和多态性。在本章中主要介绍有关继承的知识,在第12章中将介绍多态性。

面向对象技术强调软件的可重用性(software reusability) 。C++语言提供了类的继承机制,解决了软件重用问题。

11.1 继承与派生的概念

在C++中可重用性是通过继承(inheritance)这一机制来实现的。继承是C++的一个重要组成部分。

一个类中包含了若干数据成员和成员函数。在不同的类中,数据成员和成员函数是不相同的。但有时两个类的内容基本相同或有一部分相同。

利用原来声明的类Student作为基础,再加上新的内容即可,以减少重复的工作量。 C++提供的继承机制就是为了解决这个问题。

在第8章已举了马的例子来说明继承的概念。见图11.1示意。

                       

一个新类从已有的类那里获得其已有特性,这种现象称为类的继承。通过继承,一个新建子类从已有的父类那里获得父类的特性。从另一角度说,从已有的类(父类)产生一个新的子类,称为类的派生。类的继承是用已有的类来建立专用类的编程技术。派生类继承了基类的所有数据成员和成员函数,并可以对成员作必要的增加或调整。一个基类可以派生出多个派生类,每一个派生类又可以作为基类再派生出新的派生类,因此基类和派生类是相对而言的。

以上介绍的是最简单的情况: 一个派生类只从一个基类派生,这称为单继承(single inheritance),这种继承关系所形成的层次是一个树形结构,可以用图11.3表示。

11.2 派生类的声明方式

假设已经声明了一个基类Student,在此基础上通过单继承建立一个派生类Student1:

class Student1: public Student//声明基类是Student

{public:

void display_1( )                          //新增加的成员函数

{cout<<″age: ″<<age<<endl;

cout<<″address: ″<<addr<<endl;}

private:

int age;                               //新增加的数据成员

string addr;                           //新增加的数据成员

};

基类名前面有public的称为“公用继承(public inheritance)”。

声明派生类的一般形式为

class 派生类名: [继承方式] 基类名

{

派生类新增加的成员

} ;

继承方式包括: public(公用的),private(私有的)和protected(受保护的),此项是可选的,如果不写此项,则默认为private(私有的)。

11.3 派生类的构成

派生类中的成员包括从基类继承过来的成员和自己增加的成员两大部分。在基类中包括数据成员和成员函数(或称数据与方法)两部分,派生类分为两大部分: 一部分是从基类继承来的成员,另一部分是在声明派生类时增加的部分。每一部分均分别包括数据成员和成员函数。

实际上,并不是把基类的成员和派生类自己增加的成员简单地加在一起就成为派生类。构造一个派生类包括以下3部分工作:

(1) 从基类接收成员。派生类把基类全部的成员(不包括构造函数和析构函数)接收过来,也就是说是没有选择的,不能选择接收其中一部分成员,而舍弃另一部分成员。

要求我们根据派生类的需要慎重选择基类,使冗余量最小。事实上,有些类是专门作为基类而设计的,在设计时充分考虑到派生类的要求。

(1)   调整从基类接收的成员。接收基类成员是程序人员不能选择的,但是程序人员可以对这些成员作某些调整。

(3) 在声明派生类时增加的成员。这部分内容是很重要的,它体现了派生类对基类功能的扩展。要根据需要仔细考虑应当增加哪些成员,精心设计。

此外,在声明派生类时,一般还应当自己定义派生类的构造函数和析构函数,因为构造函数和析构函数是不能从基类继承的。

派生类是基类定义的延续。可以先声明一个基类,在此基类中只提供某些最基本的功能,而另外有些功能并未实现,然后在声明派生类时加入某些具体的功能,形成适用于某一特定应用的派生类。通过对基类声明的延续,将一个抽象的基类转化成具体的派生类。因此,派生类是抽象基类的具体实现。

11.4 派生类成员的访问属性

既然派生类中包含基类成员和派生类自己增加的成员,就产生了这两部分成员的关系和访问属性的问题。在建立派生类的时候,并不是简单地把基类的私有成员直接作为派生类的私有成员,把基类的公用成员直接作为派生类的公用成员。实际上,对基类成员和派生类自己增加的成员是按不同的原则处理的。

具体说,在讨论访问属性时,要考虑以下几种情况:

(1) 基类的成员函数访问基类成员。

(2) 派生类的成员函数访问派生类自己增加的成员。

(3) 基类的成员函数访问派生类的成员。

(4) 派生类的成员函数访问基类的成员。

(5) 在派生类外访问派生类的成员。

(6) 在派生类外访问基类的成员。

对于第(1)和第(2)种情况,比较简单,按第8章介绍过的规则处理,即: 基类的成员函数可以访问基类成员,派生类的成员函数可以访问派生类成员。私有数据成员只能被同一类中的成员函数访问,公用成员可以被外界访问。

第(3)种情况也比较明确,基类的成员函数只能访问基类的成员,而不能访问派生类的成员。第(5)种情况也比较明确,在派生类外可以访问派生类的公用成员,而不能访问派生类的私有成员。

对于第(4)和第(6)种情况,就稍微复杂一些,也容易混淆。

这些牵涉到如何确定基类的成员在派生类中的访问属性的问题,不仅要考虑对基类成员所声明的访问属性,还要考虑派生类所声明的对基类的继承方式,根据这两个因素共同决定基类成员在派生类中的访问属性。

前面已提到: 在派生类中,对基类的继承方式可以有public(公用的),private(私有的)和protected(保护的)3种。不同的继承方式决定了基类成员在派生类中的访问属性。

简单地说:

(1) 公用继承(public inheritance)

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

(2) 私有继承(private inheritance)

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

(3) 受保护的继承(protected inheritance)

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

保护成员的意思是: 不能被外界引用,但可以被派生类的成员引用,具体的用法将在稍后介绍。

11.4.1 公用继承

在定义一个派生类时将基类的继承方式指定为public的,称为公用继承,用公用继承方式建立的派生类称为公用派生类(public derived class),其基类称为公用基类(public base class)。

采用公用继承方式时,基类的公用成员和保护成员在派生类中仍然保持其公用成员和保护成员的属性,而基类的私有成员在派生类中并没有成为派生类的私有成员,它仍然是基类的私有成员,只有基类的成员函数可以引用它,而不能被派生类的成员函数引用,因此就成为派生类中的不可访问的成员。

公用基类的成员在派生类中的访问属性见书中表11.1。

例11.1 访问公有基类的成员。

下面写出类的声明部分:

Class Student//声明基类

{public:                                       //基类公用成员

void get_value( )

{cin>>num>>name>>sex;}

void display( )

{cout<<″ num: ″<<num<<endl;

cout<<″ name: ″<<name<<endl;

cout<<″ sex: ″<<sex<<endl;}

private :                                     //基类私有成员

   int num;

   string name;

   char sex;

};  

class Student1: public Student                 //以public方式声明派生类Student1

{public:

void display_1( )

{cout<<″ num: ″<<num<<endl;           //企图引用基类的私有成员,错误

       cout<<″ name: ″<<name<<endl;         //企图引用基类的私有成员,错误

       cout<<″ sex: ″<<sex<<endl;           //企图引用基类的私有成员,错误

       cout<<″ age: ″<<age<<endl;           //引用派生类的私有成员,正确

       cout<<″ address: ″<<addr<<endl;}     //引用派生类的私有成员,正确

private:

       int age;

       string addr;

 };

由于基类的私有成员对派生类来说是不可访问的,因此在派生类中的display_1函数中直接引用基类的私有数据成员num,name和sex是不允许的。只能通过基类的公用成员函数来引用基类的私有数据成员。

可以将派生类Student1的声明改为

class Student1: public Student//以public方式声明派生类Student1

{public:

void display_1( )                

{cout<<″ age: ″<<age<<endl;          //引用派生类的私有成员,正确

cout<<″ address: ″<<addr<<endl;     //引用派生类的私有成员,正确

}

private:

int age;                         

string addr;

};

然后在main函数中分别调用基类的display函数和派生类中的display_1函数,先后输出5个数据。

可以这样写main函数(假设对象stud中已有数据):

int main( )

{Student1  stud;//定义派生类Student1的对象stud

stud.display( );         //调用基类的公用成员函数,输出基类中3个数据成员的值

stud.display_1();       //调用派生类的公用成员函数,输出派生类中两个数据成员的值

return 0;

请根据上面的分析,写出完整的程序,程序中应包括输入数据的函数。

实际上,程序还可以改进,在派生类的display_1函数中调用基类的display函数,在主函数中只要写一行:

stud.display_1();

即可输出5个数据。

11.4.2 私有继承

在声明一个派生类时将基类的继承方式指定为private的,称为私有继承,用私有继承方式建立的派生类称为私有派生类(private derived class) ,其基类称为私有基类(private base class)。

私有基类的公用成员和保护成员在派生类中的访问属性相当于派生类中的私有成员,即派生类的成员函数能访问它们,而在派生类外不能访问它们。私有基类的私有成员在派生类中成为不可访问的成员,只有基类的成员函数可以引用它们。一个基类成员在基类中的访问属性和在派生类中的访问属性可能是不同的。私有基类的成员在私有派生类中的访问属性见书中表11.2。

对表11.2的规定不必死记,只需理解: 既然声明为私有继承,就表示将原来能被外界引用的成员隐藏起来,不让外界引用,因此私有基类的公用成员和保护成员理所当然地成为派生类中的私有成员。私有基类的私有成员按规定只能被基类的成员函数引用,在基类外当然不能访问他们,因此它们在派生类中是隐蔽的,不可访问的。

对于不需要再往下继承的类的功能可以用私有继承方式把它隐蔽起来,这样,下一层的派生类无法访问它的任何成员。

可以知道: 一个成员在不同的派生层次中的访问属性可能是不同的。它与继承方式有关。

例11.2 将例11.1中的公用继承方式改为用私有继承方式(基类Student不改)。

可以写出私有派生类如下:

class Student1: private Student//用私有继承方式声明派生类Student1

{public:

void display_1( )                       //输出两个数据成员的值

{cout<<″age: ″<<age<<endl;          //引用派生类的私有成员,正确

cout<<″address: ″<<addr<<endl;}    //引用派生类的私有成员,正确

private:

int age;                         

string addr;

};请分析下面的主函数:int main( )

{Student1  stud1;//定义一个Student1类的对象stud1

 stud1.display();             //错误,私有基类的公用成员函数在派生类中是私有函数

stud1.display_1( );          //正确。Display_1函数是Student1类的公用函数

stud1.age=18;                //错误。外界不能引用派生类的私有成员

return 0;

 }

可以看到:

(1) 不能通过派生类对象(如stud1)引用从私有基类继承过来的任何成员(如stud1.display()或stud1.num)。

(2) 派生类的成员函数不能访问私有基类的私有成员,但可以访问私有基类的公用成员(如stud1.display_1函数可以调用基类的公用成员函数display,但不能引用基类的私有成员num)。

有没有办法调用私有基类的公用成员函数,从而引用私有基类的私有成员呢?有。应当注意到: 虽然在派生类外不能通过派生类对象调用私有基类的公用成员函数,但可以通过派生类的成员函数调用私有基类的公用成员函数(此时它是派生类中的私有成员函数,可以被派生类的任何成员函数调用)。

可将上面的私有派生类的成员函数定义改写为

void display_1( )//输出5个数据成员的值

{display():                         //调用基类的公用成员函数,输出3个数据成员的值

 cout<<″age: ″<<age<<endl;        //输出派生类的私有数据成员

cout<<″address: ″<<addr<<endl;}  //输出派生类的私有数据成员

main函数可改写为

int main( )

{Student1 stud1;               

 stud1.display_1( );//display_1函数是派生类Student1类的公用函数

 return 0;

}

这样就能正确地引用私有基类的私有成员。可以看到,本例采用的方法是:

① 在main函数中调用派生类中的公用成员函数stud1.display_1;

② 通过该公用成员函数调用基类的公用成员函数display(它在派生类中是私有函数,可以被派生类中的任何成员函数调用);

③ 通过基类的公用成员函数display引用基类中的数据成员。

请根据上面的要求,补充和完善上面的程序,使之成为完整、正确的程序。程序中应包括输入数据的函数。

由于私有派生类限制太多,使用不方便,一般不常使用。

11.4.3 保护成员和保护继承

由protected声明的成员称为“受保护的成员”,或简称“保护成员”。从类的用户角度来看,保护成员等价于私有成员。但有一点与私有成员不同,保护成员可以被派生类的成员函数引用。见图11.8示意。

如果基类声明了私有成员,那么任何派生类都是不能访问它们的,若希望在派生类中能访问它们,应当把它们声明为保护成员。如果在一个类中声明了保护成员,就意味着该类可能要用作基类,在它的派生类中会访问这些成员。

在定义一个派生类时将基类的继承方式指定为protected的,称为保护继承,用保护继承方式建立的派生类称为保护派生类(protected derived class),其基类称为受保护的基类(protected base class),简称保护基类。

保护继承的特点是: 保护基类的公用成员和保护成员在派生类中都成了保护成员,其私有成员仍为基类私有。也就是把基类原有的公用成员也保护起来,不让类外任意访问。

将表11.1和表11.2综合起来,并增加保护继承的内容,见书中表11.3。

保护基类的所有成员在派生类中都被保护起来,类外不能访问,其公用成员和保护成员可以被其派生类的成员函数访问。

比较一下私有继承和保护继承(也就是比较在私有派生类中和在保护派生类中的访问属性),可以发现,在直接派生类中,以上两种继承方式的作用实际上是相同的: 在类外不能访问任何成员,而在派生类中可以通过成员函数访问基类中的公用成员和保护成员。但是如果继续派生,在新的派生类中,两种继承方式的作用就不同了。

例如,如果以公用继承方式派生出一个新派生类,原来私有基类中的成员在新派生类中都成为不可访问的成员,无论在派生类内或外都不能访问,而原来保护基类中的公用成员和保护成员在新派生类中为保护成员,可以被新派生类的成员函数访问。

从表11.3可知: 基类的私有成员被派生类继承后变为不可访问的成员,派生类中的一切成员均无法访问它们。如果需要在派生类中引用基类的某些成员,应当将基类的这些成员声明为protected,而不要声明为private。

如果善于利用保护成员,可以在类的层次结构中找到数据共享与成员隐蔽之间的结合点。既可实现某些成员的隐蔽,又可方便地继承,能实现代码重用与扩充。

通过以上的介绍,可以知道:

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

① 公用的,派生类内和派生类外都可以访问。

② 受保护的,派生类内可以访问,派生类外不能访问,其下一层的派生类可以访问。

③ 私有的,派生类内可以访问,派生类外不能访问。

④ 不可访问的,派生类内和派生类外都不能访问。

可以用书中表11.4表示。

需要说明的是:

①     这里所列出的成员的访问属性是指在派生类中所获得的访问属性。

② 所谓在派生类外部,是指在建立派生类对象的模块中,在派生类范围之外。

③ 如果本派生类继续派生,则在不同的继承方式下,成员所获得的访问属性是不同的,在本表中只列出在下一层公用派生类中的情况,如果是私有继承或保护继承,读者可以从表11.3中找到答案。

(2) 类的成员在不同作用域中有不同的访问属性,对这一点要十分清楚。

下面通过一个例子说明怎样访问保护成员。

例11.3 在派生类中引用保护成员。

#include <iostream>

#include <string>

using namespace std;

class Student//声明基类

{public:                             //基类公用成员

  void display( );

 protected :                         //基类保护成员

int num;

string name;

char sex;

};

void Student::display( )             //定义基类成员函数

   {cout<<″num: ″<<num<<endl;

    cout<<″name: ″<<name<<endl;

    cout<<″sex: ″<<sex<<endl;

   }

class Student1: protected Student        //用protected方式声明派生类Student1

{public:

void display1( );                      //派生类公用成员函数

private:

int age;                                //派生类私有数据成员

string addr;                          //派生类私有数据成员

};

void Student1::display1( )                 //定义派生类公用成员函数

    {cout<<″num: ″<<num<<endl;            //引用基类的保护成员,合法

     cout<<″name: ″<<name<<endl;          //引用基类的保护成员,合法

   cout<<″sex: ″<<sex<<endl;            //引用基类的保护成员,合法

     cout<<″age: ″<<age<<endl;            //引用派生类的私有成员,合法

     cout<<″address: ″<<addr<<endl;       //引用派生类的私有成员,合法

   }

int main( )

 {Student1 stud1;                     //stud1是派生类Student1类的对象

  stud1.display1( );                  //合法,display1是派生类中的公用成员函数

  stud1.num=10023;                    //错误,外界不能访问保护成员

return 0;

 }

在派生类的成员函数中引用基类的保护成员是合法的。保护成员和私有成员不同之处,在于把保护成员的访问范围扩展到派生类中。

注意: 在程序中通过派生类Student1的对象stud1的公用成员函数display1去访问基类的保护成员num.name和sex,不要误认为可以通过派生类对象名去访问基类的保护成员。

请补充、修改上面的程序,使之能正常运行。程序中应包括输入数据的函数。

私有继承和保护继承方式在使用时需要十分小心,很容易搞错,一般不常用。

11.4.4 多级派生时的访问属性

如果有图11.9所示的派生关系: 类A为基类,类B是类A的派生类,类C是类B的派生类,则类C也是类A的派生类。类B称为类A的直接派生类,类C称为类A的间接派生类。类A是类B的直接基类,是类C的间接基类。在多级派生的情况下,各成员的访问属性仍按以上原则确定。

例11.4 多级派生的访问属性。

如果声明了以下的类:

class A                         //基类

{public:

  int i;

 protected:

  void f2( );

  int j;

 private:

  int k;

};

class B: public A              //public方式

{public:

void f3( );

 protected:

void f4( );

 private:

  int m;

};

class C: protected B           //protected方式

{public:

void f5( );

 private:

  int n;

};

类A是类B的公用基类,类B是类C的保护基类。各成员在不同类中的访问属性如下:

无论哪一种继承方式,在派生类中是不能访问基类的私有成员的,私有成员只能被本类的成员函数所访问,毕竟派生类与基类不是同一个类。如果在多级派生时都采用公用继承方式,那么直到最后一级派生类都能访问基类的公用成员和保护成员。如果采用私有继承方式,经过若干次派生之后,基类的所有的成员已经变成不可访问的了。如果采用保护继承方式,在派生类外是无法访问派生类中的任何成员的。而且经过多次派生后,人们很难清楚地记住哪些成员可以访问,哪些成员不能访问,很容易出错。因此,在实际中,常用的是公用继承。

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

用户在声明类时可以不定义构造函数,系统会自动设置一个默认的构造函数,在定义类对象时会自动调用这个默认的构造函数。这个构造函数实际上是一个空函数,不执行任何操作。如果需要对类中的数据成员初始化,应自己定义构造函数。

构造函数的主要作用是对数据成员初始化。在设计派生类的构造函数时,不仅要考虑派生类所增加的数据成员的初始化,还应当考虑基类的数据成员初始化。也就是说,希望在执行派生类的构造函数时,使派生类的数据成员和基类的数据成员同时都被初始化。解决这个问题的思路是: 在执行派生类的构造函数时,调用基类的构造函数。

11.5.1 简单的派生类的构造函数

任何派生类都包含基类的成员,简单的派生类只有一个基类,而且只有一级派生(只有直接派生类,没有间接派生类),在派生类的数据成员中不包含基类的对象(即子对象)。

例11.5 简单的派生类的构造函数。

#include <iostream>

#include<string>

using namespace std;

class Student//声明基类Student

 {public:                                      

   Student(int n,string nam,char s)           //基类构造函数

    {num=n;

     name=nam;

     sex=s; }

~Student( ){ }                                 //基类析构函数

   protected:                                     //保护部分

    int num;

    string name;

    char sex ;                                

};

class Student1: public Student      //声明派生类Student1

 {public:                           //派生类的公用部分

   Student1(int n,string nam,char s,int a,string ad):Student(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;

    }

   ~Student1( ){ }                         //派生类析构函数

  private:                               //派生类的私有部分

   int age;                          

   string addr;               

 };

int main( )

 {Student1 stud1(10010,″Wang-li″,′f′,19,″115 Beijing Road,Shanghai″);

  Student1 stud2(10011,″Zhang-fun″,′m′,21,″213 Shanghai Road,Beijing″);

  stud1.show( );                          //输出第一个学生的数据

  stud2.show( );                          //输出第二个学生的数据

  return 0;

}

运行结果为

num:10010

name:Wang-li

sex:f

address: 115 Beijing Road,Shanghai

num:10011

name:Zhang-fun

sex:m

address: 213 Shanghai Road,Beijing

请注意派生类构造函数首行的写法:

Student1(int n,string nam,char s,int a,string ad):Student(n,nam,s)

其一般形式为

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

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

在main函数中,建立对象stud1时指定了5个实参。它们按顺序传递给派生类构造函数Student1的形参。然后,派生类构造函数将前面3个传递给基类构造函数的形参。见图11.10。    

通过Student (n, nam, s)把3个值再传给基类构造函数的形参,见图11.11。

Student(  n,  nam,           s)

                     ↓    ↓           ↓

Student(int    n,  string nam,char s)//这是基类构造函数的首部

图11.11

在上例中也可以将派生类构造函数在类外面定义,而在类体中只写该函数的声明:

Student1(int n,string nam,char s,int a,string ad);

在类的外面定义派生类构造函数:

Student1∷Student1(int n,string nam,char s,int a,string ad):Student(n,nam,s)

  {age=a;

     addr=ad;

}

请注意: 在类中对派生类构造函数作声明时,不包括基类构造函数名及其参数表列(即Student(n,nam,s))。只在定义函数时才将它列出。

在以上的例子中,调用基类构造函数时的实参是从派生类构造函数的总参数表中得到的,也可以不从派生类构造函数的总参数表中传递过来,而直接使用常量或全局变量。例如,派生类构造函数首行可以写成以下形式:

Student1(string nam,char s,int a,string ad):Student(10010,nam,s)

即基类构造函数3个实参中,有一个是常量10010,另外两个从派生类构造函数的总参数表传递过来。

请回顾一下在第9章介绍过的构造函数初始化表的例子:

Box::Box(int h,int w,int len):height(h),width(w),length(len)

{ }

它也有一个冒号,在冒号后面的是对数据成员的初始化表。实际上,本章介绍的在派生类构造函数中对基类成员初始化,就是在第9章介绍的构造函数初始化表。也就是说,不仅可以利用初始化表对构造函数的数据成员初始化,而且可以利用初始化表调用派生类的基类构造函数,实现对基类数据成员的初始化。也可以在同一个构造函数的定义中同时实现这两种功能。例如,例11.5中派生类的基类构造函数的定义采用了下面的形式:

Student1(int n, string nam,char s,int a, string ad):Student(n,nam,s)

    {age=a;//在函数体中对派生类数据成员初始化

     addr=ad;

    }

可以将对age和addr的初始化也用初始化表处理,将构造函数改写为以下形式:

Student1(int n, string nam,char s,int a, string ad):Student(n,nam,s),age(a),addr(ad){}

这样函数体为空,更显得简单和方便。

在建立一个对象时,执行构造函数的顺序是: ①派生类构造函数先调用基类构造函数;②再执行派生类构造函数本身(即派生类构造函数的函数体)。对上例来说,先初始化num,name,sex,然后再初始化age和addr。

在派生类对象释放时,先执行派生类析构函数~Student1( ),再执行其基类析构函数~Student( )。

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

类的数据成员中还可以包含类对象,如可以在声明一个类时包含这样的数据成员:

Student s1;// Student是已声明的类名,s1是Student类的对象

这时,s1就是类对象中的内嵌对象,称为子对象(subobject),即对象中的对象。

通过例子来说明问题。

例11.6 包含子对象的派生类的构造函数。

为了简化程序以易于阅读,这里设基类Student的数据成员只有两个,即num和name。

#include <iostream>

#include <string>

using namespace std;

class Student//声明基类

 {public:                                  //公用部分

Student(int n, string nam )             //基类构造函数,与例11.5相同

    {num=n;

     name=nam;

    }

   void display( )                           //成员函数,输出基类数据成员

    {cout<<″num:″<<num<<endl<<″name:″<<name<<endl;}

protected:                                //保护部分

int num;

    string name;

};

class Student1: public Student              //声明公用派生类Student1

{public:

   Student1(int n, string nam,int n1, string nam1,int a, string ad)

      :Student(n,nam),monitor(n1,nam1)                 //派生类构造函数

    {age=a;                        

     addr=ad;

    }

   void show( )

    {cout<<″This student is:″<<endl;

     display();                               //输出num和name

     cout<<″age: ″<<age<<endl;                //输出age

     cout<<″address: ″<<addr<<endl<<endl;     //输出addr

    }

       void show_monitor( )                        //成员函数,输出子对象

    {cout<<endl<<″Class monitor is:″<<endl;

     monitor.display( );                       //调用基类成员函数

  }

   private:                                   //派生类的私有数据

    Student monitor;                          //定义子对象(班长)

    int age;

    string addr;

  };

int main( )

{Student1 stud1(10010,″Wang-li″,10001,″Li-sun″,19,″115 Beijing Road,Shanghai″);

   stud1.show( );                       //输出学生的数据

   stud1.show_monitor();                //输出子对象的数据

return 0;

}

运行时的输出如下:

This student is:

num: 10010

name: Wang-li

age: 19

address:115 Beijing Road,Shanghai

Class monitor is:

num:10001

name:Li-sun

派生类构造函数的任务应该包括3个部分:

(1) 对基类数据成员初始化;

(2) 对子对象数据成员初始化;

(3) 对派生类数据成员初始化。

程序中派生类构造函数首部如下:

Student1(int n, string nam,int n1, string nam1,int a, string ad):

Student(n,nam),monitor(n1,nam1)

在上面的构造函数中有6个形参,前两个作为基类构造函数的参数,第3、第4个作为子对象构造函数的参数,第5、第6个是用作派生类数据成员初始化的。见图11.12。

归纳起来,定义派生类构造函数的一般形式为

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

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

执行派生类构造函数的顺序是:

① 调用基类构造函数,对基类数据成员初始化;

② 调用子对象构造函数,对子对象数据成员初始化;

③ 再执行派生类构造函数本身,对派生类数据成员初始化。

派生类构造函数的总参数表列中的参数,应当包括基类构造函数和子对象的参数表列中的参数。基类构造函数和子对象的次序可以是任意的,如上面的派生类构造函数首部可以写成

Student1(int n, string nam,int n1, string nam1,int a, string ad):

monitor(n1,nam1),Student(n,nam)

编译系统是根据相同的参数名(而不是根据参数的顺序)来确立它们的传递关系的。但是习惯上一般先写基类构造函数。

如果有多个子对象,派生类构造函数的写法依此类推,应列出每一个子对象名及其参数表列。

11.5.3 多层派生时的构造函数

一个类不仅可以派生出一个派生类,派生类还可以继续派生,形成派生的层次结构。在上面叙述的基础上,不难写出在多级派生情况下派生类的构造函数。

例11.7 多级派生情况下派生类的构造函数。

#include <iostream>

#include<string>

using namespace std;

class Student//声明基类

 {public:                                  //公用部分

   Student(int n, string nam )            //基类构造函数

    {num=n;

     name=nam;

    }

   void display( )                           //输出基类数据成员

    {cout<<″num:″<<num<<endl;

     cout<<″name:″<<name<<endl;

    }

  protected:                                //保护部分

    int num;                                //基类有两个数据成员

    string name;

};

class Student1: public Student               //声明公用派生类Student1

 {public:

   Student1(int n,char nam[10],int a):Student(n,nam)//派生类构造函数

    {age=a; }                         //在此处只对派生类新增的数据成员初始化

   void show( )                               //输出num,name和age

    {display( );                               //输出num和name

     cout<<″age: ″<<age<<endl;

    }

   private:                                   //派生类的私有数据

    int age;                                  //增加一个数据成员

  };

class Student2:public Student1               //声明间接公用派生类Student2

 {public:

   //下面是间接派生类构造函数

   Student2(int n, string nam,int a,int s):Student1(n,nam,a)

{score=s;}

   void show_all( )                              //输出全部数据成员

{show( );                                    //输出num和name

     cout<<″score:″<<score<<endl;               //输出age

    }

  private:

   int score;                                   //增加一个数据成员

 };

int main( )

 {Student2 stud(10010,″Li″,17,89);

  stud.show_all( );                            //输出学生的全部数据

return 0;

 }

运行时的输出如下:

num:10010

name:Li

age:17

score:89 

请注意基类和两个派生类的构造函数的写法:

基类的构造函数首部:

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构造函数;在执行Student2构造函数时,先调用Student1构造函数;在执行Student1构造函数时,先调用基类Student构造函数。初始化的顺序是:

① 先初始化基类的数据成员num和name。

②     再初始化Student1的数据成员age。

③     最后再初始化Student2的数据成员score。

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

在使用派生类构造函数时,有以下特殊的形式:

(1) 当不需要对派生类新增的成员进行任何初始化操作时,派生类构造函数的函数体可以为空,即构造函数是空函数,如例11.6程序中派生类Student1构造函数可以改写为

Student1(int n, strin nam,int n1, strin nam1):Student(n,nam),
monitor(n1,nam1) { }

此派生类构造函数的作用只是为了将参数传递给基类构造函数和子对象,并在执行派生类构造函数时调用基类构造函数和子对象构造函数。在实际工作中常见这种用法。

(2) 如果在基类中没有定义构造函数,或定义了没有参数的构造函数,那么在定义派生类构造函数时可不写基类构造函数。因为此时派生类构造函数没有向基类构造函数传递参数的任务。调用派生类构造函数时系统会自动首先调用基类的默认构造函数。

如果在基类和子对象类型的声明中都没有定义带参数的构造函数,而且也不需对派生类自己的数据成员初始化,则可以不必显式地定义派生类构造函数。因为此时派生类构造函数既没有向基类构造函数和子对象构造函数传递参数的任务,也没有对派生类数据成员初始化的任务。在建立派生类对象时,系统会自动调用系统提供的派生类的默认构造函数,并在执行派生类默认构造函数的过程中,调用基类的默认构造函数和子对象类型默认构造函数。

如果在基类或子对象类型的声明中定义了带参数的构造函数,那么就必须显式地定义派生类构造函数,并在派生类构造函数中写出基类或子对象类型的构造函数及其参数表。

如果在基类中既定义无参的构造函数,又定义了有参的构造函数(构造函数重载),则在定义派生类构造函数时,既可以包含基类构造函数及其参数,也可以不包含基类构造函数。在调用派生类构造函数时,根据构造函数的内容决定调用基类的有参的构造函数还是无参的构造函数。编程者可以根据派生类的需要决定采用哪一种方式。

11.5.5 派生类的析构函数

在派生时,派生类是不能继承基类的析构函数的,也需要通过派生类的析构函数去调用基类的析构函数。在派生类中可以根据需要定义自己的析构函数,用来对派生类中所增加的成员进行清理工作。基类的清理工作仍然由基类的析构函数负责。在执行派生类的析构函数时,系统会自动调用基类的析构函数和子对象的析构函数,对基类和子对象进行清理。

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

11.6 多重继承

前面讨论的是单继承,即一个类是从一个基类派生而来的。实际上,常常有这样的情况: 一个派生类有两个或多个基类,派生类从两个或多个基类中继承所需的属性。C++为了适应这种情况,允许一个派生类同时继承多个基类。这种行为称为多重继承(multiple inheritance)。

11.6.1 声明多重继承的方法

如果已声明了类A、类B和类C,可以声明多重继承的派生类D:

class D:public A,private B,protected C

{类D新增加的成员}

D是多重继承的派生类,它以公用继承方式继承A类,以私有继承方式继承B类,以保护继承方式继承C类。D按不同的继承方式的规则继承A,B,C的属性,确定各基类的成员在派生类中的访问权限。

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

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

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

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

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

例11.8 声明一个教师(Teacher)类和一个学生(Student)类,用多重继承的方式声明一个研究生(Graduate)派生类。教师类中包括数据成员name(姓名)、age(年龄)、title(职称)。学生类中包括数据成员name1(姓名)、age(性别)、score(成绩)。在定义派生类对象时给出初始化的数据,然后输出这些数据。

#include <iostream>

#include <string>

using namespace std;

class Teacher//声明类Teacher(教师)

 {public:                                  //公用部分

   Teacher(string nam,int a, string t)      //构造函数

    {name=nam;

     age=a;

     title=t;}

   void display( )                          //输出教师有关数据

     {cout<<″name:″<<name<<endl;

      cout<<″age″<<age<<endl;

      cout<<″title:″<<title<<endl;

     }

  protected:                               //保护部分

    string name;

    int age;

    string title;                          //职称

};

class Student                                //定义类Student(学生)

 {public:

   Student(char nam[],char s,float sco)

     {strcpy(name1,nam);

      sex=s;

      score=sco;}                          //构造函数

   void display1( )                        //输出学生有关数据

    {cout<<″name:″<<name1<<endl;

     cout<<″sex:″<<sex<<endl;

     cout<<″score:″<<score<<endl;

    }

  protected:                               //保护部分

   string name1;

   char sex;

   float score;                            //成绩

 };

class Graduate:public Teacher,public Student   //声明多重继承的派生类Graduate

 {public:

   Graduate(string nam,int a,char s, string t,float sco,float w):

        Teacher(nam,a,t),Student(nam,s,sco),wage(w) { }

   void show( )                                      //输出研究生的有关数据

    {cout<<″name:″<<name<<endl;

     cout<<″age:″<<age<<endl;

     cout<<″sex:″<<sex<<endl;

     cout<<″score:″<<score<<endl;

     cout<<″title:″<<title<<endl;

     cout<<″wages:″<<wage<<endl;

     }

  private:

    float wage;                     //工资

 };

int main( )

 {Graduate grad1(″Wang-li″,24,′f′,″assistant″,89.5,1234.5);

  grad1.show( );

  return 0;

}

程序运行结果如下:

name: Wang-li

age: 24

sex:f

score: 89.5

title: assistance

wages: 1234.5   

在两个基类中分别用name和name1来代表姓名,其实这是同一个人的名字,从Graduate类的构造函数中可以看到总参数表中的参数nam分别传递给两个基类的构造函数,作为基类构造函数的实参。

解决这个问题有一个好方法: 在两个基类中可以都使用同一个数据成员名name,而在show函数中引用数据成员时指明其作用域,如

cout<<″name:″<<Teacher::name<<endl;

这就是惟一的,不致引起二义性,能通过编译,正常运行。

通过这个程序还可以发现一个问题: 在多重继承时,从不同的基类中会继承一些重复的数据。如果有多个基类,问题会更突出。在设计派生类时要细致考虑其数据成员,尽量减少数据冗余。

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

多重继承可以反映现实生活中的情况,能够有效地处理一些较复杂的问题,使编写程序具有灵活性,但是多重继承也引起了一些值得注意的问题,它增加了程序的复杂度,使程序的编写和维护变得相对困难,容易出错。其中最常见的问题就是继承的成员同名而产生的二义性(ambiguous)问题。

在上一节中已经初步地接触到这个问题了。现在作进一步的讨论。

如果类A和类B中都有成员函数display和数据成员a,类C是类A和类B的直接派生类。分别讨论下列3种情况:

(1) 两个基类有同名成员

class A

{public:

  int a;

void display( );

};

class B

{public:

  int a;

  void display( );

};

class C :public A,public B

{public :

  int b;

  void show();

};

如果在main函数中定义C类对象c1,并调用数据成员a和成员函数display:

C c1;

c1.a=3;

c1.display();

由于基类A和基类B都有数据成员a和成员函数display,编译系统无法判别要访问的是哪一基类的成员,因此,程序编译出错。可以用基类名来限定:

c1.A::a=3;//引用c1对象中的基类A的数据成员a

c1.A::display();               //调用c1对象中的基类A的成员函数display

如果是在派生类C中通过派生类成员函数show访问基类A的display和a,可以不必写对象名而直接写

A::a=3;//指当前对象

A::display( );

如同上一节最后所介绍的那样。为清楚起见,图11.14应改用图11.15的形式表示。

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

class C :public A,public B

{int a;

  void display();

};

如图11.16所示。即有3个a,3个display函数。

如果在main函数中定义C类对象c1,并调用数据成员a和成员函数display:

C c1;

c1.a=3;

c1.display( );

程序能通过编译,也可正常运行。访问的是派生类C中的成员。规则是: 基类的同名成员在派生类中被屏蔽,成为“不可见”的,或者说,派生类新增加的同名成员覆盖了基类中的同名成员。因此如果在定义派生类对象的模块中通过对象名访问同名的成员,则访问的是派生类的成员。请注意: 不同的成员函数,只有在函数名和参数个数相同、类型相匹配的情况下才发生同名覆盖,如果只有函数名相同而参数不同,不会发生同名覆盖,而属于函数重载。

要在派生类外访问基类A中的成员,应指明作用域A,写成以下形式:

c1.A::a=3;//表示是派生类对象c1中的基类A中的数据成员a

c1.A::display();     //表示是派生类对象c1中的基类A中的成员函数display

(3) 如果类A和类B是从同一个基类派生的,如图11.18所示。

class N

{public:

int a;

void display(){cout<<″A::a=”<<a<<endl;}

};

class A:public N

{public:

int a1;

};

class B:public N

{public:

int a2;

 };

class C :public A,public B

{public :

int a3;

void show( ){cout<<″a3=″<<a3<<endl;}

};

int main( )

{C c1;//定义C类对象c1

}

图11.19和图11.20表示了派生类C中成员的情况。

             

怎样才能访问类A中从基类N继承下来的成员呢?显然不能用

c1.a=3;c1.display( );或c1.N::a=3; c1.N::display();

因为这样依然无法区别是类A中从基类N继承下来的成员,还是类B中从基类N继承下来的成员。应当通过类N的直接派生类名来指出要访问的是类N的哪一个派生类中的基类成员。如

c1.A::a=3; c1.A::display();//要访问的是类N的派生类A中的基类成员

11.6.4 虚基类

1. 虚基类的作用

从上面的介绍可知: 如果一个派生类有多个直接基类,而这些直接基类又有一个共同的基类,则在最终的派生类中会保留该间接共同基类数据成员的多份同名成员。如图11.19和图11.20所示。在引用这些同名的成员时,必须在派生类对象名后增加直接基类名,以避免产生二义性,使其惟一地标识一个成员,如c1.A::display( )。

在一个类中保留间接共同基类的多份同名成员,这种现象是人们不希望出现的。

C++提供虚基类(virtual base class)的方法,使得在继承间接共同基类时只保留一份成员。

现在,将类A声明为虚基类,方法如下:

class A//声明基类A

 {…};

class B :virtual public A           //声明类B是类A的公用派生类,A是B的虚基类

 {…};

class C :virtual public A           //声明类C是类A的公用派生类,A是C的虚基类

 {…};

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

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

经过这样的声明后,当基类通过多条派生路径被一个派生类继承时,该派生类只继承该基类一次。

在派生类B和C中作了上面的虚基类声明后,派生类D中的成员如图11.23所示。

需要注意: 为了保证虚基类在派生类中只继承一次,应当在该基类的所有直接派生类中声明为虚基类。否则仍然会出现对基类的多次继承。如果像图11.24所示的那样,在派生类B和C中将类A声明为虚基类,而在派生类D中没有将类A声明为虚基类,则在派生类E中,虽然从类B和C路径派生的部分只保留一份基类成员,但从类D路径派生的部分还保留一份基类成员。

D

int data;

int data_b;

int data_c

void fun( );

int data_d;

void fun_d(   );

2. 虚基类的初始化

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

class A//定义基类A

 {A(int i){ }                         //基类构造函数,有一个参数

…};

class B :virtual public A           //A作为B的虚基类

 {B(int n):A(n){ }                   //B类构造函数,在初始化表中对虚基类初始化

…};

class C :virtual public A           //A作为C的虚基类

 {C(int n):A(n){ }                   //C类构造函数,在初始化表中对虚基类初始化

…};

class D :public B,public C     //类D的构造函数,在初始化表中对所有基类初始化

 {D(int n):A(n),B(n),C(n){ }

…};

注意: 在定义类D的构造函数时,与以往使用的方法有所不同。规定: 在最后的派生类中不仅要负责对其直接基类进行初始化,还要负责对虚基类初始化。

C++编译系统只执行最后的派生类对虚基类的构造函数的调用,而忽略虚基类的其他派生类(如类B和类C) 对虚基类的构造函数的调用,这就保证了虚基类的数据成员不会被多次初始化。

3. 虚基类的简单应用举例

例11.9 在例11.8的基础上,在Teacher类和Student类之上增加一个共同的基类Person,如图11.25所示。作为人员的一些基本数据都放在Person中,在Teacher类和Student类中再增加一些必要的数据。

#include <iostream>

#include <string>

using namespace std;

//声明公共基类Person

class Person

{public:

Person(string nam,char s,int a)//构造函数

{name=nam;sex=s;age=a;}

protected:                                        //保护成员

   string name;

   char sex;

   int age;

};

//声明Person的直接派生类Teacher

class Teacher:virtual public Person                //声明Person为公用继承的虚基类

{public:                                

   Teacher(string nam,char s,int a, string t):Person(nam,s,a)//构造函数

    {title=t;

    }

protected:                                       //保护成员

string title;                                  //职称

};

//声明Person的直接派生类Student

class Student:virtual public Person               //声明Person为公用继承的虚基类

{public:

Student(string nam,char s,int a,float sco)      //构造函数

      :Person(nam,s,a),score(sco){ }                //初始化表

protected:                                       //保护成员

    float score;                                   //成绩

 };

//声明多重继承的派生类Graduate

class Graduate:public Teacher,public Student   //Teacher和Student为直接基类

{public:

Graduate(string nam,char s,int a, string t,float sco,float w)//构造函数

:Person(nam,s,a),Teacher(nam,s,a,t),Student(nam,s,a,sco),wage(w){}

//初始化表

    void show( )                                             //输出研究生的有关数据

    {cout<<″name:″<<name<<endl;

     cout<<″age:″<<age<<endl;

     cout<<″sex:″<<sex<<endl;

     cout<<″score:″<<score<<endl;

     cout<<″title:″<<title<<endl;

     cout<<″wages:″<<wage<<endl;

     }

private:

    float wage;                     //工资

 };

//主函数

int main( )

{Graduate grad1(″Wang-li″,′f′,24,″assistant″,89.5,1234.5);

grad1.show( );

return 0;

}

运行结果为

name: Wang-li

age:24

sex:f

score:89.5

title:assistant

wages:1234.5

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

11.7 基类与派生类的转换

只有公用派生类才是基类真正的子类型,它完整地继承了基类的功能。

基类与派生类对象之间有赋值兼容关系,由于派生类中包含从基类继承的成员,因此可以将派生类的值赋给基类对象,在用到基类对象的时候可以用其子类对象代替。具体表现在以下几个方面:

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

可以用子类(即公用派生类)对象对其基类对象赋值。如

A a1;              //定义基类A对象a1

B b1;                //定义类A的公用派生类B的对象b1

a1=b1;               //用派生类B对象b1对基类对象a1赋值

在赋值时舍弃派生类自己的成员。如图11.26示意。实际上,所谓赋值只是对数据成员赋值,对成员函数不存在赋值问题。

请注意: 赋值后不能企图通过对象a1去访问派生类对象b1的成员,因为b1的成员与a1的成员是不同的。假设age是派生类B中增加的公用数据成员,分析下面的用法:

a1.age=23;//错误,a1中不包含派生类中增加的成员

b1.age=21;                 //正确,b1中包含派生类中增加的成员

应当注意,子类型关系是单向的、不可逆的。B是A的子类型,不能说A是B的子类型。只能用子类对象对其基类对象赋值,而不能用基类对象对其子类对象赋值,理由是显然的,因为基类对象不包含派生类的成员,无法对派生类的成员赋值。同理,同一基类的不同派生类对象之间也不能赋值。

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

如已定义了基类A对象a1,可以定义a1的引用变量:

A a1;           //定义基类A对象a1

B b1;                //定义公用派生类B对象b1

A& r=a1;             //定义基类A对象的引用变量r,并用a1对其初始化

这时,引用变量r是a1的别名,r和a1共享同一段存储单元。也可以用子类对象初始化引用变量r,将上面最后一行改为

A& r=b1;//定义基类A对象的引用变量r,并用派生类B对象b1

//对其初始化

或者保留上面第3行“A& r=a1;”,而对r重新赋值:

r=b1;//用派生类B对象b1对a1的引用变量r赋值

注意: 此时r并不是b1的别名,也不与b1共享同一段存储单元。它只是b1中基类部分的别名,r与b1中基类部分共享同一段存储单元,r与b1具有相同的起始地址。见图11.27。

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

如有一函数fun:

void fun(A& r)//形参是类A的对象的引用变量

 {cout<<r.num<<endl;}            //输出该引用变量的数据成员num

函数的形参是类A的对象的引用变量,本来实参应该为A类的对象。由于子类对象与派生类对象赋值兼容,派生类对象能自动转换类型,在调用fun函数时可以用派生类B的对象b1作实参:

fun(b1);

输出类B的对象b1的基类数据成员num的值。

与前相同,在fun函数中只能输出派生类中基类成员的值。

(4) 派生类对象的地址可以赋给指向基类对象的指针变量,也就是说,指向基类对象的指针变量也可以指向派生类对象。

例11.10 定义一个基类Student(学生),再定义Student类的公用派生类Graduate(研究生),用指向基类对象的指针输出数据。

本例主要是说明用指向基类对象的指针指向派生类对象,为了减少程序长度,在每个类中只设很少成员。学生类只设num(学号),name(名字)和score(成绩)3个数据成员,Graduate类只增加一个数据成员pay(工资)。程序如下:

#include <iostream>

#include <string>

using namespace std;

class Student//声明Student类

{public:

   Student(int, string,float);                       //声明构造函数

   void display( );                                   //声明输出函数

private:

   int num;

   string name;

   float score;

};

Student::Student(int n, string nam,float s)          //定义构造函数

{num=n;

  name=nam;

  score=s;

}

void Student::display( )                             //定义输出函数

{cout<<endl<<″num:″<<num<<endl;

  cout<<″name:″<<name<<endl;

  cout<<″score:″<<score<<endl;

}

class Graduate:public Student                    //声明公用派生类Graduate

{public:

  Graduate(int, string ,float,float);             //声明构造函数

  void display( );                                 //声明输出函数

private:

  float pay;                                      //工资

};

Graduate::Graduate(int n, string nam,float s,float p):Student(n,nam,s),pay(p){ }

                                         //定义构造函数

void Graduate::display()                             //定义输出函数

{Student::display();                          //调用Student类的display函数

  cout<<″pay=″<<pay<<endl;

}

int main()

 {Student stud1(1001,″Li″,87.5);                 //定义Student类对象stud1

  Graduate grad1(2001,″Wang″,98.5,563.5);        //定义Graduate类对象grad1

  Student *pt=&stud1;//定义指向Student类对象的指针并指向stud1

  pt->display( );                 //调用stud1.display函数

  pt=&grad1;                     //指针指向grad1

  pt->display( );                 //调用grad1.display函数

 }

很多读者会认为: 在派生类中有两个同名的display成员函数,根据同名覆盖的规则,被调用的应当是派生类Graduate对象的display函数,在执行Graduate::display函数过程中调用Student::display函数,输出num,name,score,然后再输出pay的值。事实上这种推论是错误的,先看看程序的输出结果:

num:1001

name:Li

score:87.5

num:2001

name:wang

score:98.5

并没有输出pay的值。问题在于pt是指向Student类对象的指针变量,即使让它指向了grad1,但实际上pt指向的是grad1中从基类继承的部分。通过指向基类对象的指针,只能访问派生类中的基类成员,而不能访问派生类增加的成员。所以pt->display()调用的不是派生类Graduate对象所增加的display函数,而是基类的display函数,所以只输出研究生grad1的num,name,score3个数据。如果想通过指针输出研究生grad1的pay,可以另设一个指向派生类对象的指针变量ptr,使它指向grad1,然后用ptr->display()调用派生类对象的display函数。但这不大方便。

通过本例可以看到: 用指向基类对象的指针变量指向子类对象是合法的、安全的,不会出现编译上的错误。但在应用上却不能完全满足人们的希望,人们有时希望通过使用基类指针能够调用基类和子类对象的成员。在下一章就要解决这个问题。办法是使用虚函数和多态性。

11.8 继承与组合

在本章11.5.2节中已经说明: 在一个类中可以用类对象作为数据成员,即子对象。在11.5.2节中的例11.6中,对象成员的类型是基类。实际上,对象成员的类型可以是本派生类的基类,也可以是另外一个已定义的类。在一个类中以另一个类的对象作为数据成员的,称为类的组合(composition)。

例如,声明Professor(教授)类是Teacher(教师)类的派生类,另有一个类BirthDate(生日),包含year,month,day等数据成员。可以将教授生日的信息加入到Professor类的声明中。如

class Teacher//教师类

{public:

private:

   int num;

   string name;

   char sex;

  };

class BirthDate            //生日类

{ public:

private:

int year;

int month;

int day;

};

class Professor:public Teacher        //教授类

{public:

  private:

  

   BirthDate birthday;              //BirthDate类的对象作为数据成员

 };

类的组合和继承一样,是软件重用的重要方式。组合和继承都是有效地利用已有类的资源。但二者的概念和用法不同。

Professor类通过继承,从Teacher类得到了num,name,age,sex等数据成员,通过组合,从BirthDate类得到了year,month,day等数据成员。继承是纵向的,组合是横向的。

如果定义了Professor对象prof1,显然prof1包含了生日的信息。通过这种方法有效地组织和利用现有的类,大大减少了工作量。

如果有

void fun1(Teacher &);

void fun2(BirthDate &);

在main函数中调用这两个函数:

fun1(prof1);//正确,形参为Teacher类对象的引用,实参为Teacher类的子类对象,与之赋值兼容

fun2(prof1.birthday);//正确,实参与形参类型相同,都是BirthDate类对象

fun2(prof1);//错误,形参要求是BirthDate类对象,而prof1是Professor类型,不匹配

对象成员的初始化的方法已在11.5.2节中作过介绍。如果修改了成员类的部分内容,只要成员类的公用接口(如头文件名)不变,如无必要,组合类可以不修改。但组合类需要重新编译。

11.9 继承在软件开发中的重要意义

有了继承,使软件的重用成为可能。继承是C++和C的最重要的区别之一。

由于C++提供了继承的机制,这就吸引了许多厂商开发各类实用的类库。用户将它们作为基类去建立适合于自己的类(即派生类),并在此基础上设计自己的应用程序。类库的出现使得软件的重用更加方便,现在有一些类库是随着C++编译系统卖给用户的。读者不要认为类库是C++编译系统的一部分。不同的C++编译系统提供的由不同厂商开发的类库一般是不同的。

对类库中类的声明一般放在头文件中,类的实现(函数的定义部分)是单独编译的,以目标代码形式存放在系统某一目录下。用户使用类库时,不需要了解源代码,但必须知道头文件的使用方法和怎样去连接这些目标代码(在哪个子目录下),以便源程序在编译后与之连接。

由于基类是单独编译的,在程序编译时只需对派生类新增的功能进行编译,这就大大提高了调试程序的效率。如果在必要时修改了基类,只要基类的公用接口不变,派生类不必修改,但基类需要重新编译,派生类也必须重新编译,否则不起作用。

人们为什么这么看重继承,要求在软件开发中使用继承机制,尽可能地通过继承建立一批新的类?为什么不是将已有的类加以修改,使之满足自己应用的要求呢?

(1) 有许多基类是被程序的其他部分或其他程序使用的,这些程序要求保留原有的基类不受破坏。

(2) 用户往往得不到基类的源代码。

(3) 在类库中,一个基类可能已被指定与用户所需的多种组件建立了某种关系,因此在类库中的基类是不容许修改的。

(4) 实际上,许多基类并不是从已有的其他程序中选取来的,而是专门作为基类设计的。

(5) 在面向对象程序设计中,需要设计类的层次结构,从最初的抽象类出发,每一层派生类的建立都逐步地向着目标的具体实现前进。

12 多态性与虚函数

12.1 多态性的概念

多态性(polymorphism)是面向对象程序设计的一个重要特征。利用多态性可以设计和实现一个易于扩展的系统。

在C++程序设计中,多态性是指具有不同功能的函数可以用同一个函数名,这样就可以用一个函数名调用不同内容的函数。在面向对象方法中一般是这样表述多态性的: 向不同的对象发送同一个消息,不同的对象在接收时会产生不同的行为(即方法)。也就是说,每个对象可以用自己的方式去响应共同的消息。

在C++程序设计中,在不同的类中定义了其响应消息的方法,那么使用这些类时,不必考虑它们是什么类型,只要发布消息即可。

从系统实现的角度看,多态性分为两类: 静态多态性和动态多态性。以前学过的函数重载和运算符重载实现的多态性属于静态多态性,在程序编译时系统就能决定调用的是哪个函数,因此静态多态性又称编译时的多态性。静态多态性是通过函数的重载实现的(运算符重载实质上也是函数重载)。动态多态性是在程序运行过程中才动态地确定操作所针对的对象。它又称运行时的多态性。动态多态性是通过虚函数(virtual function)实现的。

有关静态多态性的应用已经介绍过了,在本章中主要介绍动态多态性和虚函数。

要研究的问题是: 当一个基类被继承为不同的派生类时,各派生类可以使用与基类成员相同的成员名,如果在运行时用同一个成员名调用类对象的成员,会调用哪个对象的成员?也就是说,通过继承而产生了相关的不同的派生类,与基类成员同名的成员在不同的派生类中有不同的含义。也可以说,多态性是“一个接口,多种方法”。

12.2 一个典型的例子

下面是一个承上启下的例子。一方面它是有关继承和运算符重载内容的综合应用的例子,通过这个例子可以进一步融会贯通前面所学的内容,另一方面又是作为讨论多态性的一个基础用例。

例12.1 先建立一个Point(点)类,包含数据成员x,y(坐标点)。以它为基类,派生出一个Circle(圆)类,增加数据成员r(半径),再以Circle类为直接基类,派生出一个Cylinder(圆柱体)类,再增加数据成员h(高)。要求编写程序,重载运算符“<<”和“>>”,使之能用于输出以上类对象。

对于一个比较大的程序,应当分成若干步骤进行。先声明基类,再声明派生类,逐级进行,分步调试。

(1) 声明基类Point类

可写出声明基类Point的部分如下:

#include <iostream>

//声明类Point

class Point

{public:

  Point(float x=0,float y=0);//有默认参数的构造函数

  void setPoint(float,float);                 //设置坐标值

  float getX( ) const {return x;}              //读x坐标

  float getY( ) const {return y;}              //读y坐标

  friend ostream & operator<<(ostream &,const Point &);//重载运算符“<<”

protected:                                   //受保护成员

  float x,y;

};

//下面定义Point类的成员函数

//Point的构造函数

Point::Point(float a,float b)                 //对x,y初始化

{x=a;y=b;}

//设置x和y的坐标值

void Point::setPoint(float a,float b)         //为x,y赋新值

{x=a;y=b;}

//重载运算符“<<”,使之能输出点的坐标

ostream & operator<<(ostream &output,const Point &p)

{output<<″[″<<p.x<<″,″<<p.y<<″]″<<endl;

 return output;

}

以上完成了基类Point类的声明。

现在要对上面写的基类声明进行调试,检查它是否有错,为此要写出main函数。实际上它是一个测试程序。

int main( )

{Point p(3.5,6.4);//建立Point类对象p

 cout<<″x=″<<p.getX( )<<″,y=″<<p.getY( )<<endl;//输出p的坐标值

 p.setPoint(8.5,6.8);                            //重新设置p的坐标值

 cout<<″p(new):″<<p<<endl;                       //用重载运算符“<<”输出p点坐标

}

程序编译通过,运行结果为

x=3.5,y=6.4

p(new):[8.5,6.8]

测试程序检查了基类中各函数的功能,以及运算符重载的作用,证明程序是正确的。

(2) 声明派生类Circle

在上面的基础上,再写出声明派生类Circle的部分:

class Circle:public Point//circle是Point类的公用派生类

{public:

  Circle(float x=0,float y=0,float r=0); //构造函数

  void setRadius(float);                 //设置半径值

  float getRadius( ) const;               //读取半径值

  float area ( ) const;                   //计算圆面积

  friend ostream &operator<<(ostream &,const Circle &);//重载运算符“<<”

 private:

  float radius;

};

//定义构造函数,对圆心坐标和半径初始化

Circle::Circle(float a,float b,float r):Point(a,b),radius(r){ }

//设置半径值

void Circle::setRadius(float r)

{radius=r;}

//读取半径值

float Circle::getRadius( ) const {return radius;}

//计算圆面积

float Circle::area( ) const

{return 3.14159*radius*radius;}

//重载运算符“<<”,使之按规定的形式输出圆的信息

ostream &operator<<(ostream &output,const Circle &c)

{output<<″Center=[″<<c.x<<″,″<<c.y<<″],r=″<<c.radius<<″,area=″<<c.area( )<<endl;

 return output;

}

为了测试以上Circle类的定义,可以写出下面的主函数:

int main( )

{Circle c(3.5,6.4,5.2);//建立Circle类对象c,并给定圆心坐标和半径

 cout<<″original circle:\\nx=″<<c.getX()<<″, y=″<<c.getY()<<″, r=″<<c.getRadius( )

     <<″, area=″<<c.area( )<<endl;     //输出圆心坐标、半径和面积

 c.setRadius(7.5);                   //设置半径值

 c.setPoint(5,5);                    //设置圆心坐标值x,y

 cout<<″new circle:\\n″<<c;           //用重载运算符“<<”输出圆对象的信息

 Point &pRef=c;                      //pRef是Point类的引用变量,被c初始化

 cout<<″pRef:″<<pRef;                //输出pRef的信息

 return 0;

}程序编译通过,运行结果为original circle:(输出原来的圆的数据)

x=3.5, y=6.4, r=5.2, area=84.9486

new circle:                                (输出修改后的圆的数据)

Center=[5,5], r=7.5, area=176.714

pRef:[5,5]                                 (输出圆的圆心“点”的数据)

(3) 声明Circle的派生类Cylinder

前面已从基类Point派生出Circle类,现在再从Circle派生出Cylinder类。

class Cylinder:public Circle// Cylinder是Circle的公用派生类

{public:

  Cylinder (float x=0,float y=0,float r=0,float h=0);//构造函数

  void setHeight(float);                      //设置圆柱高

  float getHeight( ) const;                    //读取圆柱高

  float area( ) const;                         //计算圆表面积

  float volume( ) const;                       //计算圆柱体积

  friend ostream& operator<<(ostream&,const Cylinder&);//重载运算符“<<”

 protected:

  float height;                               //圆柱高

};

//定义构造函数

Cylinder::Cylinder(float a,float b,float r,float h) 

    :Circle(a,b,r),height(h){}

//设置圆柱高

void Cylinder::setHeight(float h){height=h;}

//读取圆柱高

float Cylinder::getHeight( ) const {return height;}

//计算圆表面积

float Cylinder::area( ) const

{ return 2*Circle::area( )+2*3.14159*radius*height;}

//计算圆柱体积

float Cylinder::volume() const

{return Circle::area()*height;}

//重载运算符“<<”

ostream &operator<<(ostream &output,const Cylinder& cy)

{output<<″Center=[″<<cy.x<<″,″<<cy.y<<″],r=″<<cy.radius<<″,h=″<<cy.height

       <<″\\narea=″<<cy.area( )<<″, volume=″<<cy.volume( )<<endl;

 return output;

}

可以写出下面的主函数:

int main( )

{Cylinder cy1(3.5,6.4,5.2,10);//定义Cylinder类对象cy1

 cout<<″\\noriginal cylinder:\\nx=″<<cy1.getX( )<<″, y=″<<cy1.getY( )<<″, r=″

     <<cy1.getRadius( )<<″, h=″<<cy1.getHeight( )<<″\\narea=″<<cy1.area()

     <<″,volume=″<<cy1.volume()<<endl;//用系统定义的运算符“<<”输出cy1的数据

 cy1.setHeight(15);                     //设置圆柱高

 cy1.setRadius(7.5);                    //设置圆半径

 cy1.setPoint(5,5);                     //设置圆心坐标值x,y

 cout<<″\\nnew cylinder:\\n″<<cy1;        //用重载运算符“<<”输出cy1的数据

 Point &pRef=cy1;                       //pRef是Point类对象的引用变量

cout<<″\\npRef as a Point:″<<pRef;      //pRef作为一个“点”输出

 Circle &cRef=cy1;                      //cRef是Circle类对象的引用变量

 cout<<″\\ncRef as a Circle:″<<cRef;     //cRef作为一个“圆”输出

 return 0;

}

运行结果如下:

original cylinder:                         (输出cy1的初始值)

x=3.5, y=6.4, r=5.2, h=10                  (圆心坐标x,y。半径r,高h)

area=496.623, volume=849.486               (圆柱表面积area和体积volume)

new cylinder:                              (输出cy1的新值)

Center=[5,5], r=7.5, h=15                 (以[5,5]形式输出圆心坐标)

area=1060.29, volume=2650.72               (圆柱表面积area和体积volume)

pRef as a Point:[5,5]                      (pRef作为一个“点”输出)

cRef as a Circle: Center=[5,5], r=7.5, area=176.714(cRef作为一个“圆”输出)

在本例中存在静态多态性,这是运算符重载引起的。可以看到,在编译时编译系统即可以判定应调用哪个重载运算符函数。稍后将在此基础上讨论动态多态性问题。

12.3 虚函数

12.3.1 虚函数的作用

在类的继承层次结构中,在不同的层次中可以出现名字相同、参数个数和类型都相同而功能不同的函数。编译系统按照同名覆盖的原则决定调用的对象。在例12.1程序中用cy1.area( )调用的是派生类Cylinder中的成员函数area。如果想调用cy1中的直接基类Circle的area函数,应当表示为: cy1.Circle::area( )。用这种方法来区分两个同名的函数。但是这样做很不方便。

人们提出这样的设想,能否用同一个调用形式,既能调用派生类又能调用基类的同名函数。在程序中不是通过不同的对象名去调用不同派生层次中的同名函数,而是通过指针调用它们。例如,用同一个语句“pt->display( );”可以调用不同派生层次中的display函数,只需在调用前给指针变量pt赋以不同的值(使之指向不同的类对象)即可。

C++中的虚函数就是用来解决这个问题的。虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。

请分析例12.2。这个例子开始时没有使用虚函数,然后再讨论使用虚函数的情况。

例12.2 基类与派生类中有同名函数。

在下面的程序中Student是基类,Graduate是派生类,它们都有display这个同名的函数。

#include <iostream>

#include <string>

using namespace std;

//声明基类Student

class Student

{public:

   Student(int, string,float);//声明构造函数

   void display( );                                             //声明输出函数

  protected:                                      //受保护成员,派生类可以访问

   int num;

   string name;

   float score;

 };

//Student类成员函数的实现

Student::Student(int n, string nam,float s)                     //定义构造函数

 {num=n;name=nam;score=s;}

void Student::display( )                                        //定义输出函数

{cout<<″num:″<<num<<″\\nname:″<<name<<″\\nscore:″<<score<<″\\n\\n″;}

//声明公用派生类Graduate

class Graduate:public Student

{public:

   Graduate(int, string, float, float);                          //声明构造函数

   void display( );                                             //声明输出函数

private:

  float pay;

};

// Graduate类成员函数的实现

void Graduate::display( )                                       //定义输出函数

 {cout<<″num:″<<num<<″\\nname:″<<name<<″\\nscore:″<<score<<″\\npay=″<<pay<<endl;}

Graduate::Graduate(int n, string nam,float s,float p):Student(n,nam,s),pay(p){ }

//主函数

int main()

 {Student stud1(1001,″Li″,87.5);                   //定义Student类对象stud1

  Graduate grad1(2001,″Wang″,98.5,563.5);          //定义Graduate类对象grad1

  Student *pt=&stud1;                              //定义指向基类对象的指针变量pt

  pt->display( );

  pt=&grad1;

  pt->display( );

  return 0;

 }

运行结果如下,请仔细分析。

num:1001(stud1的数据)

name:Li

score:87.5

num:2001                  (grad1中基类部分的数据)

name:wang

score:98.5

下面对程序作一点修改,在Student类中声明display函数时,在最左面加一个关键字virtual,即

virtual void display( );

这样就把Student类的display函数声明为虚函数。程序其他部分都不改动。再编译和运行程序,请注意分析运行结果:

num:1001(stud1的数据)

name:Li

score:87.5

num:2001                  (grad1中基类部分的数据)

name:wang

score:98.5

pay=1200                   (这一项以前是没有的)

由虚函数实现的动态多态性就是: 同一类族中不同类的对象,对同一函数调用作出不同的响应。虚函数的使用方法是:

(1) 在基类用virtual声明成员函数为虚函数。这样就可以在派生类中重新定义此函数,为它赋予新的功能,并能方便地被调用。

在类外定义虚函数时,不必再加virtual。

(2)   在派生类中重新定义此函数,要求函数名、函数类型、函数参数个数和类型全部与基类的虚函数相同,并根据派生类的需要重新定义函数体。

C++规定,当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数。因此在派生类重新声明该虚函数时,可以加virtual,也可以不加,但习惯上一般在每一层声明该函数时都加virtual,使程序更加清晰。

如果在派生类中没有对基类的虚函数重新定义,则派生类简单地继承其直接基类的虚函数。

(3) 定义一个指向基类对象的指针变量,并使它指向同一类族中需要调用该函数的对象。

(4) 通过该指针变量调用此虚函数,此时调用的就是指针变量指向的对象的同名函数。

通过虚函数与指向基类对象的指针变量的配合使用,就能方便地调用同一类族中不同类的同名函数,只要先用基类指针指向即可。如果指针不断地指向同一类族中不同类的对象,就能不断地调用这些对象中的同名函数。这就如同前面说的,不断地告诉出租车司机要去的目的地,然后司机把你送到你要去的地方。

需要说明;有时在基类中定义的非虚函数会在派生类中被重新定义(如例12.1中的area函数),如果用基类指针调用该成员函数,则系统会调用对象中基类部分的成员函数;如果用派生类指针调用该成员函数,则系统会调用派生类对象中的成员函数,这并不是多态性行为(使用的是不同类型的指针),没有用到虚函数的功能。

以前介绍的函数重载处理的是同一层次上的同名函数问题,而虚函数处理的是不同派生层次上的同名函数问题,前者是横向重载,后者可以理解为纵向重载。但与重载不同的是: 同一类族的虚函数的首部是相同的,而函数重载时函数的首部是不同的(参数个数或类型不同)。

12.3.2 静态关联与动态关联

编译系统要根据已有的信息,对同名函数的调用作出判断。对于调用同一类族中的虚函数,应当在调用时用一定的方式告诉编译系统,你要调用的是哪个类对象中的函数。这样编译系统在对程序进行编译时,即能确定调用的是哪个类对象中的函数。

确定调用的具体对象的过程称为关联(binding)。在这里是指把一个函数名与一个类对象捆绑在一起,建立关联。一般地说,关联指把一个标识符和一个存储地址联系起来。

前面所提到的函数重载和通过对象名调用的虚函数,在编译时即可确定其调用的虚函数属于哪一个类,其过程称为静态关联(static binding),由于是在运行前进行关联的,故又称为早期关联(early binding)。函数重载属静态关联。

在上一小节程序中看到了怎样使用虚函数,在调用虚函数时并没有指定对象名,那么系统是怎样确定关联的呢?是通过基类指针与虚函数的结合来实现多态性的。先定义了一个指向基类的指针变量,并使它指向相应的类对象,然后通过这个基类指针去调用虚函数(例如“pt->display( )”)。显然,对这样的调用方式,编译系统在编译该行时是无法确定调用哪一个类对象的虚函数的。因为编译只作静态的语法检查,光从语句形式是无法确定调用对象的。

在这样的情况下,编译系统把它放到运行阶段处理,在运行阶段确定关联关系。在运行阶段,基类指针变量先指向了某一个类对象,然后通过此指针变量调用该对象中的函数。此时调用哪一个对象的函数无疑是确定的。例如,先使pt指向grad1,再执行“pt->display( )”,当然是调用grad1中的display函数。由于是在运行阶段把虚函数和类对象“绑定”在一起的,因此,此过程称为动态关联(dynamic binding)。这种多态性是动态的多态性,即运行阶段的多态性。

在运行阶段,指针可以先后指向不同的类对象,从而调用同一类族中不同类的虚函数。由于动态关联是在编译以后的运行阶段进行的,因此也称为滞后关联(late binding)。

12.3.3 在什么情况下应当声明虚函数

使用虚函数时,有两点要注意:

(1)只能用virtual声明类的成员函数,使它成为虚函数,而不能将类外的普通函数声明为虚函数。因为虚函数的作用是允许在派生类中对基类的虚函数重新定义。显然,它只能用于类的继承层次结构中。

(2) 一个成员函数被声明为虚函数后,在同一类族中的类就不能再定义一个非virtual的但与该虚函数具有相同的参数(包括个数和类型)和函数返回值类型的同名函数。

根据什么考虑是否把一个成员函数声明为虚函数呢?主要考虑以下几点:

(1) 首先看成员函数所在的类是否会作为基类。然后看成员函数在类的继承后有无可能被更改功能,如果希望更改其功能的,一般应该将它声明为虚函数。

(2) 如果成员函数在类被继承后功能不需修改,或派生类用不到该函数,则不要把它声明为虚函数。不要仅仅考虑到要作为基类而把类中的所有成员函数都声明为虚函数。

(3) 应考虑对成员函数的调用是通过对象名还是通过基类指针或引用去访问,如果是通过基类指针或引用去访问的,则应当声明为虚函数。

(4) 有时,在定义虚函数时,并不定义其函数体,即函数体是空的。它的作用只是定义了一个虚函数名,具体功能留给派生类去添加。在12.4节中将详细讨论此问题。

需要说明的是: 使用虚函数,系统要有一定的空间开销。当一个类带有虚函数时,编译系统会为该类构造一个虚函数表(virtual function table,简称vtable),它是一个指针数组,存放每个虚函数的入口地址。系统在进行动态关联时的时间开销是很少的,因此,多态性是高效的。

12.3.4 虚析构函数

析构函数的作用是在对象撤销之前做必要的“清理现场”的工作。当派生类的对象从内存中撤销时一般先调用派生类的析构函数,然后再调用基类的析构函数。但是,如果用new运算符建立了临时对象,若基类中有析构函数,并且定义了一个指向该基类的指针变量。在程序用带指针参数的delete运算符撤销对象时,会发生一个情况: 系统会只执行基类的析构函数,而不执行派生类的析构函数。

例12.3 基类中有非虚析构函数时的执行情况。

为简化程序,只列出最必要的部分。

#include <iostream>

using namespace std;

class Point//定义基类Point类

{public:

  Point( ){ }                                                   //Point类构造函数

  ~Point(){cout<<″executing Point destructor″<<endl;}//Point类析构函数

};

class Circle:public Point                                     //定义派生类Circle类

{public:

  Circle( ){ }                                                 //Circle类构造函数

  ~Circle( ){cout<<″executing Circle destructor″<<endl;}//Circle类析构函数

 private:

  int radius;

};

int main( )

{ Point *p=new Circle;                             //用new开辟动态存储空间

delete p;                                        //用delete释放动态存储空间

return 0;

}

这只是一个示意的程序。p是指向基类的指针变量,指向new开辟的动态存储空间,希望用detele释放p所指向的空间。但运行结果为

executing Point destructor

表示只执行了基类Point的析构函数,而没有执行派生类Circle的析构函数。原因是以前介绍过的。如果希望能执行派生类Circle的析构函数,可以将基类的析构函数声明为虚析构函数,如

virtual ~Point(){cout<<″executing Point destructor″<<endl;}

程序其他部分不改动,再运行程序,结果为

executing Circle destructor

executing Point destructor

先调用了派生类的析构函数,再调用了基类的析构函数,符合人们的愿望。当基类的析构函数为虚函数时,无论指针指的是同一类族中的哪一个类对象,系统会采用动态关联,调用相应的析构函数,对该对象进行清理工作。

如果将基类的析构函数声明为虚函数时,由该基类所派生的所有派生类的析构函数也都自动成为虚函数,即使派生类的析构函数与基类的析构函数名字不相同。

最好把基类的析构函数声明为虚函数。这将使所有派生类的析构函数自动成为虚函数。这样,如果程序中显式地用了delete运算符准备删除一个对象,而delete运算符的操作对象用了指向派生类对象的基类指针,则系统会调用相应类的析构函数。

虚析构函数的概念和用法很简单,但它在面向对象程序设计中却是很重要的技巧。专业人员一般都习惯声明虚析构函数,即使基类并不需要析构函数,也显式地定义一个函数体为空的虚析构函数,以保证在撤销动态分配空间时能得到正确的处理。

构造函数不能声明为虚函数。这是因为在执行构造函数时类对象还未完成建立过程,当然谈不上函数与类对象的绑定。

12.4 纯虚函数与抽象类

12.4.1 纯虚函数

有时在基类中将某一成员函数定为虚函数,并不是基类本身的要求,而是考虑到派生类的需要,在基类中预留了一个函数名,具体功能留给派生类根据需要去定义。例如在本章的例12.1程序中,基类Point中没有求面积的area函数,因为“点”是没有面积的,也就是说,基类本身不需要这个函数,所以在例12.1程序中的Point类中没有定义area函数。但是,在其直接派生类Circle和间接派生类Cylinder中都需要有area函数,而且这两个area函数的功能不同,一个是求圆面积,一个是求圆柱体表面积。

有的读者自然会想到,在这种情况下应当将area声明为虚函数。可以在基类Point中加一个area函数,并声明为虚函数:

virtual float area( ) const {return 0;}

其返回值为0,表示“点”是没有面积的。其实,在基类中并不使用这个函数,其返回值也是没有意义的。为简化,可以不写出这种无意义的函数体,只给出函数的原型,并在后面加上“=0”,如

virtual float area( ) const =0;//纯虚函数

这就将area声明为一个纯虚函数(pure virtual function)。纯虚函数是在声明虚函数时被“初始化”为0的函数。声明纯虚函数的一般形式是

virtual 函数类型 函数名 (参数表列) =0;

注意: ①纯虚函数没有函数体;②最后面的“=0”并不表示函数返回值为0,它只起形式上的作用,告诉编译系统“这是纯虚函数”; ③这是一个声明语句,最后应有分号。

纯虚函数只有函数的名字而不具备函数的功能,不能被调用。它只是通知编译系统: “在这里声明一个虚函数,留待派生类中定义”。在派生类中对此函数提供定义后,它才能具备函数的功能,可被调用。

纯虚函数的作用是在基类中为其派生类保留一个函数的名字,以便派生类根据需要对它进行定义。如果在基类中没有保留函数名字,则无法实现多态性。

如果在一个类中声明了纯虚函数,而在其派生类中没有对该函数定义,则该虚函数在派生类中仍然为纯虚函数。

12.4.2 抽象类

如果声明了一个类,一般可以用它定义对象。但是在面向对象程序设计中,往往有一些类,它们不用来生成对象。定义这些类的惟一目的是用它作为基类去建立派生类。它们作为一种基本类型提供给用户,用户在这个基础上根据自己的需要定义出功能各异的派生类。用这些派生类去建立对象。

一个优秀的软件工作者在开发一个大的软件时,决不会从头到尾都由自己编写程序代码,他会充分利用已有资源(例如类库)作为自己工作的基础。

这种不用来定义对象而只作为一种基本类型用作继承的类,称为抽象类(abstract class),由于它常用作基类,通常称为抽象基类(abstract base class)。

凡是包含纯虚函数的类都是抽象类。因为纯虚函数是不能被调用的,包含纯虚函数的类是无法建立对象的。抽象类的作用是作为一个类族的共同基类,或者说,为一个类族提供一个公共接口。

一个类层次结构中当然也可不包含任何抽象类,每一层次的类都是实际可用的,可以用来建立对象的。但是,许多好的面向对象的系统,其层次结构的顶部是一个抽象类,甚至顶部有好几层都是抽象类。

如果在抽象类所派生出的新类中对基类的所有纯虚函数进行了定义,那么这些函数就被赋予了功能,可以被调用。这个派生类就不是抽象类,而是可以用来定义对象的具体类(concrete class)。如果在派生类中没有对所有纯虚函数进行定义,则此派生类仍然是抽象类,不能用来定义对象。

虽然抽象类不能定义对象(或者说抽象类不能实例化),但是可以定义指向抽象类数据的指针变量。当派生类成为具体类之后,就可以用这种指针指向派生类对象,然后通过该指针调用虚函数,实现多态性的操作。

12.4.3 应用实例

例12.4 虚函数和抽象基类的应用。

在本章例12.1介绍了以Point为基类的点—圆—圆柱体类的层次结构。现在要对它进行改写,在程序中使用虚函数和抽象基类。类的层次结构的顶层是抽象基类Shape(形状)。Point(点), Circle(圆), Cylinder(圆柱体)都是Shape类的直接派生类和间接派生类。

下面是一个完整的程序,为了便于阅读,分段插入了一些文字说明。

程序如下:

第(1)部分

#include <iostream>

using namespace std;

//声明抽象基类Shape

class Shape

{public:

 virtual float area( ) const {return 0.0;}//虚函数

 virtual float volume() const {return 0.0;}      //虚函数

 virtual void shapeName() const =0;              //纯虚函数

};

第(2)部分

//声明Point类

class Point:public Shape//Point是Shape的公用派生类

{public:

  Point(float=0,float=0);

  void setPoint(float,float);

  float getX( ) const {return x;}

  float getY( ) const {return y;}

  virtual void shapeName( ) const {cout<<″Point:″;}      //对虚函数进行再定义

  friend ostream & operator<<(ostream &,const Point &);

protected:

  float x,y;

};

//定义Point类成员函数

Point::Point(float a,float b)

{x=a;y=b;}

void Point::setPoint(float a,float b)

{x=a;y=b;}

ostream & operator<<(ostream &output,const Point &p)

{output<<″[″<<p.x<<″,″<<p.y<<″]″;

return output;

}

第(3)部分

//声明Circle类

class Circle:public Point

{public:

  Circle(float x=0,float y=0,float r=0);

  void setRadius(float);

  float getRadius( ) const;

  virtual float area( ) const;

  virtual void shapeName( ) const {cout<<″Circle:″;}//对虚函数进行再定义

  friend ostream &operator<<(ostream &,const Circle &);

protected:

  float radius;

};

//声明Circle类成员函数

Circle::Circle(float a,float b,float r):Point(a,b),radius(r){ }

void Circle::setRadius(float r):radius(r){ }

float Circle::getRadius( ) const {return radius;}

float Circle::area( ) const {return 3.14159*radius*radius;}

ostream &operator<<(ostream &output,const Circle &c)

{output<<″[″<<c.x<<″,″<<c.y<<″], r=″<<c.radius;

 return output;

}

第(4)部分

//声明Cylinder类

class Cylinder:public Circle

{public:

  Cylinder (float x=0,float y=0,float r=0,float h=0);

  void setHeight(float);

  virtual float area( ) const;

  virtual float volume( ) const;

  virtual void shapeName( ) const {cout<<″Cylinder:″;}//对虚函数进行再定义

  friend ostream& operator<<(ostream&,const Cylinder&);

 protected:

  float height;

};

//定义Cylinder类成员函数

Cylinder::Cylinder(float a,float b,float r,float h)

    :Circle(a,b,r),height(h){ }

void Cylinder::setHeight(float h){height=h;}

float Cylinder::area( ) const

{ return 2*Circle::area( )+2*3.14159*radius*height;}

float Cylinder::volume( ) const

{return Circle::area( )*height;}

ostream &operator<<(ostream &output,const Cylinder& cy)

{output<<″[″<<cy.x<<″,″<<cy.y<<″], r=″<<cy.radius<<″, h=″<<cy.height;

return output;

}

第(5)部分

//main函数

int main( )

{Point point(3.2,4.5);//建立Point类对象point

 Circle circle(2.4,1.2,5.6);                     //建立Circle类对象circle

 Cylinder cylinder(3.5,6.4,5.2,10.5);            //建立Cylinder类对象cylinder

 point.shapeName();                              //静态关联

 cout<<point<<endl;

 circle.shapeName();                             //静态关联

 cout<<circle<<endl;

 cylinder.shapeName();                           //静态关联

 cout<<cylinder<<endl<<endl;

 Shape *pt;                                      //定义基类指针

 pt=&point;                                      //指针指向Point类对象

 pt->shapeName( );                                //动态关联

 cout<<″x=″<<point.getX( )<<″,y=″<<point.getY( )<<″\\narea=″<<pt->area( )

     <<″\\nvolume=″<<pt->volume()<<″\\n\\n″;

 pt=&circle;                                     //指针指向Circle类对象

pt->shapeName( );                                //动态关联

 cout<<″x=″<<circle.getX( )<<″,y=″<<circle.getY( )<<″\\narea=″<<pt->area( )

     <<″\\nvolume=″<<pt->volume( )<<″\\n\\n″;

 pt=&cylinder;                                   //指针指向Cylinder类对象

 pt->shapeName( );                                //动态关联

 cout<<″x=″<<cylinder.getX( )<<″,y=″<<cylinder.getY( )<<″\\narea=″<<pt->area( )

     <<″\\nvolume=″<<pt->volume( )<<″\\n\\n″;

 return 0;

}

程序运行结果如下。请读者对照程序分析。

Point:[3.2,4.5](Point类对象point的数据: 点的坐标)

Circle:[2.4,1.2], r=5.6             (Circle类对象circle的数据: 圆心和半径)

Cylinder:[3.5,6.4], r=5.5, h=10.5   (Cylinder类对象cylinder的数据: 圆心、半径和高)

Point:x=3.2,y=4.5                    (输出Point类对象point的数据: 点的坐标)

area=0                                (点的面积)

volume=0                              (点的体积)

Circle:x=2.4,y=1.2                    (输出Circle类对象circle的数据: 圆心坐标)

area=98.5203                           (圆的面积)

volume=0                               (圆的体积)

Cylinder:x=3.5,y=6.4                  (输出Cylinder类对象cylinder的数据: 圆心坐标)

area=512.595                          (圆的面积)

volume=891.96                         (圆柱的体积)

从本例可以进一步明确以下结论:

(1) 一个基类如果包含一个或一个以上纯虚函数,就是抽象基类。抽象基类不能也不必要定义对象。

(2) 抽象基类与普通基类不同,它一般并不是现实存在的对象的抽象(例如圆形(Circle)就是千千万万个实际的圆的抽象),它可以没有任何物理上的或其他实际意义方面的含义。

(3) 在类的层次结构中,顶层或最上面的几层可以是抽象基类。抽象基类体现了本类族中各类的共性,把各类中共有的成员函数集中在抽象基类中声明。

(4) 抽象基类是本类族的公共接口。或者说,从同一基类派生出的多个类有同一接口。

(5) 区别静态关联和动态关联。

(6) 如果在基类声明了虚函数,则在派生类中凡是与该函数有相同的函数名、函数类型、参数个数和类型的函数,均为虚函数(不论在派生类中是否用virtual声明)。

(7) 使用虚函数提高了程序的可扩充性。

把类的声明与类的使用分离。这对于设计类库的软件开发商来说尤为重要。开发商设计了各种各样的类,但不向用户提供源代码,用户可以不知道类是怎样声明的,但是可以使用这些类来派生出自己的类。

利用虚函数和多态性,程序员的注意力集中在处理普遍性,而让执行环境处理特殊性。

多态性把操作的细节留给类的设计者(他们多为专业人员)去完成,而让程序人员(类的使用者)只需要做一些宏观性的工作,告诉系统做什么,而不必考虑怎么做,极大地简化了应用程序的编码工作,大大减轻了程序员的负担,也降低了学习和使用C++编程的难度,使更多的人能更快地进入C++程序设计的大门。

14 C++工具

在C++发展的后期,有时C++编译系统根据实际工作的需要,增加了一些功能,作为工具来使用,其中主要有模板(包括函数模板和类模板)、异常处理、命名空间和运行时类型识别,以帮助程序设计人员更方便地进行程序的设计和调试工作。1997年ANSI C++委员会将它们纳入了ANSI C++标准,建议所有的C++编译系统都能实现这些功能。这些工具是非常有用的,C++的使用者应当尽量使用这些工具。

猜你喜欢

转载自www.cnblogs.com/Aha-Best/p/10912950.html