在 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() 时就会自动执行各自的扣款逻辑。以后就算接入新的支付方式,结算循环也无需改动,我们只需要新增一个派生类就可以了。
