第十七讲:基类与派生类的转换

来源:互联网 发布:蚂蜂窝app无网络 编辑:程序博客网 时间:2024/06/01 11:58

第十七讲:基类与派生类的转换

本讲基本要求

    * 掌握:基类与派生类的转换; 继承与组合。
    * 理解:继承在软件开发中的重要意义
    重点、难点:继承与组合。

一、 基类与派生类的转换

   3种继承方式(公用、保护、私有继承)中,公用派生类才是基类真正的子类型,它完整地继承了基类的功能。

   不同类型数据之间在一定条件下可以进行类型的转换。基类与派生类对象之间是否也有赋值兼容的关系,可否进行类型间的转换?回答是可以的。基类与派生类对象之间有赋值兼容关系,由于派生类中包含从基类继承的成员,因此可以将派生类的值赋给基类对象,在用到基类对象的时候可以用其子类对象代替

注意:有的数据类型是不可转换的;有的转换是不可逆的。

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

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

如:
   A al; //定义基类A对象al

   B bl;
//定义类A的公用派生类B的对象bl
   a1=b1;
//用派生类B对象bl对基类对象al赋值

实际上,所谓赋值只是对数据成员赋值,对成员函数不存在赋值问题。

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

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

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

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

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

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

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

   A&r=bl;//定义基类A对象的引用变量r,并用派生类B对象b1对其初始化或者保留上面第3行“A&r=al;”,而对r重新赋值:
   
r=bl; //用派生类B对象bl对a1的引用变量r赋值

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

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

如有一函数fun:

   void fun(A&r)          //形参是类A的对象的引用
     
{ cout<<r.num<<endl;}//输出该引用所代表的对象的数据成员num

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

   在fun函数中只能输出派生类中基类成员的值

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

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

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

#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 //声明公用派生类Graduale
   
{ public:
      Graduate(int,string,float,float);//声明构造函数
      void display();     //声明输出函数
     
private:
      float pay; };      //工资
void Graduate::display()   //定义输山函数
  
{ Student::display();    //调用Student类的display函数
    
cout<<"pay="<<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;       //定义指向Student类对象的指针并指向studl
   
pt->display();           //调用studl.display函数
   
pt=&grad1;              //指针指向gradl
   
pt->display();           //调用gradl.display函数
   
return 0; }

 程序的输出结果:
      
num:1001
      name:Li
      score:87.5
      num:2001
      nume:wang
      score:98.5

为什么没有输出pay的值?

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

二、继承与组合

   在一个类中以另一个类的对象作为数据成员的,称为类的组合(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对象profl,显然profl包含了生日的信息。通过这种方法有效地组织和利用现有的类,改变了程序人员“一切都要自己于”的工作方式,大大减少了工作量。

如果有以下两个函数:
   void funl(Teacher &);
   void fun2(BirthDate &);
在main函数中调用这两个函数:
   funl(profl);//正确,形参为Teacher类对象的引用,实参为Teacher类的子类对象,与
//之赋值兼容

   
fun2(profl.birthday);//正确,实参与形参类型相同,都是BirthDate类对象
   
fun2(profl);//错误,形参要求是BirthDate类对象,而profl是Professor类型,不匹配

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

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

   继承是面向对象技术的一个重要内容。有了继承,使软件的重用成为可能。

   1、继承是C++和C的最重要的区别之一。
   2、由于c++提供了继承的机制,这就吸引了许多厂商开发各类实用的类库。
   3、对类库中类的声明一般放在头文件中,类的实现(函数的定义部分)是单独编译的,以目标代码形式存放在系统某一目录下。
   4、由于基类是单独编译的,在程序编译时只需对派生类新增的功能进行编译,这就大大提高了调试程序的效率。如果在必要时修改了基类,只要基类的公用接口不变,派生类不必修改,但基类需要重新编译,派生类也必须重新编译,否则不起作用。

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

   1、有许多基类是同时被程序的其他部分或其他程序使用的,这些程序要求保留原有的基类不受破坏。使用继承是建立新的数据类型,它继承了基类的所有特征,但不改变基类本身。基类的名称、构成和访问属性丝毫没有改变,不会影响其他程序的使用。
   2、用户往往得不到基类的源代码。如果想修改已有的类,必须掌握类的声明和类的实现(成员函数的定义)的源代码。但是,如果使用类库,用户是无法知道成员函数的代码的,因此也就无法刘‘基类进行修改。
   3、在类库中,一个基类可能已被指定与用户所需的多种组件建立了某种关系,因此在类库中的基类是不容许修改的(即使用户知道了源代码,也决不允许修改)。
   4、实际上,许多基类并不是从已有的其他程序中选取来的,而是专门作为基类设计的。有些基类可能并没有什么独立的功能,只是一个框架,或者说是抽象类。人们根据需要没计了一批能适用于不同用途的通用类,目的是建立通用的数据结构,以便用户在此基础上添加各种功能,从而建立各种功能的派生类。
   5、在面向对象程序设计中,需要设计类的层次结构,从最初的抽象类出发,每一层派生类的建立都逐步地向着目标的具体实现前进,换句话说,是不断地从抽象到具体的过程,每一层的派生和继堆都需要站在整个系统的角度统一规划,精心组织。

 

转自 :http://210.44.195.12/cgyy/text/HTML/text/17.htm

 

 

 

 

原创粉丝点击