C++ 多态

2019-10-29 0 条评论 77 次阅读 0 人点赞

多态

《C++ Primer》

C++有三种方式支持多态性:

  1. 通过一个隐式转换,从“派生类指针或引用”转换到“其共有基类类型的指针或引用“

    Query *pquery = new NameQuery("Glass");

  2. 虚拟函数机制:pquery->eval();

  3. 通过dynamic_cast和typeid操作符

    if(NameQuery *pnq = dynamic_cast<NameQuery*>(pquery))...

1. 多态的定义及实现

通俗来说,多态就是当要完成某个行为的时候,不同对象去完成时产生的不同状态。

1.1 多态构成的条件

多态是不同继承关系的类对象去调用同一个函数,产生了不同的行为

在继承中要构成多态有两个条件:

  1. 必须通过基类指针或引用调用虚函数

    虚函数:即被virtual修饰的类成员函数称为虚函数

  2. 被调用的函数必须是虚函数,并且派生类必须对基类函数进行重写

    虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派生类的虚函数重写了基类的虚函数。

    //BuyTicket()构成重写
    class Person{
    public:
        //虚函数
        virtual void BuyTicket(){
            cout << "全价票" << endl;
        }    
    };
    
    class Student:public Person{
    public:
        //虚函数
        virtual void BuyTicket(){
            cout << "学生票" << endl;
        }    
    };
    
    void func(Person &people){
        people.BuyTicket();
    }
    
    void Test(){
        Person Mike;
        Func(Mike);
    
        Student John;
        Func(John);
    }                                             

1.2 虚函数重写的两个例外

  1. 协变(基类与派生类虚函数返回值类型不同)

    派生类重写基类虚函数时,与基类虚函数返回值类型不同,即基类虚函数返回基类对象的指针或引用,派生类函数返回派生类对象的指针或引用,称为协变。

    概括:假如B继承自A, D继承自C, 那么A中虚函数返回了C类指针, B中虚函数返回了D类指针,这种情况也构成重写。

    class A{};
    class B: plubic A {};
    
    class Person{
    public:
    virtual A* func(){return new A;}
    };
    
    class Student : public Person{
    public:
       virtual B* func(){return new B;}
    }
  2. 析构函数的重写(基类与派生类析构函数名字不同)

    若基类的析构函数为虚函数,此实派生类析构函数只要定义,无论是否加virtual关键字,都与积累的析构函数构成重写,即使基类与派生类析构函数名字不同。

    函数名不同看似不符合重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,变异后的析构函数的名称统一理解成dedtructor。

    所有的析构函数在底层的函数名都是相同的。

    虚析构函数使用来解决子类对象(或指针)转化为父类对象(或指针)进行析构的问题的.

      class Person{
      pubilc:
          virtual ~Person(){cout << "~Person" << endl;}
      };
    
      class Student{
      pubilc:
          virtual ~Student(){cout << "~Student" << endl;}
      };
    
      //只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数的时候才能构成多态,才能保证p1和p2指向的对象正确调用析构函数。
      int main(){
          Person *p1 = new Person;
          Person *p2 = new Student;
    
          delete p1;
          delete p2;
    
          return 0;
      }
    1. C++11中的final和override

      final: 不能被继承的类或者不能被重写的虚函数(父类)

      virtual void Drive() final{}

      override: 声明子类的某个函数必须重写父类的某个函数(子类)

      virtual void Drive(){cout << "Benz" << endl;}

1.3 重载、重写、隐藏的比较

  • 重载:1. 两个函数在同一个作用域内;2. 函数/参数相同(与返回值无关)

  • 重写(覆盖):1. 两个函数分别在基类和派生类的作用域;2. 函数名/参数/返回值都必须相同(协变除外);3. 两个函数必须时虚函数

  • 隐藏(重定义):1. 两个函数分别在基类和派生类的作用域;2. 函数名相同; 两个基类和派生类的函数不构成重写就是重定义

    重写是换了个函数, 隐藏是藏了一个函数, 所以重写是一个函数, 隐藏是两个函数

2. 抽象类

在虚函数后面写上= 0,则这个函数位纯虚函数。包含纯虚函数的类叫做抽象类(接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

class Car{
public:
    virtual void Drive() = 0;
};

class Benz :public Car{
public:
    virtual void Drive(){
    cout << "Benz-舒适" << endl;
    }
};

class BMW :public Car{
public:
    virtual void Drive(){
    cout << "BMW-操控" << endl;
    }
};

void Test(){
    Car* pBenz = new Benz;
    pBenz->Drive();
    Car* pBMW = new BMW;
    pBMW->Drive();
}

关于接口继承和实现继承:

  • 普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
  • 虚函数的继承是一种接口集成,派生类继承的是基类虚函数的接口,目的就是为了重写,达成多态,继承的是接口。
  • 如果不实现多态,不要把函数定义成虚函数。

3. 虚表

  • 虚表是一个二级函数指针,只要类中包含虚函数,就会在对象头部包含一个虚表指针(32位系统下占用4个字节,64位系统占用8个字节,虚表指针在不同系统下存放位置可能产生差异)
  • 虚表相当于一个函数指针数组,里面存放的就是虚函数的地址
  • 当子类继承父类时,会继承虚表;当子类有新的虚函数时,会在虚表后面增加新的项;当子类重写父类的虚函数时,会把虚表中原来的某一项覆盖。
  • 虚表存放在代码段(vs,有平台差异)

4. 多重继承

  • 一个派生类可以来源于多个基类,多个基类用逗号隔开;

    如果多个父类中有重名的成员那么就会产生二义,必须用父类名::的方式指明调用谁的成员。

  • 如果继承中的多个父类中有多个虚表,那么子类将全部继承下来。

  • 如果子类出现了新的虚函数,那么会加载第一个虚表(第一个继承的父类虚表)后面;

    如果多个父类中含有相同的虚函数,子类重写后将只会出现一个虚函数。

5. 菱形继承、虚继承

将父类的父类称为爷爷类

某个类的两个父类用于一个相同的父类

  • 冗余性:这个类包含了两份爷爷类
  • 二义性:两个爷爷长得一样,分不清楚

含有一个虚基类指针(vbptr),指向自己的基类,作用是可以描述自己的父类,当发现被继承的另一个父类也有这么一个相同基类时,两个基类就会合并,只保留一个。

  • 普通的继承只继承了爷爷的衣钵,不知道爷爷是谁
  • 虚继承既继承了爷爷的衣钵,也知道爷爷是谁,于是当发现两个父类的父类是相同的时候,就会合并两个爷爷只保留一个

虚继承只用于菱形继承的情况,因为它可以解决菱形继承带来的问题。

L_KingMing

这个人太懒什么东西都没留下

文章评论(0)