zoukankan      html  css  js  c++  java
  • 【转】C++的继承与多态:为什么需要虚函数

    转自:http://www.educity.cn/zk/gjyy/201306271108011682.htm

    多态性是面向对象设计语言的基本特征。仅仅是将数据和函数捆绑在一起,进行类的封装,使用一些简单的继承,还不能算是真正应用了面向对象的设计思想。多态性是面向对象的精髓,也是难点。在C++中,多态性是通过虚函数来实现的。

    1. 为什么需要虚函数

      为了说明虚函数的作用,我们先看一个程序实例:

    #include <iostream.h>
    
    class vehicle{
      int wheels;
      float weight;
    public:
      void message(void) {cout << "Vehicle message
    ";}
    };
    
    class car : public vehicle{
      int passenger_load;
    public:
      void message(void) {cout << "Car message
    ";}
    };
    
    class truck : public vehicle{
      int passenger_load;
      float payload;
    public:
      int passengers(void) {return passenger_load;}
    };
    
    class boat : public vehicle{
      int passenger_load;
    public:
      int passengers(void) {return passenger_load;}
      void message(void) {cout << "Boat message
    ";}
    };
    
    int main(){
      vehicle *unicycle;
      car *sedan;
      truck semi;
      boat sailboat;
    
      unicycle = new vehicle;
      unicycle-> message(); //输出Vehicle message
      delete unicycle;
    
      unicycle = new car;
      unicycle -> message(); //输出Vehicle message
    
      sedan = (car *) unicycle;
      sedan -> message(); //输出Car message
      delete sedan;
    
      semi.message(); //输出Vehicle message
      sailboat.message(); //输出Boat message
    }
    

    该程序的运行结果,我们已经标注在程序之中。因为指针的类型决定调用那一个成员函数,所以,一个vehicle*调用vehicle成员函数,即使它指向派生类的对象。同样,一个car *也调用car 的成员函数。我们把这称为早期联编或静态联编,因为指针要调用那一个函数是在编译时就确定的

    那么,当vehicle*指向派生类对象时,我们能不能通过该指针来调用派生类的成员函数呢?在C++中,我们是可以作到的,这要用到C++的多态特性。 也就是说,基类指针是调用基类的成员函数,还是调用派生类的成员函数,不是由指针的类型决定的,而是由指针指向的对象的类型决定的。

    2. 什么是多态

    多态也称为动态联编或迟后联编,因为到底调用哪一个函数,在编译时不能确定,而要推迟到运行时确定。也就是说,要等到程序运行时,确定了指针所指向的对象的类型时,才能够确定。在C++中,动态联编是通过虚函数来实现的。

    我们知道,函数调用是通过相应的函数名来实现的。对于源程序进行编译后,存放在内存中的可执行程序,函数实际上是一段机器代码,它是通过首地址进行标识和调用的。例如,假定定义一个函数:  

    void func(){
        //…
    };
    

    我们可以用下面的语句调用这个函数:

    func(); //调用func函数
    

     这是在源程序中调用函数的方法,它是用函数名操作的。下面我们看看在可执行程序中函数调用是怎么操作的,我们用汇编语言来说明,因为汇编语言和机器语言(计算机可以直接执行的语言)是一一对应的。

      在可执行程序中,函数调用使用下面的方法:

    call [xxxxx]
    xxxxx代表存放函数代码内存空间的首地址。
    

     call是汇编语句中的一条指令,意思是调用一个函数。实际操作过程是:保存当前地址、保护现场,跳转到xxxxx地址执行。正是基于这个原因,在C/C++中的函数名是一个指针,该指针指向该函数段代码在内存中的首地址。如何将源程序中的函数调用和函数体(也就是在内存中该函数的机器代码)联系起来呢?这件工作是由编译器和连接程序来完成的。

     在C/C++语言中,函数调用在程序运行之前就已经和函数体(函数的首地址)联系起来。编译器把函数体翻译成机器代码,并记录了函数的首地址。 在对函数调用的源程序段进行编译的时候,编译器知道这个函数名的首地址在那里(它可以从生成的标识符表中查到这个函数名对应的首地址),然后将这个首地址替换函数名,一并翻译成机器码。这种编译方法称为早期或静态联编。

     那么,当vehicle*指向派生类对象时,我们能不能通过该指针来调用派生类的成员函数呢?从这种编译方法来看,是不可能的。因为编译器只会寻找vehicle*的成员函数。如何实现这个功能:当用基类指针调用成员函数时,是调用基类的成员函数,还是调用派生类的成员函数,不由指针的类型决定,而由指针指向的对象的类型决定呢?也就是说,如果基类指针指向基类对象,就调用基类的成员函数,如果基类指针指向派生类对象,就调用派生类的成员函数。这就要用到另外一种方法,称为动态联编或迟后联编。到底调用哪一个函数,在编译时不能确定,而要推迟到运行时确定。在C++中,动态联编是通过虚函数来实现的。下面我们先介绍虚函数,然后讨论动态联编实现的原理。

    3. 为什么使用虚函数

    使用虚函数,我们可以获得良好的可扩展性。在一个设计比较好的面向对象程序中,大多数函数都是与基类的接口进行通信。因为使用基类接口时,调用基类接口的程序不需要改变就可以适应新类。如果用户想添加新功能,他就可以从基类继承并添加相应的新功能。

    4. 虚函数的特点

    虚函数的定义很简单,只要在成员函数原型前加一个关键字virtual即可。如果一个基类的成员函数定义为虚函数,那么,它在所有派生类中也保持为虚函数,即使在派生类中省略了virtual关键字。需要注意的是:要达到动态联编的效果,基类和派生类的对应函数不仅名字相同,而且返回类型、参数个数和类型也必须相同。

    基类vehicle的成员函数message被定义为虚函数,虽然其派生类中的message成员函数定义时,没有virtual关键字,但都是虚函数。如果派生类中有与基类对应的方法,并且基类指针指向派生类的对象,那么基类指针调用的方法是派生类的方法。

    如果将vechicle中的message()函数定义为虚函数,那么main函数中四个相同的语句“unicycle->message();”,它们的结果并不相同,结果依次是:vechicle message, car message, car message, vechicle message, boat message。哪一个类的message成员函数被调用,不是在编译时确定的,而是根据运行时unicycle指针指向的对象的类型确定的。由于类truck没有覆盖基类的message成员函数,系统调用基类的message成员函数。

    一个有意思的情况:如果在类car中,将虚函数void message(void)声明为private,那么,car sedan将无法调用message()函数(这是因为message()是car类的私有函数,实例不能调用),而vehicle *unicycle = &sedan,却可以调用message()函数,且调用的是car类中的message()函数。

    5. 什么是纯虚函数及纯虚函数的作用

    纯虚函数是一种特殊的虚函数。纯虚函数的定义如下:

    class vehicle{
      int wheels;
      float weight;
    public:
      virtual void message(void) = 0;  //纯虚函数
    };
    

     纯虚函数是一个在基类中声明的虚函数,它在该基类中没有定义具体的操作内容,要求各派生类根据实际需要定义自己的版本,纯虚函数的声明格式为:

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

     声明为纯虚函数之后,基类中就不再给出函数的实现部分。纯虚函数的函数体由派生类给出。含有纯虚函数的类称为虚类,又叫做抽象类,不能被实例化。那这种虚类为什么会存在呢?这是因为,有些定义的基类,并不适合被实例化。比如交通工具可以是车、船等,但交通工具本身是一个抽象的概念,不对应着某一具体的事物。所以,可以以虚类为基类,然后由派生类来实现虚基类中的纯虚函数,再实例化这个派生类。

  • 相关阅读:
    leetcode 29-> Divide Two Integers without using multiplication, division and mod operator
    ros topic 发布一次可能会接收不到数据
    python中的print()、str()和repr()的区别
    python 部分函数
    uiautomatorviewer错误 unable toconnect to adb
    pyqt 不规则形状窗口显示
    appium 计算器demo
    Spring 3.0 注解注入详解
    Spring Autowire自动装配
    restful 学习地址
  • 原文地址:https://www.cnblogs.com/sunada2005/p/3397423.html
Copyright © 2011-2022 走看看