C++第十四篇:多态

目录

一、多态的实现

二、静态多态和动态多态

1. 静态多态(编译时多态)

2. 动态多态(运行时多态)

三、虚函数

四、虚函数表和虚函数指针

五、虚析构函数


在 C++ 中,多态是面向对象编程的核心特性之一,指 “同一接口,不同实现”—— 通过基类的接口调用时,能根据对象的实际类型执行不同的派生类实现。多态让代码更灵活、可扩展,是实现 “开闭原则”(对扩展开放,对修改关闭)的重要手段。

说白话,多态就是C++的一种特点,那它具体怎么提现,我简单举一个例子,让大家能理解多态的。

还是那王者**来说,王者**有那么多英雄,我们假设想要构建游戏中的角色,在C++这样以面向对象的编程中是怎样来实现的呢?直接说思路,首先创建一个基类叫英雄类,所有的英雄都会继承这个基类,这个类中有英雄最基本的属性,血量、蓝条、攻击范围等。然后再去实现不同的派生类。派生类就是对应的具体英雄,如齐天大圣、诸葛亮等。

那这里什么地方提现了多态呢?现在解释会点空,还是先看例程。

一、多态的实现

#include <iostream>
#include <string>
using namespace std;

// 英雄基类
class Hero {
protected:
    string name; // 英雄名称
public:
    Hero(string n) : name(n) {}
    
    // 纯虚函数:强制子类重写语音方法
    virtual void sayVoice() = 0;
};

// 子类1:亚瑟
class Arthur : public Hero {
public:
    Arthur() : Hero("亚瑟") {}
    
    // 重写语音方法(亚瑟专属语音)
    void sayVoice() override {
        cout << name << ":为了父王的荣耀!" << endl;
    }
};

// 子类2:后羿
class HouYi : public Hero {
public:
    HouYi() : Hero("后羿") {}
    
    // 重写语音方法(后羿专属语音)
    void sayVoice() override {
        cout << name << ":太阳,终将照耀大地!" << endl;
    }
};

// 子类3:安琪拉
class AnQiLa : public Hero {
public:
    AnQiLa() : Hero("安琪拉") {}
    
    // 重写语音方法(安琪拉专属语音)
    void sayVoice() override {
        cout << name << ":玩火,是会烧到自己的哦~" << endl;
    }
};

// 多态调用
void play(Hero* hero) {
    hero->sayVoice(); // 同一接口,执行不同英雄的语音
}

int main() {
    Hero* hero1 = new Arthur();
    Hero* hero2 = new HouYi();
    Hero* hero3 = new AnQiLa();
    
    // 自动匹配对应英雄的语音
    play(hero1);
    play(hero2);
    play(hero3);
    
    delete hero1;
    delete hero2;
    delete hero3;
    return 0;
}

结果如下:

这段代码就是一个多态的实现,基类是英雄类,然后不同的英雄都会继承这个类。

多态首先需要创建子类的对象,然后将它的基类指针指向派生类的地址。即:

Hero* hero1 = new Arthur();    //创建亚瑟的对象,然后让基类指向它

为什么要这样写呢,我们来看代码中的play函数。

代码中的play函数实际就是一个打印每个角色的语音的函数,但是每个角色的派生类都不一样,那这个参数类型应该怎样去给呢?。。。。。这里就可以使用多态,直接将类型定为他它的一个基类指针,让这个子类的指针在创建的时候就直接指向它的派生类。那么后面调用直接调用这个HERO基类的指针,不同的英雄它的类型是一样的,但是内部的实现却不一样。这就是多态机制。

二、静态多态和动态多态

1. 静态多态(编译时多态)

  • 定义:指在编译阶段就确定具体调用哪个函数的多态行为。
  • 实现方式:主要通过函数重载(Overloading)和运算符重载实现。
    • 函数重载:在同一作用域内,允许存在多个同名函数,但它们的参数列表(参数类型、个数或顺序)不同,编译器会根据实参类型在编译时匹配对应的函数。
    • 运算符重载:对已有的运算符(如+<<等)重新定义,使其能作用于自定义类型,编译器在编译时根据操作数类型确定调用的重载版本。
  • 特点:依赖编译器的静态绑定,效率较高,但灵活性较低,行为在编译时就已确定。

2. 动态多态(运行时多态)

  • 定义:指在程序运行阶段才确定具体调用哪个函数的多态行为。
  • 实现方式:主要通过虚函数继承实现。
    • 当子类重写(Override)父类的虚函数时,通过父类指针或引用指向子类对象,调用虚函数时,程序会在运行时根据对象的实际类型(而非指针 / 引用的声明类型)确定调用子类还是父类的函数。
  • 特点:依赖动态绑定,灵活性高,能根据运行时对象的实际类型动态调整行为,但由于需要动态查找函数地址(如虚函数表),效率略低于静态多态。

三、虚函数

我们这个主要讲动态多态,因为动态和虚函数相关。

根据之前的代码中,我们看到了这样一段类中函数声明。

    // 纯虚函数:强制子类重写语音方法
    virtual void sayVoice() = 0;

这个函数声明的前面加了virtual,这是虚函数的关键词。为什么要加上这个呢,这是实现多态必要的,加上virtual才可以对该函数重写(不是重载),如果不加这个关键词,在派生类再写sayVoice()函数,这个叫做函数隐藏派生类会将基类函数名相同的函数隐藏,这个在上一篇中继承讲过,不熟悉可以看一下上一篇。

再看注释那里还写了纯虚函数。这又要说明了,纯虚函数和虚函数是不一样的。纯虚函数需要在后面后加上 “= 0”。

并且他有几个特点:

  • 声明纯虚函数后,该类会成为抽象类,无法被构建。
  • 要求子类必须重写该函数,否则子类也会成为抽象类,无法构建。

相比之下,虚函数就比较自由,可以选择重写或者不重写,同时也可以实例化。

以上就是虚函数基本概念和特点,但有一个问题就是虚函数的底层原理有是怎样的呢?

四、虚函数表和虚函数指针

在 C++ 中,动态多态的实现依赖于虚函数表 和虚函数指针,这是编译器在底层自动实现的机制。

虚函数表每个包含虚函数的类,编译器都会为其生成一个全局唯一的虚函数表。这是一个函数指针数组,存储了该类所有虚函数的地址(包括继承自基类且未被重写的虚函数,以及自身重写的虚函数)。

虚函数指针每个包含虚函数的类的对象,都会隐含一个虚函数指针,它指向该对象所属类的虚函数表。虚函数指针 是对象内存布局的一部分,由编译器自动添加,在对象构造时初始化(指向当前类的 虚函数表)。

就是说,每个包含虚函数的类和对应的派生类都会创建一个虚函数表,在对象创建后,对象中会保存一个指向虚函数表的指针叫虚函数指针

在虚函数表中保存了所有虚函数的函数指针。对于派生类的虚函数表,它的函数指针需要根据自己是否重写对应的虚函数来定,如果重写了,那对应的函数指针就会被更新为重写后的函数指针。没有重写,则还是存为原来基类的函数指针。

那这时再来看这个段代码,就应该能理解多态的底层原理了:

    Hero* hero3 = new AnQiLa();
    play(hero3);

当play中参数为hero3时,hero3保存的AnQiLa对象的指针,在内部程序调用的就会是调用派生类的虚函数表,派生类会对基类的虚函数进行重写,因而虚函数表中的函数指针会保存为重写后的函数指针,派生类play执行的时候就会调用这个重写后函数,从而实现多态。

五、虚析构函数

1、正常继承的析构顺序(非多态)

在正常的继承时,派生类继承基类在调用析构函数时,析构顺序是先派生类、再基类(与构造顺序相反)。

2.多态场景的问题(基类析构非虚函数)

当通过基类指针指向派生类对象,并删除指针时,此时编译器只根据指针的声明类型 调用析构函数,导致派生类的资源(如动态分配的内存等)无法释放,造成内存泄漏。

下面是一个简单示例:

#include <iostream>
#include <string>
using namespace std;

// 英雄基类(抽象类)
class Hero {
protected:
    string name; // 英雄名称
public:
    Hero(string n) : name(n) {
        cout << "基类Hero构造:" << name << endl;
    }
    
    // 基类析构必须声明为虚函数,确保多态下派生类析构被调用
    ~Hero() {
        cout << "基类Hero析构:" << name << endl;
    }
    
    // 纯虚函数:语音接口
    virtual void sayVoice() = 0;
};

// 唯一派生类:亚瑟
class Arthur : public Hero {
public:
    // 调用基类构造函数初始化名称
    Arthur() : Hero("亚瑟") {
        cout << "派生类Arthur构造" << endl;
    }
    
    // 派生类析构(自动成为虚函数)
    ~Arthur(){
        cout << "派生类Arthur析构" << endl;
    }
    
    // 重写语音方法
    void sayVoice() override {
        cout << name << ":为了父王的荣耀!" << endl;
    }
};

// 多态调用函数
void play(Hero* hero) {
    hero->sayVoice();
}

int main() {
    // 基类指针指向派生类对象
    Hero* hero = new Arthur();
    
    play(hero); // 调用派生类的语音实现
    
    // 删除基类指针时,会先调用派生类析构,再调用基类析构
    delete hero; 
    return 0;
}

结果如下:

我们发现,删除hero指针,仅仅只调用了基类的析构函数,并没有使用派生类的析构函数,这样就会导致内存的泄露。

解决的办法就是使用virtual来修饰基类的析构函数,这样编译器就会链式的的调用析构函数。

//修改点1,加上虚函数关键词    
virtual ~Hero() {
        cout << "基类Hero析构:" << name << endl;
    }
//修改点2
virtual ~Arthur() override{    // 这里的override、virtual可加,可不加,它重写了基类的虚函数,
        cout << "派生类Arthur析构" << endl;     // 本身就是虚函数,派生类也可重写它的虚函数
    }                                          // 这里只是显示声明

执行结果如下:

那么这些就是一下关于多态和虚函数的一些内容了,有问题的可以在评论区交流,共同进步。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值