在这里插入图片描述

一、多态开篇

前面我们把继承讲完了,今天我们来学习面向对象最后一个、也是最难、最重要的特性——多态

多态是面试必考、底层原理最难、但写代码最实用的知识点。

如果还没学继承的小伙伴,一定要先看继承:
【C++】继承详解——基类/派生类、作用域、默认函数、菱形继承

今天我会把多态概念、构成条件、虚函数、重写、override/final、抽象类、多态原理、虚表全部讲透,全程高能,希望大家最后能有收获


二、多态的概念

什么是多态?通俗来说:多种形态

同一件事,不同的对象去做,会表现出不同的结果

比如:

  • 普通人买票:全价
  • 学生买票:半价
  • 军人买票:优先

比如:

  • 猫叫:喵~
  • 狗叫:汪汪

这就是多态

多态分为两种:

  1. 静态多态(编译时多态)
    函数重载、模板——编译时就确定调用哪个函数(之前讲过的内容,在我的主页可以查看)
  2. 动态多态(运行时多态)
    运行时才确定调用哪个函数——我们今天重点讲这个

三、多态的定义及实现

1. 多态的两个必须条件(背下来)

要实现多态,必须同时满足两个条件

  1. 必须通过基类的指针 或者 引用 调用虚函数
  2. 被调用的函数必须是虚函数,且子类必须对父类虚函数完成重写

两个条件缺一不可!


2. 虚函数

成员函数前面加 virtual 关键字,这个函数就是虚函数。

注意:只有类的成员函数才能加 virtual,普通函数不行!

class Person
{
public:
    // 虚函数
    virtual void BuyTicket()
    {
        cout << "买票-全价" << endl;
    }
};

3. 虚函数的重写(覆盖)

重写(覆盖)的要求:

  1. 函数名相同
  2. 参数相同
  3. 返回值相同
  4. 两个函数必须都是虚函数

满足这些,子类就重写了父类的虚函数。

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

class Student : public Person
{
public:
    // 重写父类虚函数
    virtual void BuyTicket()
    {
        cout << "买票-半价" << endl;
    }
};

特别注意

子类重写时,不加 virtual 也能构成重写,因为虚函数特性会被继承。

但规范写法必须加 virtual!


4. 多态使用示例

void Func(Person* ptr)
{
    // 多态调用:看指向的对象,不看指针类型
    ptr->BuyTicket();
}

int main()
{
    Person ps;
    Student st;

    Func(&ps); // 全价
    Func(&st); // 半价
    return 0;
}

结果:

买票-全价
买票-半价

这就是多态:同一个函数调用,指向不同对象,执行不同逻辑


5. 动物叫声示例(经典)

class Animal
{
public:
    virtual void talk() const
    {}
};

class Dog : public Animal
{
public:
    virtual void talk() const
    {
        cout << "汪汪" << endl;
    }
};

class Cat : public Animal
{
public:
    virtual void talk() const
    {
        cout << "喵~" << endl;
    }
};

void letsHear(const Animal& animal)
{
    animal.talk();
}

int main()
{
    Cat cat;
    Dog dog;

    letsHear(cat); // 喵
    letsHear(dog); // 汪汪
    return 0;
}

四、多态中的重点坑点

1. 析构函数的重写(面试必考,超重要)

父类析构函数写成虚函数,子类析构函数就会自动构成重写。

为什么?
编译器会把所有析构函数名都处理成 destructor(),所以名字统一了。

为什么要这样做?
避免内存泄漏!如果父类析构不是虚函数,那么当使用父类Person的指针ps指向子类Student对象st后,如果delete释放ps,此时编译器只知道ps是父类,不知道有多态,就会只调用父类析构,那么子类如果有堆上的资源就会造成内存泄漏,只有将父类写成虚函数,子类析构才能自动构成重写,此时编译器就会按照正常顺序析构子类后析构父类!!!

class A
{
public:
    virtual ~A()
    {
        cout << "~A()" << endl;
    }
};

class B : public A
{
public:
    ~B()
    {
        cout << "~B()" << endl;
        delete _p;
    }
protected:
    int* _p = new int[10];
};

使用:

int main()
{
    A* p2 = new B;
    delete p2;

    // 如果父类析构不是虚函数,只会调用~A(),造成泄漏!
    // 是虚函数,就会先调用~B() 再 ~A()
    return 0;
}

结论:基类析构函数尽量写成虚函数!


2. 协变(了解)

重写时返回值可以不同:

  • 父类返回:父类指针/引用
  • 子类返回:子类指针/引用

这叫协变,实际很少用,了解即可。


3. override / final 关键字(C++11)

(1)override

检查子类函数是否成功重写,写错直接编译报错。

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

class Benz : public Car
{
public:
    // 写错会直接报错!
    virtual void Drive() override
    {
        cout << "Benz-舒适" << endl;
    }
};

(2)final

  • 修饰类:该类不能被继承
  • 修饰虚函数:该函数不能被重写
class Car
{
public:
    virtual void Drive() final
    {}
};

// 报错:不能重写final函数
class Benz : public Car
{
    virtual void Drive()
    {}
};

4. 重载 / 重写 / 隐藏 对比(面试必考)

我给你总结成最清晰表格:

概念 作用域 函数名 参数 返回值 关键字
重载 同一类 相同 不同 随意
重写 父子类 相同 相同 相同 virtual
隐藏 父子类 相同 随意 随意

口诀:

  • 同一作用域、参数不同 → 重载
  • 父子类、都是虚函数、完全相同 → 重写
  • 父子类、同名、不是重写 → 隐藏

五、纯虚函数 和 抽象类

1. 纯虚函数

虚函数后面写 =0,就是纯虚函数。

virtual void Drive() = 0;

2. 抽象类

包含纯虚函数的类,叫抽象类

抽象类不能实例化对象!

子类必须重写纯虚函数,否则子类也是抽象类。

class Car
{
public:
    // 纯虚函数 → 抽象类
    virtual void Drive() = 0;
};

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

使用:

// Car car; 报错!
Car* pBenz = new Benz;
pBenz->Drive();

意义:强制子类重写,规范接口。


六、多态的原理(最难!)

1. 虚函数表指针(_vfptr)

只要类里有虚函数,对象中就会多一个指针:_vfptr 虚表指针

这个指针指向一张表:虚函数表(vftable)

我们看大小:

class Base
{
public:
    virtual void Func1()
    {
        cout << "Func1()" << endl;
    }
protected:
    int _b = 1;
    char _ch = 'x';
};

// 32位平台:4+1+3(对齐)+4(虚表指针) = 12字节
cout << sizeof(Base) << endl; // 输出12

2. 多态底层原理

多态调用时:

  1. 去对象中取 _vfptr
  2. 通过虚表指针找到 虚函数表
  3. 在表中找到对应 虚函数地址
  4. 调用函数

这就是运行时确定,也就是动态多态。

  • 父类对象 → 用父类虚表 → 调用父类虚函数
  • 子类对象 → 用子类虚表 → 调用子类虚函数

一句话:多态是靠虚表指针 + 虚函数表 + 重写实现的!


3. 静态绑定 vs 动态绑定

  • 静态绑定:编译时确定地址(普通函数)
  • 动态绑定:运行时查虚表确定(多态)

满足多态条件就是动态绑定,否则静态绑定。


4. 虚表存放位置

  • 虚函数:代码段
  • 虚表:代码段(常量区)
  • 虚表指针:在对象内部(栈/堆)

七、多态知识点总结

  1. 多态是父类指针指向不同对象,同样函数调用产生不同行为
  2. 多态条件:虚函数 + 重写 + 指针/引用
  3. 虚函数:virtual + 成员函数声明
  4. 重写:父子类关系、父类是虚函数、函数声明完全相同
  5. 析构函数建议全部写成虚函数,防止造成内存泄漏
  6. override 的作用是检查重写,final 的作用是禁止重写/继承
  7. 纯虚函数就是虚函数声明后面加上 = 0 → 有纯虚函数的类是抽象类,不能实例化
  8. 多态底层:虚表指针 + 虚函数表
  9. 动态绑定:运行时查虚表;静态绑定:编译时确定

那么今天关于C++ 多态就全部讲完了,内容非常多、非常细,大家一定要多画图、多敲代码。

有什么不懂欢迎私信问我,我会及时做出解答!
下一篇我们开始学习二叉搜索树,敬请期待吧!

bye~

更多推荐