『 C++ 入门到放弃 』- 多态

一、多态的概念

  • 多态顾名思义就是多种形态

  • 多态的分类

    • 编译时多态 => 静态多态

      静态多态的例子:函数模版、函数重载

      它们的特点是可以根据我们传入不同的参数,达成调用不同的函数的效果。之所以称为「静态」,是因为实参传给形参的参数匹配是在编译阶段完成的

    • 运行时多态 => 动态多态 ( 本篇重点说明的目标 )

      动态多态就是根据传入不同的对象来达到调用不同的行为 ( 相同函数不同效果 ) 以达成多态的效果

      举例来说:高铁定票 => 学生票 75 折,全票不打折

      如何达到例子中的效果?就是借由传入不同的对象,调用不同行为( 打折或不打折等 )

所以接下来我们要来正式说明面向对象编程三大特性 ( 封装、继承、多态 ) 之一的多态

1.1 多态的定义与条件

多态就是有着继承关系的类对象,调用相同的函数但形成不同的行为。如上面的例子 Student 继承 Person

假设我们有个函数 Buyticket()

Student 调用 => 票价打75折

Person 调用 => 不打折
多态的条件有:

  1. 需要有继承关系

  2. 要是基类的指针或引用调用虚函数

    => 因为只有基类的指针或引用才能既指向基类对象又指向派生类对象

  3. 被调用的函数一定要是虚函数,并且是完成了虚函数的重写 ( 覆盖 )

    => 重写或者覆盖了,基类和派生类之间才能有不同的函数,多态的不同形态效果才能达到

  4. 静态绑定 & 动态绑定

    • 静态绑定:对不满足多态条件 (指针或者引用+调用虚函数函数) 的函数调用是在编译时绑定的也就是编译时就确定了调用函数的地址,叫做静态绑定
    • 动态绑定:满足多态条件的函数调用是在运行时绑定,也就是在运行时到指向对象的虚函数表中找到调用函数的地址,也就做动态绑定。而多态是在运行时才确定要调用函数的地址,所以多态属于动态绑定

1.2 虚函数

  • 虚函数就是在类的「成员函数」前加上关键字 virtual 那么这个函数就会被定义成了虚函数
  • 不是成员函数不能加 virtual 修饰
c++ 复制代码
class Person
{
public:
};
virtual void BuyTicket() { cout << "买票-全价" << endl;}
1.2.1 虚函数的重写

虚函数重写 / 覆盖的定义:

  • 派生类和基类有个完全相同的函数 ( 包含:函数名、返回值类型、参数列表 ) 且要在基类要定义为虚函数的函数前加上 virtual

    • 派生类的虚函数可以不加virtual => 因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性

      但这种比较不规范,建议还是加上

    • virtual关键字只在声明时加上,在类外实现时不能加

c++ 复制代码
#include <iostream>
using namespace std;
class Person
{
public:
    virtual void Buyticket()
    {
        cout << "全票" << endl;
    }
};

class Student : public Person
{
public:
    virtual void Buyticket()
    {
        cout << "七五折" << endl;
    }
};
void func(Person & p){
    p.Buyticket();
}
int main()
{
    Person p;
    Student st;

    func(p); // 全票
    func(st); // 七五折
    return 0;
}
  • 静态成员函数不能设为虚函数

    理由:静态成员函数与具体对象无关,属于整个类,核心关键是没有隐藏的this指针,可以通过类名::成员函数名直接调用,此时没有this无法拿到虚表,就无法实现多态,因此不能设置为虚函数

  • 析构函数重写

    基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,所以基类的析构函数加了vialtual修饰,派生类的析构函数就构成重写。

    • Q1 : 为什么要把基类的析构函数变成虚函数?

      c++ 复制代码
      class A {
      public:
          ~A() {
              cout << "A 析构" << endl;
          }
      };
      
      class B : public A {
      public:
          ~B() {
              cout << "B 析构" << endl;
          }
      };
      
      int main() {
          A* p = new B;
          delete p; // 只会呼叫 A 的析构函数 
      
        	/*
        	p 是 A*,但实际上它指向 B。
        	delete p; 时,只会呼叫 ~A(),不会呼叫 ~B()!
      		如果 ~B() 有需要释放的资源,这些资源就不会被释放,会「造成内存泄漏
        	*/
      }
1.2.2 override & final 关键字
  • override

    因为虚函数的条件较严苛,如果在设计虚函数时不免有因为疏忽而少写或写错的时候,因此C++11提供了关键字override 帮助用户检测是否重写

  • final

    如果我们不想让派生类重写这个虚函数,那么可以用final去修饰

1.2.3 纯虚函数 & 抽象类

在虚函数的后面写上 =0,则这个函数称为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被纯派生类重写,但是语法上可以实现) ,只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例虚函数化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。纯虚函数某种程度上强制了派生类重写虚函数,否则派生类自己也无法实例化对象。

c++ 复制代码
class Car
{
  public:
  virtual void Derive() = 0;
};

class BMW : public Car
{
  public:
  virtual void Derive() override
  {
    cout << "BMW" << endl;
  }
};

class Benz : public Car
{
  public:
  virtual void Derive() override
  {
    cout << "Benz" << endl;
  }
};

int main()
{
  Car car; // error: variable type 'Car' is an abstract class

  Car *pBenz = new Benz;
  Car *pBMW = new BMW;

  pBenz->Derive();
  pBMW->Derive();
  return 0;
}

二、重写 vs. 隐藏 vs. 重载

三、多态的底层原理

首先我们要先认识两个新名词 : 虚函数表、虚函数指针

  • 虚函数表:每个定义了虚函数的类,编译器会为它产生一个表,里面放的是函数指针,指向该类的虚函数实现

    基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共用同一张虚表,不同类型的对象有各自独立的虚表,所以基类和派生类有各自独立的虚表

  • 虚函数指针 ( vfptr ) : 每个对象中会有一个隐藏的指针,指向该类的虚函数表,称为 vfptr

3.1 多态底层运作流程

c++ 复制代码
class Base {
public:
    virtual void speak() { std::cout << "Base speaking\n"; }
};

class Derived : public Base {
public:
    void speak() override { std::cout << "Derived speaking\n"; }
};

int main() {
    Base* obj = new Derived();
    obj->speak(); 
}
  1. Derived 是 Base 的子类,且 speak() 是虚函数,编译器会为每个类生成对应的 vtable。
  2. 建立 Derived 对象时,编译器会:
    • 在对象中加入一个 vfptr
    • 将这个指针设为指向 Derived 的 vtable
  3. 执行 obj->speak(); 时:
    • 透过 vfptr 找到 Derived 的 vtable
    • 从 vtable 中找到对应的 speak() 函数指针
    • 呼叫 Derived::speak()

3.2 虚函数表长什么样

"*虚函数表是在编译期间就确定的

c++ 复制代码
/*vtable for Derived:

Index 0: &Derived::speak
Index 1: ...(其他虚函数)

*/


// 每个 vtable 本质上是「函数指针数组」 类似下面
typedef void(*FuncPtr)();

FuncPtr vtable_Derived[] = {
    (FuncPtr)&Derived::speak,
    ...
};

3.3 vfptr 是怎么储存在对象中

c++ 复制代码
Derived obj;

obj 内存:
+-------------------+
| vfptr --------+   |
|               |   |
| 其他资料成员    |   |
+-------------------+
                |
                v
       +------------------+
       | vtable_Derived   |
       +------------------+
相关推荐
dongzhenmao34 分钟前
P1484 种树,特殊情形下的 WQS 二分转化。
数据结构·c++·windows·线性代数·算法·数学建模·动态规划
Dxy123931021638 分钟前
Python PDFplumber详解:从入门到精通的PDF处理指南
开发语言·python·pdf
EutoCool2 小时前
Qt:布局管理器Layout
开发语言·c++·windows·嵌入式硬件·qt·前端框架
Cyanto2 小时前
Spring注解IoC与JUnit整合实战
java·开发语言·spring·mybatis
写不出来就跑路2 小时前
WebClient与HTTPInterface远程调用对比
java·开发语言·后端·spring·springboot
茫忙然2 小时前
【WEB】Polar靶场 Day7 详细笔记
笔记
悠哉清闲2 小时前
C++ MediaCodec H264解码
开发语言·c++
张人玉3 小时前
c#中Random类、DateTime类、String类
开发语言·c#
Jinkxs3 小时前
JavaScript性能优化实战技术
开发语言·javascript·性能优化
今天背单词了吗9804 小时前
算法学习笔记:17.蒙特卡洛算法 ——从原理到实战,涵盖 LeetCode 与考研 408 例题
java·笔记·考研·算法·蒙特卡洛算法