作为编程小白,刚接触 C++ 的面向对象时,是不是被 “多态” 这个词搞得晕头转向?其实多态一点都不神秘,它就像生活中 “同一个行为,不同人做有不同结果” 的场景 —— 比如买票,普通人全价、学生半价、军人优先;再比如动物叫,猫 “喵喵”、狗 “汪汪”。今天就用最通俗的语言,结合实例带你吃透 C++ 多态的核心知识点!
一、先搞懂:多态到底是什么?
多态的本质是:继承关系下的不同类对象,调用同一个函数,却产生不同的行为。
举个生活例子:
- 基类(父类):
Person(普通人),有一个BuyTicket()(买票)函数,行为是 “全价”; - 派生类(子类):
Student(学生),继承自Person,重写BuyTicket()函数,行为是 “半价”; - 当用基类的指针或引用分别指向普通人对象和学生对象时,调用
BuyTicket()会自动执行对应的行为 —— 这就是多态的魅力!
二、实现多态的 2 个核心条件(缺一不可)
想让代码实现多态效果,必须满足以下两个条件,记牢啦!
条件 1:基类的指针或引用调用函数
为什么必须用指针 / 引用?因为只有基类的指针或引用,才能 “身兼数职”—— 既能指向基类对象,又能指向派生类对象。就像一个通用接口,能适配不同的 “子类对象”。
条件 2:被调用的函数是虚函数,且完成重写
这是多态的核心,我们拆成两步理解:
第一步:什么是虚函数?
在类的成员函数前加virtual关键字,这个函数就是虚函数。比如:
class Person {
public:
// 虚函数:加了virtual关键字
virtual void BuyTicket() {
cout << "普通人买票-全价" << endl;
}
};
⚠️ 注意:非成员函数(比如全局函数)不能加virtual,加了会报错!
第二步:什么是虚函数重写?
派生类中有一个和基类完全相同的虚函数(函数名、参数列表、返回值都一样),这就叫 “重写”(也叫覆盖)。比如学生类重写买票函数:
class Student : public Person {
public:
// 重写基类的虚函数:函数名、参数、返回值完全一致
virtual void BuyTicket() {
cout << "学生买票-半价" << endl;
}
};
⚠️ 小坑提醒:派生类重写时,就算不加virtual关键字,也能构成重写(因为继承了基类的虚函数属性),但这种写法不规范,千万别学!
三、手把手写第一个多态程序
结合上面的条件,我们写一个完整的例子,跑起来看看效果:
#include <iostream>
using namespace std;
// 基类:普通人
class Person {
public:
// 虚函数
virtual void BuyTicket() {
cout << "普通人买票-全价" << endl;
}
};
// 派生类:学生(继承自Person)
class Student : public Person {
public:
// 重写基类虚函数
virtual void BuyTicket() {
cout << "学生买票-半价" << endl;
}
};
// 派生类:军人(继承自Person)
class Soldier : public Person {
public:
// 重写基类虚函数
virtual void BuyTicket() {
cout << "军人买票-优先" << endl;
}
};
// 关键:用基类引用调用函数(满足条件1)
void DoBuyTicket(Person& people) {
people.BuyTicket(); // 调用哪个版本,由people指向的对象决定
}
int main() {
Person p; // 普通人对象
Student s; // 学生对象
Soldier sol; // 军人对象
DoBuyTicket(p); // 输出:普通人买票-全价
DoBuyTicket(s); // 输出:学生买票-半价
DoBuyTicket(sol); // 输出:军人买票-优先
return 0;
}
运行结果完全符合预期!这就是多态的魔力 —— 同一个DoBuyTicket函数,传入不同对象,自动执行不同行为。
四、多态的 “特殊情况”:协变与析构函数重写
除了上面的常规情况,还有两个实用的特殊场景,小白也得了解:
1. 协变(了解即可)
正常重写要求返回值完全一致,但有一种例外:基类虚函数返回基类指针 / 引用,派生类虚函数返回派生类指针 / 引用,这叫 “协变”,也能构成多态。比如:
class A {};
class B : public A {}; // B继承A
class Person {
public:
// 返回基类A的指针
virtual A* BuyTicket() {
cout << "普通人买票-全价" << endl;
return nullptr;
}
};
class Student : public Person {
public:
// 返回派生类B的指针(协变)
virtual B* BuyTicket() {
cout << "学生买票-半价" << endl;
return nullptr;
}
};
协变实际用得不多,知道有这个情况就行~
2. 析构函数的重写(重点!)
析构函数比较特殊:基类析构函数加virtual后,派生类析构函数不管加不加virtual,都算重写。
为什么要这么做?防止内存泄漏!比如:
class A {
public:
// 基类析构函数加virtual
virtual ~A() {
cout << "~A()" << endl;
}
};
class B : public A {
public:
B() {
_p = new int[10]; // 申请堆内存
}
// 派生类析构函数(自动重写基类虚析构)
~B() {
cout << "~B():释放内存" << endl;
delete[] _p; // 释放堆内存
}
private:
int* _p;
};
int main() {
A* p1 = new A;
A* p2 = new B; // 基类指针指向派生类对象
delete p1; // 输出:~A()(正常释放)
delete p2; // 输出:~B():释放内存 → ~A()(正确释放子类+基类)
return 0;
}
如果基类析构函数不加virtual,delete p2时只会调用基类~A(),子类的_p内存不会释放,就会造成内存泄漏!所以记住:基类析构函数尽量加 virtual。
五、C++11 新特性:override 和 final(避坑神器)
新手写重写时,很容易犯 “函数名写错”“参数漏写” 的错误,这些错误编译时不报错,运行时才出问题,排查起来很麻烦。C++11 提供了两个关键字,帮我们快速避坑:
1. override:检查是否重写成功
在派生类的虚函数后加override,编译器会自动检查是否真的重写了基类虚函数。如果没重写(比如函数名错了),直接编译报错!
class Student : public Person {
public:
// 函数名写错了(BuyTicket→BuyTicker),加override会编译报错
virtual void BuyTicker() override {
cout << "学生买票-半价" << endl;
}
};
2. final:禁止派生类重写
在基类的虚函数后加final,表示这个函数 “最终版”,派生类不能再重写它,否则编译报错:
class Person {
public:
// 加final,禁止子类重写
virtual void BuyTicket() final {
cout << "普通人买票-全价" << endl;
}
};
class Student : public Person {
public:
// 报错:不能重写final函数
virtual void BuyTicket() {
cout << "学生买票-半价" << endl;
}
};
六、纯虚函数与抽象类(强制子类实现接口)
有时候我们希望基类只定义 “接口”,不实现具体功能,强制子类必须实现这个接口 —— 这时候就需要 “纯虚函数” 和 “抽象类”。
1. 纯虚函数
在虚函数后加=0,就是纯虚函数,不需要写实现(写了也没用):
class Animal {
public:
// 纯虚函数:只声明,不实现
virtual void Sound() = 0; // 动物叫的接口
virtual void Walk() = 0; // 动物走的接口
};
2. 抽象类
包含纯虚函数的类,叫 “抽象类”。抽象类有两个特点:
- 不能直接实例化对象(比如
Animal a;会编译报错); - 派生类必须重写所有纯虚函数,否则派生类也是抽象类,不能实例化。
举个例子:
// 抽象类:Animal
class Animal {
public:
virtual void Sound() = 0;
virtual void Walk() = 0;
};
// 派生类:Dog(必须重写所有纯虚函数)
class Dog : public Animal {
public:
virtual void Sound() {
cout << "汪汪!" << endl;
}
virtual void Walk() {
cout << "四条腿跑" << endl;
}
};
// 派生类:Duck(必须重写所有纯虚函数)
class Duck : public Animal {
public:
virtual void Sound() {
cout << "嘎嘎!" << endl;
}
virtual void Walk() {
cout << "两条腿走" << endl;
}
};
int main() {
// Animal a; // 报错:抽象类不能实例化
Dog dog;
Duck duck;
dog.Sound(); // 汪汪!
duck.Walk(); // 两条腿走
return 0;
}
抽象类的作用就像 “模板”,强制所有子类遵循统一的接口,比如所有动物都必须有 “叫” 和 “走” 的功能,保证了代码的规范性。
七、多态的底层原理(小白浅尝即可)
最后聊聊多态的底层逻辑,不用深究,知道大概就行:
- 含有虚函数的类,每个对象都会多一个 “虚函数表指针”(简称
vfptr),这个指针指向一张 “虚函数表”(简称vtable); - 虚函数表是一个存储虚函数地址的数组,基类和派生类有各自的虚表;
- 派生类重写虚函数后,会用自己的函数地址覆盖虚表中对应的基类函数地址;
- 当用基类指针 / 引用调用虚函数时,会在运行时通过
vfptr找到对应的虚表,再找到函数地址 —— 这就是 “动态绑定”(运行时确定调用哪个函数); - 普通函数调用是 “静态绑定”(编译时就确定函数地址)。
简单说:多态是靠 “虚表指针 + 虚表” 实现的,运行时动态查找函数地址,才能做到 “一个调用,多种行为”。
八、总结:多态核心知识点速记
- 多态本质:继承下的不同对象,调用同一函数产生不同行为;
- 实现条件:基类指针 / 引用 + 虚函数重写;
- 虚函数:加
virtual的成员函数,非成员函数不能加; - 重写要求:函数名、参数、返回值完全一致(协变除外);
- 避坑神器:
override检查重写,final禁止重写; - 抽象类:含纯虚函数(
=0),不能实例化,强制子类重写接口; - 析构函数:基类析构加
virtual,避免子类内存泄漏。
多态是 C++ 面向对象的核心特性之一,学会它能让代码更灵活、更易扩展。新手可以先从简单例子入手,理解清楚重写和多态的条件,再慢慢接触抽象类、析构函数重写等场景。多写几遍代码,跑一跑不同的情况,很快就能掌握啦!
595

被折叠的 条评论
为什么被折叠?



