C++ 多态

在 C++ 中,多态(Polymorphism)是面向对象编程的三大特性之一,另外两个是封装和继承。多态指的是当我们对不同的对象执行同一个函数调用时,程序会表现出不同的行为,简单来说就是 “一个接口,多种实现”。

C++ 多态是什么?

其实在前面的章节中,我们已经接触过一种简单的多态:比如使用函数重载时,编译器在编译阶段就能根据实参类型确定调用哪个函数,这种形式通常称为 “静态多态”。

而在面向对象编程中,我们平时说的多态主要是指运行时的 “动态多态”。基类先声明虚函数来规定统一的函数接口,各个派生类再编写各自专属的实现逻辑。

比如在游戏里定义一个英雄基类 Hero 并提供 skill() 虚函数,传入战士 Warrior 对象时发动近战斩击,传入法师 Mage 对象时施放火焰法术。结合前面几节的内容,实现动态多态需要同时满足以下 3 个条件:

  • 存在继承关系:多个派生类公有继承自同一个基类,比如 Warrior 和 Mage 都继承 Hero。
  • 完成虚函数重写:基类将成员函数声明为 virtual,并且派生类按相同格式完成了函数重写。
  • 通过基类引用或指针调用:使用基类的引用(如 Hero&)或基类的指针(如 Hero*)去调用该虚函数。

C++ 多态的实现

在实际开发中,实现动态多态主要有两种常见方式。一种是将函数形参声明为基类引用,使用同一个函数接收不同的子类对象。另一种是使用基类指针,把多个不同的子类对象放进同一个数组或容器里集中管理。

1. 通过基类引用实现多态

下面我们先定义一个 Hero 基类,再定义 Warrior 和 Mage 两个派生类,然后编写一个接收 Hero& 基类引用的 castSkill() 函数。

示例 1:统一调用不同英雄技能

#include <iostream>

class Hero {
public:
    virtual void skill() {
        std::cout << "英雄释放普通技能\n";
    }
};

class Warrior : public Hero {
public:
    void skill() {
        std::cout << "战士发动近战斩击\n";
    }
};

class Mage : public Hero {
public:
    void skill() {
        std::cout << "法师施放火焰法术\n";
    }
};

// 接收 Hero 基类引用
void castSkill(Hero& hero) {
    hero.skill();
}

int main() {
    Warrior warrior;
    Mage mage;

    castSkill(warrior);
    castSkill(mage);
    return 0;
}

运行结果如下。

战士发动近战斩击
法师施放火焰法术

分析:

在这个例子中,函数 castSkill(Hero& hero) 的形参只有一个基类引用 Hero&,我们并没有分别为 Warrior 和 Mage 编写两个不同的函数。

第一次调用 castSkill(warrior) 时,hero 绑定的是 Warrior 对象,自动输出 “战士发动近战斩击”。第二次调用 castSkill(mage) 时,hero 绑定的是 Mage 对象,自动输出 “法师施放火焰法术”。

这就是多态最大的好处:castSkill() 内部不用写一堆分支语句去判断英雄类型,只要是 Hero 的派生类就能自动执行对应的 skill()。以后哪怕再新增一个弓箭手 Archer 类,castSkill() 也一行代码都不用改。

2. 通过基类指针实现多态

除了基类引用之外,基类指针同样可以作为多态的统一入口。特别是当我们手里有多个不同的派生类对象,想把它们放进同一个数组里批量处理时,因为 C++ 数组不能直接存放引用,使用基类指针数组就会非常方便。

示例 2:基类指针数组批量调用

#include <iostream>

class Hero {
public:
    virtual void skill() {
        std::cout << "英雄释放普通技能\n";
    }
};

class Warrior : public Hero {
public:
    void skill() {
        std::cout << "战士发动近战斩击\n";
    }
};

class Mage : public Hero {
public:
    void skill() {
        std::cout << "法师施放火焰法术\n";
    }
};

int main() {
    Warrior warrior;
    Mage mage;

    // 保存不同派生类对象的地址
    Hero* team[2] = {&warrior, &mage};

    for (Hero* member : team) {
        member->skill();
    }
    return 0;
}

运行结果如下。

战士发动近战斩击
法师施放火焰法术

分析:

Hero* team[2] = {&warrior, &mage}; 这句代码定义了一个 Hero* 类型的基类指针数组,用来保存两个不同子类对象的内存地址。

普通数组要求所有元素的类型完全相同,我们不能直接把 Warrior 和 Mage 对象本身塞进同一个数组,但把它们的地址存进 Hero* 指针数组就没有问题了。

在循环遍历时,虽然每次取出的指针 member 表面类型都是 Hero*,但执行 member->skill() 时,程序会根据指针当前实际指向的对象去调用对应的重写函数,依次输出战士和法师的技能。

提示 给函数传递单个对象时,我们推荐优先使用基类引用 Hero&,写法简洁且不用担心空指针问题。如果需要把多个子类对象放进数组或容器里批量管理,此时就应该使用基类指针 Hero*。

为什么多态不能按值传参?

看到前面的 castSkill(Hero& hero),很多初学的小伙伴会感到好奇:既然 Warrior 本身也是一种 Hero,直接把函数形参写成普通对象 Hero hero,不使用引用或指针,行不行呢?

实际上是不行的。一旦采用值传递接收参数,函数不会直接操作原来的子类对象,而是会根据形参类型 Hero 重新复制出一个基类对象。在这个复制过程中,子类特有的成员和子类身份都会被直接 “切掉”,只留下基类 Hero 的那一部分。

这种现象在 C++ 中称为 “对象切片(Object Slicing)”,会导致多态直接失效。下面我们来看一个例子。

示例 3:按值传参导致对象切片

#include <iostream>

class Hero {
public:
    virtual void skill() {
        std::cout << "英雄释放普通技能\n";
    }
};

class Warrior : public Hero {
public:
    void skill() {
        std::cout << "战士发动近战斩击\n";
    }
};

// 按值接收,发生对象切片
void castSkill(Hero hero) {
    hero.skill();
}

int main() {
    Warrior warrior;
    castSkill(warrior);
    return 0;
}

运行结果如下。

英雄释放普通技能

分析:

从运行结果可以看出,我们传入的明明是 Warrior 对象 warrior,输出的却是基类的 “英雄释放普通技能”。

这是因为 void castSkill(Hero hero) 漏掉了引用符号 &。传参时程序把 warrior 切成了纯正的 Hero 基类对象 hero,它的真实身份已经不再是 Warrior 了,调用 hero.skill() 自然只会执行基类的版本。

因此在编写多态函数时,小伙伴们一定要分清下面两种写法的区别:

// 正确方式
void castSkill(Hero& hero);

// 错误方式
void castSkill(Hero hero);

C++ 多态的实际应用

多态在实际项目开发中应用非常广泛,能够让代码更容易扩展和维护。比如在商城收银系统中,我们可以把不同渠道的待支付账单地址存进同一个 vector 容器中,通过一套循环代码完成统一结算。

示例 4:批量结算不同支付账单

#include <iostream>
#include <vector>

// 支付基类
class Payment {
public:
    double amount;

    Payment(double a) : amount(a) {}

    virtual void pay() {
        std::cout << "支付:" << amount << " 元\n";
    }
};

// 支付宝支付
class Alipay : public Payment {
public:
    Alipay(double a) : Payment(a) {}

    void pay() {
        std::cout << "支付宝支付:" << amount << " 元\n";
    }
};

// 微信支付
class WechatPay : public Payment {
public:
    WechatPay(double a) : Payment(a) {}

    void pay() {
        std::cout << "微信支付:" << amount << " 元\n";
    }
};

// 储值卡支付(享 9 折)
class MemberCardPay : public Payment {
public:
    MemberCardPay(double a) : Payment(a) {}

    void pay() {
        std::cout << "储值卡支付(9 折):" << amount * 0.9 << " 元\n";
    }
};

int main() {
    Alipay order1(100.0);
    WechatPay order2(50.0);
    MemberCardPay order3(200.0);

    std::vector<Payment*> orders = {&order1, &order2, &order3};

    for (Payment* item : orders) {
        item->pay();
    }
    return 0;
}

运行结果如下。

支付宝支付:100 元
微信支付:50 元
储值卡支付(9 折):180 元

分析:

在上面代码中,基类 Payment 保存了订单金额 amount 并提供了虚函数 pay()。Alipay、WechatPay 和 MemberCardPay 继承 Payment 后各自重写了 pay(),其中储值卡支付会自动按 9 折计算金额。

在 main() 函数中,我们将 3 笔账单的地址放进 std::vector<Payment*> 容器,遍历调用 item->pay() 时就会自动执行各自的扣款逻辑。以后就算接入新的支付方式,结算循环也无需改动,我们只需要新增一个派生类就可以了。

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号