【C++】多态门(上):多态的概念、虚函数、析构函数的重写、override和final关键字

📌 相关专栏

很高兴你点开这篇文章✨

这里会持续更新更多有用的内容,关注我,一起慢慢变好呀

👍 点赞 ⭐ 收藏 💬 评论


文章目录

  • 前言
  • [1. 多态的概念与分类](#1. 多态的概念与分类)
    • [1.1 编译时多态(静态多态)](#1.1 编译时多态(静态多态))
    • [1.2 运行时多态(动态多态)](#1.2 运行时多态(动态多态))
  • [2. 虚函数与重写(覆盖)](#2. 虚函数与重写(覆盖))
    • [2.1 虚函数的定义](#2.1 虚函数的定义)
    • [2.2 虚函数的重写(覆盖)](#2.2 虚函数的重写(覆盖))
  • [3. 多态实现的三个必要条件](#3. 多态实现的三个必要条件)
  • [4. 虚函数重写的特殊情况](#4. 虚函数重写的特殊情况)
    • [4.1 协变](#4.1 协变)
    • [4.2 析构函数的重写(重点)](#4.2 析构函数的重写(重点))
      • [🐾 两大核心知识点](#🐾 两大核心知识点)
  • [5. override 与 final 关键字](#5. override 与 final 关键字)
    • [5.1 override:检测是否重写](#5.1 override:检测是否重写)
    • [5.2 final:禁止重写](#5.2 final:禁止重写)
  • [6. 重载、重写、隐藏的对比](#6. 重载、重写、隐藏的对比)
    • [6.1 代码示例](#6.1 代码示例)
  • [7. 经典面试题分析](#7. 经典面试题分析)
    • 两大考点:
    • [7.1 题目1:默认参数与多态的坑](#7.1 题目1:默认参数与多态的坑)
    • [7.2 题目2:多层继承](#7.2 题目2:多层继承)
  • [8. 完整测试用例](#8. 完整测试用例)
    • [8.1 基础多态测试](#8.1 基础多态测试)
    • [8.2 虚析构测试](#8.2 虚析构测试)
    • [8.3 override 检测测试](#8.3 override 检测测试)
  • [9. 总结](#9. 总结)

前言

同样一句"发出叫声",狗会汪汪、猫咪会喵喵,同一个指令,不同对象表现不同行为,这就是现实里的多态。映射到编程中,多态即一个接口,多种实现,依托继承、方法重写、父类引用指向子类三大条件实现。

🐶 🐾 ✨ 🐾 🐶


1. 多态的概念与分类

🐾 多态的概念多态(Polymorphism)是面向对象程序设计的三大特性之一,顾名思义就是 "多种形态"。

🐾 多态的分类

类型 别名 实现方式 绑定时机
编译时多态 静态多态 函数重载、函数模板 编译阶段
运行时多态 动态多态 虚函数 + 继承 运行阶段

🐾类比

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

同一操作(买票),不同对象表现出不同行为 -> 这就是多态。


1.1 编译时多态(静态多态)

通过函数重载或函数模板实现,编译时就能确定调用哪个函数

cpp 复制代码
// 函数重载示例
void add(int a, int b);        // add(1, 2)
void add(int a, int b, int c); // add(1, 2, 3, 4)

1.2 运行时多态(动态多态)

通过基类指针/引用 + 虚函数重写实现,运行时根据实际对象类型确定调用哪个函数

cpp 复制代码
// 基类指针指向不同的对象
Person* p1 = new Person;    // 调用 Person::BuyTicket()
Person* p2 = new Student;   // 调用 Student::BuyTicket()

🐶 🐾 ✨ 🐾 🐶


2. 虚函数与重写(覆盖)

2.1 虚函数的定义

在类成员函数前加virtual关键字,该函数即为虚函数:

cpp 复制代码
class Person
{
public:
    virtual void BuyTicket()   // 虚函数:把BuyTicket变成虚函数,即需要参与多态
    {
        cout << "买票-全价" << endl;
    }
};

注意 :非成员函数和静态成员函数不能加 virtual。


2.2 虚函数的重写(覆盖)

派生类中定义与基类虚函数完全一致的函数即为重写:

cpp 复制代码
class Student : public Person
{
public:
    // 重写基类虚函数:函数名、参数、返回值必须完全一致
    // 派生类中的 virtual 可以省略(但不建议提高可读性)
    virtual void BuyTicket()
    {
        cout << "买票-打折" << endl;
    }
};

重写的条件 :

  • 函数名相同
  • 参数列表相同(类型、个数、顺序必须一致)
  • 返回类型相同(协变除外)

🐶 🐾 ✨ 🐾 🐶


3. 多态实现的三个必要条件

cpp 复制代码
void Func(Person* ptr)   // 条件1:基类指针(或引用)
{
    ptr->BuyTicket();    // 条件2:调用虚函数
}

int main()
{
    Person p;
    Student s;
    
    Func(&p);   // 指向父类对象 -> 调用父类版本
    Func(&s);   // 指向子类对象 -> 多态,调用子类版本
    
    // 不满足条件1:子类指针调用,不会触发多态
    Student* pst = &s;
    pst->BuyTicket();   // 静态调用,不是多态
    
    return 0;
}

三个必要条件

  1. 要有继承关系,即必须有基类和派生类
  2. 必须是基类的指针或者引用调用虚函数
  3. 被调用的函数必须是虚函数,且派生类完成了重写

🐾 说明

  • 要实现多态的效果,第一必须是基类的指针或者引用,因为只有基类的指针或引用才能既指向基类对象又指向派生类对象(派生类对象通过切割可赋值给基类的指针/基类的引用);
  • 第二派生类必须对基类的虚函数完成重写/覆盖,重写了虚函数,基类和派生类之间才能有不同的函数,多态的不同形态效果才能达到。

🐶 🐾 ✨ 🐾 🐶


4. 虚函数重写的特殊情况

  • 协变
  • 析构函数的重写(重点)

4.1 协变

派生类重写基类虚函数时,返回值类型可以不同,但必须满足:

  • 基类函数返回基类对象的指针/引用
  • 派生类虚函数返回派生类对象的指针/引用
cpp 复制代码
//基类A
class A {};
//派生类B(继承A)
class B : public A {};

class Person			//基类person
{
public:
    virtual A* BuyTicket()   // 返回基类A指针
    {
        cout << "买票-全价" << endl;
        return nullptr;
    }
};

class Student : public Person
{
public:
    virtual B* BuyTicket()   // 返回派生类B指针(协变)
    {
        cout << "买票-打折" << endl;
        return nullptr;
    }
};

4.2 析构函数的重写(重点)

基类的析构函数为虚函数时,派生类的析构函数自动构成重写(无论是否加 virtual)。

🐾 两大核心知识点

  1. 派生类析构(固定顺序---全局通用)

    先析构派生自身->自动析构基类

    • 普通派生对象 B:析构: B()->A()
    • 虚析构+基类指针 delete 派生对象:同样遵循 ~B()-> ~A()
  2. 为什么基类析构必须加"virtual"?

    • 不加virtualA* ptr = new B; delete ptr; 只调用基类~A(),派生类 _p动态内存无法释放->内存泄漏
    • 加virtual虚析构:触发多态,自动调用派生类析构,再链式调用基类析构,彻底释放资源

🐾 易错点

  1. 派生类析构重写:不写virtual也自动是虚函数
  2. 只要存在基类指针/引用管理派生类堆对象,基类析构必须加 virtual
cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;

class A
{
public:

    // 基类析构函数加virtual,支持重写:实现多态析构
    virtual ~A()
    {
        cout << "~A()" << endl;
    }
};

class B : public A
{

public:

    // 派生类析构函数:自动构成重写(加不加virtual都可以)
    ~B()
    {
        cout << "~B()->delete:" << _p << endl;

        // 释放派生类动态申请的资源
        delete[] _p;
    }
protected:
    int* _p = new int[10];                  // 派生类动态申请的数组
};


void test()
{
    cout << "-------*******---------" << endl;       //复习派生类的析构函数

    //析构顺序:~B(),~A(),~A()
    //第一个~A()是子类B析构完后调用基类的(先子后父)
    //第二个~A()是a对象析构
    A a;                //普通基类对象
    B b;                //普通派生类对象
    //局部对象出作用域后自动析构,无需手动delete
}
// 基类只要保障了析构函数是虚函数,下面场景就不会存在内存泄漏
int main()
{

    A* ptr1 = new A;                // 基类指针指向派生类对象          
    delete ptr1;                    // 调用~A()


    A* ptr2 = new B;                 // 基类指针指向基类对象
    delete ptr2;                     // 多态调用:先调用~B(),再自动调用~A(),释放子类动态内存,无内存泄漏

    test();
    return 0;
}

🐶 🐾 ✨ 🐾 🐶


5. override 与 final 关键字

5.1 override:检测是否重写

  • 在派生类虚函数后加 override,编译器会检查该函数是否真的重写了基类虚函数;
  • 若未重写(如函数名错、参数错),直接编译报错
cpp 复制代码
class Car
{
public:
 // 基类虚函数:Drive(注意拼写是Drive,不是Dirve)
    virtual void Drive()
    {
        cout << "Car-行驶" << endl;
    }
};

class Benz : public Car
{
public:
    //  错误:函数名写错(是Drive,不是Dirve),编译报错
    virtual void Dirve() override
    {
        cout << "Benz-舒适" << endl;
    }
    
    //  正确:override 检测通过
    virtual void Drive() override
    {
        cout << "Benz-舒适" << endl;
    }
};

5.2 final:禁止重写

  • 在基类虚函数后加"final",表示该虚函数不允许任何派生类重写;
  • 若派生类强行重写,编译报错
cpp 复制代码
class Car
{
public:
 	// 基类虚函数加final:禁止派生类重写
    virtual void Drive() final   // 禁止重写
    {
        cout << "Car-行驶" << endl;
    }
};

class Benz : public Car
{
public:
    //  错误:无法重写 final 函数
    virtual void Drive() override
    {
        cout << "Benz-舒适" << endl;
    }
};

🐶 🐾 ✨ 🐾 🐶


6. 重载、重写、隐藏的对比

特性 重载 重写(覆盖) 隐藏
作用域 同一作用域 基类与派生类(不同作用域) 基类与派生类(不同作用域)
函数名 相同 相同 相同
参数 不同 相同(协变除外) 相同或不同
返回值 可不同 相同(协变除外) 可不同
virtual 关键字 不需要 基类必须 virtual 不需要
关系 编译时多态 运行时多态 名字屏蔽

6.1 代码示例

cpp 复制代码
class Base
{
public:
    // 重载:同一作用域
    virtual void func(int a) { cout << "base::func(int)" << endl; }
    void func(double b)      { cout << "base::func(double)" << endl; }
    
    // 虚函数:用于重写
    virtual void show() { cout << "base::show()" << endl; }
};

class Derive : public Base
{
public:
    // 重写:虚函数 + 参数相同
    virtual void show() override { cout << "derive::show()" << endl; }
    
    // 隐藏:函数名相同但不构成重写
    void func(int a, int b) { cout << "derive::func(int,int)" << endl; }
};

int main()
{
    Derive d;
    d.func(1, 2);      // 调用 Derive::func(隐藏了基类的 func)
    // d.func(3);      // 错误:基类的 func(int) 被隐藏
    d.Base::func(3);   // 显式调用基类版本
    
    Base* p = &d;
    p->show();         // 多态:调用 Derive::show(重写)
}

🐶 🐾 ✨ 🐾 🐶


7. 经典面试题分析

两大考点:

  1. 虚函数重写:函数本体运行看"对象真实类型"
  2. 默认参数:编译器静态绑定,看"指针类型",不参与多态

7.1 题目1:默认参数与多态的坑

cpp 复制代码
class A
{
public:
    virtual void func(int val = 1)
    {
        cout << "A->" << val << endl;
    }
    
    virtual void test()
    {
        func();   // this 类型是 A*
    }
};

class B : public A
{
public:
    void func(int val = 0)   // 重写 func
    {
        cout << "B->" << val << endl;
    }
};

int main()
{
    A* p = new B;
    p->test();   // 输出:B->1
}

🐾 解析:

  1. B 没有重写 test,调用 A::test()
  2. A::test() 中 this->func(),this 编译类型是 A*(满足多态)
  3. 运行时 this 指向 B 对象 -> 多态调用 B::func
  4. 默认参数编译时绑定,使用 this 的编译类型 A* -> 默认值 = 1
  5. 最终输出B->1

虚函数的默认参数是静态绑定,函数体是动态绑定。


7.2 题目2:多层继承

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;

class A
{
public:

    //虚函数,默认参数静态绑定(编译看指针类型)
    virtual void f(int x = 1)
    {
        cout << "A->" << x << endl;
    }

    //虚函数g,内部调用f()
    virtual void g()
    {
        f();
    }
};

//派生类B 公有继承A
class B : public A
{
public:
    
    //重写虚函数f,默认参数单独定义,不参与多态
    void f(int x = 2)
    {
        cout << "B->" << x << endl;
    }

    //重写虚函数g
    void g()
    {
        f();            //内部执行this->f(),这里的this变异类型为B*
                        //1.多态:运行看真实对象,调用最终重写的f
                        //2.默认参数:静态绑定,取用B类默认值 x=2
    }

};

//派生类C 公有继承B
class C : public B
{
public:

    //再次重写虚函数f,覆盖父类所有f
    void f(int x = 3)
    {     
        cout << "C->" << x << endl;         //C没有重写 g,继承B的 g
    }
};

int main()
{
    B* p = new C;           /*
                                编译类型B* ,实际指向C对象;
                                1.C没有g,继承B的g
                                2.B::g内部f()->多态调用C::f,默认参数取B的2

                                最终输出:C->2
                             */
    p->g();
}

🐾 解析:

本题是基于上面的题目来进行拓展的

  1. 继承关系:A->B->C

  2. 所有类都重写了虚函数 f;B 重写了 g ;C 没有重写 g

  3. 执行流程:B* p = new C;p->g();

    • 指针编译类型B*,指向实际对象C
    • C没有重写 g ,调用B::g()
    • B::g()内部执行:this->f(),这里的B的编译类型是:B*
      1. 多态规则:运行看真实对象,调用 C::f

      2. 默认参数规则:默认值编译绑定、看指针类型, this 是B*,用B的默认参数 x=2

  4. 最终:调用C的f函数,传入默认值2 → 输出 C->2

🐶 🐾 ✨ 🐾 🐶


8. 完整测试用例

8.1 基础多态测试

cpp 复制代码
void Test1()
{
    Person p;
    Student s;
    
    Func(&p);   // 买票-全价
    Func(&s);   // 买票-打折(多态)
    
    Person* ps = &s;
    ps->BuyTicket();   // 买票-打折
}

8.2 虚析构测试

cpp 复制代码
void Test2()
{
    A* ptr1 = new A;
    delete ptr1;   // ~A()
    
    A* ptr2 = new B;
    delete ptr2;   // ~B() -> ~A()(多态析构)
}

8.3 override 检测测试

cpp 复制代码
void Test3()
{
    Car* p = new Benz;
    p->Drive();   // 输出:Benz-舒适
}

🐾 正确示例

🐶 🐾 ✨ 🐾 🐶


🐾 错误示例

🐶 🐾 ✨ 🐾 🐶


9. 总结

🐾 C++ 多态的核心知识点:

  • 多态分类: 编译时多态(重载/模板)、运行时多态(虚函数)

  • 虚函数重写 :函数名、参数、返回值相同(协变除外)

  • 三个必要条件: 继承 + 基类指针/引用 + 虚函数重写

  • 协变: 返回值可以是基类/派生类指针或引用

  • 虚析构 :基类析构必须加 virtual,否则可能内存泄漏

  • override: 检查是否成功重写

  • final :禁止派生类重写

  • 默认参数坑 :静态绑定,多态时使用基类默认值


🐾 多态实现总结

  1. 必须是基类的指针或引用(才能指向不同的对象类型)
  2. 派生类必须对基类的虚函数完成重写(才能有不同的行为)

🐾 建议

  • 多态场景下,基类析构函数必须声明为 virtual
  • 派生类重写虚函数时,建议加上 override 关键字
  • 理解默认参数的静态绑定规则,避免踩坑

🐶 🐾 ✨ 🐾 🐶


本文全部代码

https://gitee.com/ayidyy/cyvyan11/blob/6958ca3cb3cf1536247f0be5f38a8d8a4732e4ce

  1. 欢迎留言交流
  2. 期待你的评论与建议
  3. 留下你的想法吧

谢谢你看到这里呀

如果喜欢这篇内容,点个关注,下次更新不迷路✨

👍 点赞 ⭐ 收藏 💬 评论