C++中的多态

什么是多态?

当想不同的人发出同样的一个命令时,不同的人可能有不同的反应。这就是多态性。

有了多态性,相当于对象有了主观能动性。对对象的使用者而言,使用起来就方便了。特别是当被管理对象的数量和类型增加时,管理者的工作量并不增加。

  • 通过继承同一个基类,产生了相关的不同的派生类,与基类中的同名的成员函数在不同的派生类中会有不同的实现,也就是说:一个接口、多种方法。
  • 多态是面向对象的重要技术之一,他是一种行为的封装,是同一个事物所表现出来的多种形态,简单地说就是:一个接口、多种形态。
  • 那么,在运行时使用同一个成员名来调用类对象的成员函数,会调用哪个对象的成员函数呢?这就是多态要解决的问题!!

多态的作用

  • 多态技术允许将基类指针或基类引用指向派生类对象。
  • 把 不同派生类的对象都当作基类对象来看待,可以屏蔽不同派生类之间的差异,从而写出通用的代码适应需求的不断变化。

多态的分类

  1. 编译时的多态性(静态绑定),通过运算符重载或者函数重载实现。
  2. 运行时的额多态(动态绑定),通过虚函数和基类指针共同作用实现。

C++中多态的实现

  • C++中,基类指针是用来指向基类对象的,如果用它来指向派生类对象,则进行指针类型转换(上行转换),将派生类指针转换为基类指针,所以该指针会指向派生类对象中的基类部分,通过该指针是无法调用派生类对象中的成员函数的

但是,虚函数突破了这一限制。在派生类的基类部分中,派生类的虚函数取代了基类原来的同名虚函数,因此,在使基类指针指向派生类对象后,使用该基类指针调用这个同名函数成员时就调用了派生类的虚函数。

  • 当把基类的某个成员函数声明为虚函数时,C++允许在其派生类中对该虚函数进行重新定义,赋予它新的功能,并且可以通过基类指针指向同一类族的不同派生类的对象,来调相应派生类中的该同名虚函数
    由虚函数实现的动态多态性就是:同一类族中不同的派生类对象,对同一函数调用做出不同的响应。

  • 虚函数的使用方法:

    1. 在基类中使用virtual关键字声明成员函数为虚函数(这样就可以在派生类中对该虚函数进行重新定义,赋予它新的功能)。
    2. 在派生类中重新定义此虚函数,要求函数名、形参列表、返回值类型均要与基类中的虚函数相同,并根据具体需要重新定义它的函数体。
  • C++ 规定,当一个成员函数被定义为虚函数后,其派生类中的同名函数都自动后才能为虚函数(不一定有关键字virtual),但是为了清晰,习惯上每一层都加上virtual关键字。

class base
{
  public:
    base(int a):b(a){}
    void my()
        { cout<<"mybase."<<endl; }
    virtual void func()
        { cout<<"I am a base."<<endl; }

  private:
    int b;
};
class child1:public base
{
  public:
    child1(int a1,int a2):base(a1),c1(a2){}
    void my(){ cout<<"mychild."<<endl;}
    virtual void func(){ cout<<"I am a child1."<<endl; }

  private:
    int c1;
};

class child2:public base
{
   public:
    child2(int a1,int a2):base(a1),c2(a2){}
    void my() { cout<<"mychild."<<endl;} 
    virtual void func() { cout<<"I am a child2."<<endl; } 

   private: 
     int c2;
};


base *bptr;
child1 c1(10,1);
child2 c2(10,2);

bptr=&c1;    //基类指针指向派生类对象child1
bptr->func();//调用的是child1中的func()
c1.func();   //调用的是child1中的func()
bptr->my();  //调用的是派生类对象中的基类部分,也就是base中的my()
c1.my();     //调用的是派生类对象中的成员函数,也就是child1中的my()

bptr=&c2;    //基类指针指向派生类对象child2
bptr->func();//调用的是child2中的func()
c2.func();   //调用的是child2中的func()
bptr->my();  //调用的是派生类对象中的基类部分,也就是base中的my()
c2.my();     //调用的是派生类对象中的成员函数,也就是child2中的my()

虚函数和纯虚函数

  • 应该考虑: 对成员函数的调用是通过对象名还是通过基类指针或者基类引用去访问。如果是后两者,则应当声明为虚函数。
  • 虚函数:如果一个类中定义了虚函数virtual,那么这个虚函数是被实现的,其作用就是为了让该虚函数在这个类的的派生类中被覆盖,被实现为不同的功能,从而结合基类指针以实现动态多态性。
  • 纯虚函数:有时,在定义一个虚函数时,并不定义其函数体,即它的函数体是空的,它的作用只是保留一个虚函数名,它关注的是接口的统一性,其具体的功能实现由它的派生类完成。

    virtual 返回类型 函数名(参数表) = 0;
    //example
    virtual float area(float a,float b ) = 0;
    
    1. 最后的“=0”的作用只是告诉编译器这是一个纯虚函数。
    2. 纯虚函数只有具体函数的名称,没有函数体,不具备函数的功能,因此不能被调用。只有在派生类中被重新定义过以后才具备函数的功能,才能被调用。
    3. 如果在一个类中声明了纯虚函数,但是在其派生类中该纯虚函数并没有定义,那么该虚函数在这个派生类中仍然为纯虚函数,仍然不具备函数的功能。
  • 抽象类和抽象基类:一个类中至少含有一个纯虚函数,则称为抽象类。由于它经常用来作基类,故又被称为抽象基类。注意:由于抽象类中有未定义全的函数,因此无法定义抽象类的对象。它的作用就是作为一个类族的共同基类,或者说是为一个类族提供一个公共接口。

    如果抽象类的派生类中没有重新定义此虚函数,只是继承了基类的纯虚函数,那么派生类仍然是一个抽象类。

  • 使用虚函数,系统要有一定的空间开销。当一个类中含有虚函数时,编译系统会为它构造一个虚函数指针vptr(4字节),同时这个虚函数指针指向一个虚函数表vtable,虚函数表示一个指针数组,存放的是该类中的每个虚函数的入口地址。(查表是高效的,因此多态性是高效的。)

虚析构函数的作用

构造函数不能使虚函数,但是析构函数可以是虚函数,而且最好是虚函数。

  • 当派生类的对象从内存中撤销时,一般先调用派生类的析构函数释放该对象中的派生类部分,再调用基类的析构函数释放该对象中的基类部分,从而能够完整的释放该对象内存。
  • 但是,当用基类指针指向了一个派生类对象,即 base *bptr = new child;此时用delete bptr;来撤销bptr 指向的动态存储空间时,只会执行基类的析构函数来释放该堆内存中的基类部分,但是并不会执行派生类的析构函数来释放该堆内存中的派生类部分。此时,就会造成内存泄漏现象
  • 为了避免此类现象发生,我们将基类的析构函数声明为虚析构函数,这样就解决了上述问题(即先调用派生类的析构函数释放该动态空间中的派生类部分,再调用基类的析构函数释放该动态空间中的基类部分,从而能够完整的释放该堆内存)。
  • 如果将基类的析构函数声明为虚析构函数,那么该基类的所有派生类的析构函数都自动成为虚析构函数。

类的成员函数的重载、覆盖(重写)、隐藏(重定义)

函数重载:重载函数通常用来命名一组功能相似的函数。

  1. 两个函数要在相同的类域
  2. 两个函数的名称相同
  3. 两个函数的的形参列表必须不同

函数覆盖:覆盖是指派生类函数覆盖基类函数。

  1. 两个函数要在不同的类域
  2. 两个函数的名称相同
  3. 基类函数必须是虚函数
  4. 两个函数的形参列表和返回值类型要相同

函数隐藏:指派生类的函数屏蔽了与其同名的基类函数。

  1. 两个函数在不同的类域
  2. 两个函数的名称相同
  3. 两个函数的形参列表不同
  4. 如果派生类函数与基类函数形参列表相同,但是在基类函数中没有virtual关键字,也会发生函数隐藏