目录
  • 多态的概念
  • 多态的定义及实现
    • 构成条件
    • 虚函数
      • 虚函数的重写
      • 虚函数重写的两个例外
  • 抽象类
    • 抽象类的概念
      • 接口继承和实现继承
      • 多态的原理
        • 虚函数表
          • 多态的原理

          多态的概念

          概念:通俗的来说就是多种形态,具体就是去完成某个行为,当不同类型的对象去完成同一件事时,产生的动作是不一样的,结果也是不一样的。

          举一个现实中的例子:买票这个行为,当普通人买票时是全价;学生是半价;军人是不需要排队。

          多态也分为两种:

          • 静态的多态:函数调用
          • 动态的多态:父类指针或引用调用重写虚函数。

          这里的静态是指在编译时实现多态的,而动态是在运行时完成的。

          多态的定义及实现

          构成条件

          多态一定是建立在继承上的,那么除了继承还要两个条件:

          • 必须通过基类(父类)的指针或引用调用函数
          • 被调用的函数必须是虚函数,且派生类(子类)必须对积累的虚函数进行重写。

          C++ 超全面讲解多态

          虚函数

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

          class Person
          {
          public:
              virtual void BuyTicket()
              {
                  cout<<"全价票"<<endl;
              }
          };

          注意:

          • 只有类的非静态成员函数可以是虚函数
          • 虚函数这里virtual和虚继承中用的是同一个关键字,但是他们之间没有关系;虚函数这里是为了实现多态;虚继承是为了解决菱形继承的数据冗余和二义性,它们没有关联

          虚函数的重写

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

          例:

          class Person
          {
          public:
              virtual void BuyTicket()
              {
                  cout<<"全价票"<<endl;
              }
          };
          ​
          class Student :public Person
          {
          public:
              //子类的虚函数重写了父类的虚函数
              virtual void BuyTicket()
              {
                  cout<<"半价票"<<endl;
              }
          };
          ​
          class Soldier : public Person
          {
          public:
              //子类的虚函数重写了父类的虚函数
              virtual void BuyTicket()
              {
                  cout<<"优先买票"<<endl;
              }
          };
          //多态的实现
          void f(Person& p)//这块的参数必须是引用或者指针
          {
              p.BuyTicket();
          }
          ​
          int main()
          {
              Person p;
              Student st;
              Soldier so;
              
              f(p);
              f(st);
              f(so);
              
              return 0;
          }

          注意:这里子函数的虚函数可以不加virtual,也算完成了重写,但是父类的虚函数必须要加,因为子类是先继承父类的虚函数,继承下来后就有了virtual属性了,子类只是重写这个virtual函数;除了这个原因之外,还有一个原因,如果父类的析构函数加了virtual,子类加不加都一定完成了重写,就保证了delete时一定能实现多态的正确调用析构函数。

          虚函数重写的两个例外

          1、协变

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

          例:

          class A{};
          class B : public A{};
          ​
          class Person
          {
          public:
              virtual A* f()
              {
                  return new A;
              }
          };
          ​
          class Student : public Person
          {
          public:
              virtual B* f()           //返回值不同但是构成虚函数重写
              {
                  return new B;
              }
          };

          2、析构函数的重写

          如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor

          例:

          class Person {
          public:
              //建议把父类析构函数定义为虚函数,这样方便子类的虚函数重写父类的虚函数
              virtual ~Person() {cout << "~Person()" << endl;}
          };
          ​
          class Student : public Person {
          public:
              virtual ~Student() { cout << "~Student()" << endl; }
          };
          // 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
          int main()
          {
              Person* p1 = new Person;
             //这里p2指向的子类对象,应该调用子类析构函数,如果没有调用的话,就可能内存泄漏
              Person* p2 = new Student;
              //多态行为
              delete p1;
              delete p2;
              //只有析构函数重写了那么这里delete父类指针调用析构函数才能实现多态。
              return 0;
          }

          C++11 override和finel

          从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写

          final:修饰虚函数,表示该虚函数不能再被重写

          class Car
          {
          public:
              virtual void Drive() final {}
          };
          class Benz :public Car
          {
          public:
              //会在这块报错,因为基类的虚函数已经被final修饰,不能被重写了
              virtual void Drive() {cout << "Benz-舒适" << endl;}
          };  

          override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错

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

          重载、覆盖(重写)、隐藏(重定义)的对比

          C++ 超全面讲解多态

          抽象类

          抽象类的概念

          纯虚函数:在虚函数的后面加上=0就是纯虚函数,有纯虚函数的类就是抽象类,也叫接口类,抽象类无法实例化对象。抽象类的子类不重写父类的虚函数的话,也是一个抽象类。

          //抽象类的定义
          class Car
          {
          public:
              virtual void run()=0;   //不用实现只写接口就行。   
          };

          纯虚函数不写函数体,并不意味着不能实现,只是我们不写。因为写出来也没有人用。

          虚函数的作用

          • 强制子类重写虚函数,完成多态。
          • 表示抽象类。

          接口继承和实现继承

          普通函数的继承就是实现继承,虚函数的继承就是接口继承。子类继承了函数的实现,可以直接使用。虚函数重写后只会继承接口,重写实现。所以如果不用多态,就不要把函数写为虚函数。

          纯虚函数就体现了接口函数。下面我们来实现一道题,展现一下接口继承。

          class A
          {
          public:
              virtual void fun(int val=0) 
              {
                  cout<<"A->val = "<<val <<endl;
              }
              void Fun()
              {
                  fun();
              }
          };
          ​
          class B:public A
          {
          public:
              virtual void fun(int val=1)
              {
                  cout<<"B->val"<<val<<endl;
              }
          };
          ​
          int main()
          {
              B b;
              A* a=&b;
              a->Fun();
              return 0;
          }

          结果打印为 :B->val=0

          子类对象切片给父类指针,传给Fun函数,满足多态,会去调用子类的fun函数,但是子类的虚函数继承了父类的接口,所以val是父类的0。

          多态的原理

          虚函数表

          class A
          {
          public:
              virtual void fun()
              {
                  
              }
              protected:
              int _a;
          };

          sizeof(A)是多少?

          打印出来是8。

          我们定义了一个A类型的对象a,打开调试窗口,发现a的内容如下

          C++ 超全面讲解多态

          我们发现出了成员变量_a以外,还多了一个指针,这个指针是不准确的,实际上应该是 _vftptr(virtual function table pointer),虚函数表指针。在计算类大小的时候要加上这个指针的大小。虚表就是存放虚函数的地址地方,当我们去调用虚函数,编译器就会通过虚表指针去虚表里查找。

          class A
          {
          public:
              void fun1()
              {
                  
              }
              virtual void fun2()
              {}
          };
          ​
          int main()
          {
              A* a=nullptr;
              a->fun1();//调用函数,因为这是普通函数的调用
              a->fun2();//调用失败,虚函数需要对指针操作,无法操作空指针。
              return 0;
          }

          实现一个继承

          class A
          {
              public:
              virtual void fun1()
              {}
              virtual void fun2()
              {}
          };
          class B : public A
          {
              public:
              virtual void fun1()
              {}
              virtual void fun2()
              {}
          };
          ​
          int main()
          {
              A a;
              B b;
              return 0;
          }

          C++ 超全面讲解多态

          子类与父类一样有一个虚表指针。

          子类的虚函数表一部分继承自父类。如果重写了虚函数,那么子类的虚函数会在虚表上覆盖父类的虚函数。

          本质上虚函数表是一个虚函数指针数组,最后一个元素是nullptr,代表虚表的结束。所以,如果继承了虚函数,那么

          • 子类先拷贝一份父类虚表,然后用一个虚表指针指向这个虚表。
          • 如果有虚函数重写,那么在子类的虚表上用子类的虚函数覆盖。
          • 子类新增的虚函数按其在子类中的声明次序增加到子类虚表的最后。

          C++ 超全面讲解多态

          虚函数表放在内存的那个区,虚函数又放在哪?

          虚函数与虚函数表都放在代码段。

          多态的原理

          我们现在来看多态的原理

          class person
          {
          public:
              virtual void fun()
              {
                  cout<<"全价票"<<endl;
              }
          };
          class student : public person
          {
          public:
              virtual void fun()
              {
                  cout<<"半价票"<<endl;
              }
          };
          void buyticket(person* p)
          {
              p->fun();
          }

          C++ 超全面讲解多态

          这样就实现了不同对象去调用同一函数,展现出不同的形态。 满足多态的函数调用是程序运行是去对象的虚表查找的,而虚表是在编译时确定的。 普通函数的调用是编译时就确定的。

          动态绑定与静态绑定

          1.静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载

          2.动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。我们说的多态一般是指动态多态。

          这里我附上一个有意思的问题:

          就是在子类已经覆盖了父类的虚函数的情况下,为什么子类还是可以调用“被覆盖”的父类的虚函数呢?

          #include <iostream>
          using namespace std;
          ​
          class Base {
          public:
              virtual void func() {
                  cout << "Base func\n";
              }
          };
          ​
          class Son : public Base {
          public:
              void func() {
                  Base::func();
                  cout << "Son func\n";
              }
          };
          ​
          int main()
          {
              Son b;
              b.func();
              return 0;
          }

          输出:

          Base func

          Son func

          这是C++提供的一个回避虚函数的机制

          通过加作用域(正如你所尝试的),使得函数在编译时就绑定。

          声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。