1.虚函数的作用

    • 在类的继承层次结构中,在不同的层次中可以出现名字相同、参数个数和类型都相同而功能不同的函数。编译系统按照同名覆盖的原则决定调用的对象。
    • 在程序中不是通过不同的对象名去调用不同派生层次中的同名函数,而是通过指针调用它们。

      • 例:基类与派生类中有同名函数
        1. #include <iostream>
        2. #include <string>
        3. using namespace std;
        4. class Student
        5. {public:
        6. Student(int, string,float);//声明构造函数
        7. void display( ); //声明输出函数
        8. protected: //受保护成员,派生类可以访问
        9. int num;
        10. string name;
        11. float score;
        12. };
        13. Student::Student(int n, string nam,float s)
        14. {num=n;name=nam;score=s;}
        15. void Student::display( ) //定义输出函数
        16. {cout<<″num:″<<num<<″\\nname:″<<name<<″\\nscore:″<<score<<″\\n\\n″;}
        17. class Graduate:public Student {public: Graduate(int, string, float, float); //声明构造函数 void display( ); //声明输出函数 private: float pay;}; 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;}
    • 如何通过使用基类指针调用基类和子类对象的成员?

      • 虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。
    • 例:基类与派生类中有同名函数。

      #include <iostream>
      #include <string> 
      using namespace std;
      class Student
      {public: 
      Student(int, string,float);//声明构造函数 
      virtual void display( ); //声明输出虚函数 
      protected: //受保护成员,派生类可以访问 
      int num; 
      string name;
      float score; 
      };
      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″;}
      class Graduate:public Student 
      {public: 
      Graduate(int, string, float, float); //声明构造函数
      void display( ); //声明输出函数 
      private: 
      float pay;
      }; 
      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;}
      
      • 在**Student类中声明display函数为虚函数:virtual void display( ); **
      • 由虚函数实现的动态多态性就是: **同一类族中不同类的对象,对同一函数调用作出不同的响应**。
      • 虚函数的使用方法
        • 在基类用virtual声明成员函数为虚函数。在类外定义虚函数时,不必再加virtual。
        • 在派生类中重新定义此函数,要求函数名、函数类型、函数参数个数和类型全部与基类的虚函数相同,并根据派生类的需要重新定义函数体。
        • 定义一个指向基类对象的指针变量,并使它指向“同一类族”中需要调用该函数的对象。
        • 通过该指针变量调用此虚函数,此时调用的就是指针变量指向的对象的同名函数。
      • C++规定
        • 当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数。
        • 如果在派生类中没有对基类的虚函数重新定义,则派生类简单地继承其直接基类的虚函数。

    **

    • 重载、虚函数和覆盖的区别
      • 函数重载处理的是同一层次上的同名函数问题,而虚函数处理的是不同派生层次上的相同函数问题,前者是横向重载,后者可以理解为纵向重载 。
      • 如果没有虚函数,不同派生层次上的相同函数叫做函数覆盖。
      • 同一类族的虚函数的首部是相同的,而函数重载时函数的首部是不同的(参数个数或类型不同)。

    2.静态关联与动态关联

    • 确定调用具体对象的过程称为关联(binding)。
    • 函数重载和通过对象名调用虚函数,在编译时即可确定其调用的虚函数属于哪一个类,其过程称为静态关联(static binding),又称为早期关联。
    • 程序在调用虚函数时,如果是通过基类指针与虚函数的结合来实现多态性,例如“pt->display( )”,称为**动态关联 **
      • 编译系统在编译该行时只作静态的语法检查,无法确定调用哪一个类对象的虚函数。在运行阶段“绑定”具体对象,因此这种多态性是**动态多态性**
      • 由于动态关联是在编译以后的运行阶段进行的,因此也称为**滞后关联(late binding)。**


    3.在什么情况下应当声明虚函数**

    • 使用虚函数时,要注意:
      • 只能用virtual声明类的成员函数,而不能将类外的普通函数声明为虚函数。
      • 一个成员函数被声明为虚函数后,在同一类族中的类就不能再定义一个非virtual的但与该虚函数具有相同的参数(包括个数和类型)和函数返回值类型的同名函数。
    • 什么情况下声明虚函数
      • 首先看成员函数所在的类是否会作为基类,然后看成员函数在类的继承后有无可能被更改功能。
      • 如果成员函数在类被继承后功能不需修改,或派生类用不到该函数,则不要把它声明为虚函数。
      • 应考虑对成员函数的调用是通过对象名还是通过基类指针或引用去访问。
      • 有时,在定义虚函数时,函数体是空的。它的作用只是定义了一个虚函数名,具体功能留给派生类去添加。
    • 说明
      • 使用虚函数,系统要有一定的空间开销。
      • 当一个类带有虚函数时,编译系统会为该类构造一个**虚函数表(virtual function table,简称vtable)它是一个指针数组,存放每个虚函数的入口地址。**
      • 系统在进行动态关联时的时间开销是很少的,因此,多态性是高效的。
      • 只有基类的非静态成员函数可以被声明为虚函数。



    4.**虚析构函数**

    • 当派生类的对象从内存中撤销时一般先调用派生类的析构函数,然后再调用基类的析构函数。
    • 如果用new运算符建立了临时对象,并定义了一个指向该基类的指针变量,在程序用带指针参数的delete运算符撤销对象时,会发生一个情况: 系统会只执行基类的析构函数,而不执行派生类的析构函数。
    • 例:基类中有非虚析构函数时的执行情况。

      #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;
      }
      
      • 如果希望能执行派生类Circle的析构函数,可以将基类的析构函数声明为虚析构函数,如
        virtual ~Point(){cout<<″executing Point destructor″<<endl;}
        
    • 当基类的析构函数为虚函数时,无论指针指的是同一类族中的哪一个类对象,系统会采用动态关联,调用相应的析构函数,对该对象进行清理工作。

    • 如果将基类的析构函数声明为虚函数时,由**该基类所派生的所有派生类的析构函数也都自动成为虚函数**,即使派生类的析构函数与基类的析构函数名字不相同。
    • 最好把基类的析构函数声明为虚函数**。 **
    • 一般都习惯声明虚析构函数,即使基类并不需要析构函数,也显式地定义一个函数体为空的虚析构函数,以保证在撤销动态分配空间时能得到正确的处理
    • 构造函数不能声明为虚函数**。因为在执行构造函数时类对象还未完成建立过程,当然谈不上函数与类对象的绑定**