引言
在面向对象编程中,多态是三大特性(封装、继承、多态)中最精髓的一个。它字面意思是"多种形态",在C++中,多态允许我们通过基类指针或引用调用派生类的重写函数,从而实现"一个接口,多种实现"。
简单来说:同一个函数名,在不同的对象上执行不同的行为。
在我学习C++的过程中,多态曾让我既兴奋又困惑------兴奋的是它让代码如此灵活,困惑的是虚函数、虚表、重写这些概念交织在一起。今天,我们先从基础开始,理解多态的概念和虚函数的规则。
第一部分:什么是多态?
一、多态的分类
| 类型 | 别名 | 发生时机 | 实现方式 |
|---|---|---|---|
| 静态多态 | 编译时多态 | 编译阶段 | 函数重载、模板 |
| 动态多态 | 运行时多态 | 运行阶段 | 虚函数、继承 |
cpp
// 静态多态:函数重载
class Calculator {
public:
int add(int a, int b) { return a + b; } // 编译时确定
double add(double a, double b) { return a + b; } // 编译时确定
};
// 动态多态:虚函数(本节重点)
class Animal {
public:
virtual void speak() { cout << "动物叫" << endl; } // 运行时确定
};
class Dog : public Animal {
public:
void speak() override { cout << "汪汪" << endl; }
};
二、多态的核心思想***
第二部分:虚函数的基本概念
一、什么是虚函数?
虚函数是在基类中使用关键字 virtual 声明的成员函数,它可以在派生类中被重写(override)。
cpp
#include <iostream>
using namespace std;
class Shape {
public:
// 虚函数
virtual void draw() {
cout << "绘制图形" << endl;
}
// 普通函数(非虚)
void info() {
cout << "这是一个图形" << endl;
}
};
class Circle : public Shape {
public:
// 重写虚函数
void draw() override {
cout << "绘制圆形" << endl;
}
// 隐藏普通函数(不推荐)
void info() {
cout << "这是一个圆形" << endl;
}
};
int main() {
Circle c;
Shape* p = &c; // 基类指针指向派生类对象
p->draw(); // 输出:绘制圆形(虚函数:调用派生类版本)
p->info(); // 输出:这是一个图形(普通函数:调用基类版本)
return 0;
}
二、虚函数的作用
cpp
// 没有虚函数:无法实现多态
class Bird {
public:
void fly() { cout << "鸟在飞" << endl; }
};
class Penguin : public Bird {
public:
void fly() { cout << "企鹅不会飞" << endl; }
};
void makeFly(Bird* b) {
b->fly(); // 永远调用 Bird::fly()
}
// 有虚函数:实现多态
class BirdV {
public:
virtual void fly() { cout << "鸟在飞" << endl; }
};
class PenguinV : public BirdV {
public:
void fly() override { cout << "企鹅不会飞" endl; }
};
void makeFlyV(BirdV* b) {
b->fly(); // 根据实际对象类型调用
}
int main() {
Penguin p;
makeFly(&p); // 输出:鸟在飞(不是期望的结果)
PenguinV pv;
makeFlyV(&pv); // 输出:企鹅不会飞(正确的多态行为)
return 0;
}
第三部分:虚函数的规则
一、虚函数的基本规则
cpp
class Base {
public:
// 规则1:虚函数用 virtual 关键字声明
virtual void func1() { cout << "Base::func1" << endl; }
// 规则2:虚函数可以有默认实现
virtual void func2() { cout << "Base::func2" << endl; }
// 规则3:虚函数可以是纯虚函数(下节讲解)
// virtual void func3() = 0; // 纯虚函数
// 规则4:析构函数通常应该是虚函数
virtual ~Base() { cout << "Base析构" << endl; }
};
class Derived : public Base {
public:
// 规则5:重写虚函数时,函数签名必须完全相同
// 返回类型、函数名、参数列表都要一致
void func1() override { cout << "Derived::func1" << endl; }
// 规则6:可以使用 override 关键字(C++11)明确表示重写
void func2() override { cout << "Derived::func2" endl; }
// 错误示例:参数不同,这是重载/隐藏,不是重写
// void func1(int x) { } // 这会隐藏基类的 func1
~Derived() { cout << "Derived析构" << endl; }
};
二、虚函数规则详细说明
规则1:虚函数必须通过指针或引用调用才能实现多态
cpp
class Base {
public:
virtual void show() { cout << "Base" << endl; }
};
class Derived : public Base {
public:
void show() override { cout << "Derived" << endl; }
};
int main() {
Derived d;
Base b;
Base* p1 = &d;
p1->show(); // ✅ 多态:输出 Derived
Base& r1 = d;
r1.show(); // ✅ 多态:输出 Derived
Base b1 = d; // 对象切片
b1.show(); // ❌ 不是多态:输出 Base(切片丢失了派生类信息)
return 0;
}
规则2:虚函数不能是静态函数
cpp
class Test {
public:
// static virtual void func(); // 错误!虚函数不能是静态的
// 静态函数属于类,不属于对象,无法实现多态
};
规则3:虚函数不能是内联函数(但编译器可能忽略)
cpp
class Test {
public:
// virtual inline void func(); // 不推荐,虚函数通常不内联
// 因为虚函数的调用需要在运行时确定,内联在编译时展开
};
规则4:构造函数不能是虚函数
cpp
class Test {
public:
// virtual Test() { } // 错误!构造函数不能是虚函数
// 构造对象时需要知道确切类型,无法动态决定
};
规则5:虚函数的重写要求函数签名完全一致
cpp
class Base {
public:
virtual void func() { }
virtual void func(int x) { }
};
class Derived : public Base {
public:
// ✅ 正确:签名完全一致
void func() override { }
// ✅ 正确:重写另一个虚函数
void func(int x) override { }
// ❌ 错误:返回值类型不同(特殊情况除外:协变返回类型)
// int func() override { return 0; }
// ❌ 错误:参数不同,这是隐藏,不是重写
// void func(double x) { }
};
规则6:协变返回类型(特殊情况)
cpp
class Base {
public:
virtual Base* clone() const {
return new Base(*this);
}
};
class Derived : public Base {
public:
// 允许:返回类型是基类返回类型的派生类指针
virtual Derived* clone() const override {
return new Derived(*this);
}
};
规则7:虚函数的默认参数不会动态绑定
cpp
class Base {
public:
virtual void func(int x = 10) {
cout << "Base::func: " << x << endl;
}
};
class Derived : public Base {
public:
void func(int x = 20) override {
cout << "Derived::func: " << x << endl;
}
};
int main() {
Base* p = new Derived();
p->func(); // 输出:Derived::func: 10
// 函数体是 Derived 的,但默认参数使用的是 Base 的!
// 警告:不要重新定义虚函数的默认参数
delete p;
return 0;
}
第四部分:虚函数与析构函数
一、为什么基类析构函数应该是虚函数?
cpp
// 错误示例:基类析构函数不是虚函数
class BaseWrong {
public:
~BaseWrong() { cout << "BaseWrong析构" << endl; }
};
class DerivedWrong : public BaseWrong {
private:
int* data;
public:
DerivedWrong() : data(new int[100]) { }
~DerivedWrong() {
delete[] data;
cout << "DerivedWrong析构" << endl;
}
};
int main() {
BaseWrong* p = new DerivedWrong();
delete p; // 只调用 BaseWrong 的析构函数!
// 问题:DerivedWrong 的析构函数没有被调用,data 内存泄漏!
return 0;
}
// 正确示例:基类析构函数是虚函数
class BaseCorrect {
public:
virtual ~BaseCorrect() { cout << "BaseCorrect析构" << endl; }
};
class DerivedCorrect : public BaseCorrect {
private:
int* data;
public:
DerivedCorrect() : data(new int[100]) { }
~DerivedCorrect() override {
delete[] data;
cout << "DerivedCorrect析构" << endl;
}
};
int main() {
BaseCorrect* p = new DerivedCorrect();
delete p; // 先调用 DerivedCorrect 析构,再调用 BaseCorrect 析构
// ✅ 正确释放资源
return 0;
}
二、规则总结:只要类会被继承,析构函数就应该是虚函数
cpp
class Interface {
public:
virtual ~Interface() = default; // 虚析构函数
virtual void doSomething() = 0;
};
第五部分:override 和 final 关键字(C++11)
一、override:显式声明重写
cpp
class Base {
public:
virtual void func1() { }
virtual void func2(int x) { }
virtual void func3() const { }
};
class Derived : public Base {
public:
// ✅ 明确表示要重写基类的虚函数
void func1() override { }
// ❌ 编译错误:参数不匹配,override 会检查
// void func2(double x) override { }
// ❌ 编译错误:const 不匹配
// void func3() override { }
// 建议:只要重写虚函数,就加上 override
};
二、final:禁止重写或禁止继承
cpp
class Base {
public:
virtual void func() final { // final:派生类不能重写这个函数
cout << "Base::func" << endl;
}
};
class Derived : public Base {
public:
// void func() override { } // 错误!func 被 final 禁止重写
};
class FinalClass final { // final:不能被继承
// ...
};
// class Bad : public FinalClass { }; // 错误!FinalClass 被 final 禁止继承
第六部分:完整示例------动物叫声系统
cpp
#include <iostream>
#include <string>
#include <vector>
using namespace std;
// 基类:动物
class Animal {
protected:
string name;
public:
Animal(const string& n) : name(n) { }
// 虚函数:发出声音
virtual void speak() const {
cout << name << "发出声音" << endl;
}
// 虚函数:获取类型
virtual string getType() const {
return "动物";
}
// 虚析构函数:确保派生类正确析构
virtual ~Animal() {
cout << name << "被销毁" << endl;
}
};
// 派生类:狗
class Dog : public Animal {
public:
Dog(const string& n) : Animal(n) { }
void speak() const override {
cout << name << "汪汪叫" << endl;
}
string getType() const override {
return "狗";
}
};
// 派生类:猫
class Cat : public Animal {
public:
Cat(const string& n) : Animal(n) { }
void speak() const override {
cout << name << "喵喵叫" << endl;
}
string getType() const override {
return "猫";
}
};
// 派生类:鸟
class Bird : public Animal {
public:
Bird(const string& n) : Animal(n) { }
void speak() const override {
cout << name << "叽叽喳喳" << endl;
}
string getType() const override {
return "鸟";
}
};
// 多态演示函数
void makeSound(const Animal* animal) {
cout << "这是一只" << animal->getType();
cout << ",它在";
animal->speak();
}
int main() {
cout << "=== 多态演示 ===" << endl;
// 基类指针数组
vector<Animal*> animals;
animals.push_back(new Dog("旺财"));
animals.push_back(new Cat("咪咪"));
animals.push_back(new Bird("啾啾"));
// 统一调用,行为不同
for (Animal* a : animals) {
makeSound(a);
}
// 清理内存
for (Animal* a : animals) {
delete a; // 虚析构函数确保正确释放
}
return 0;
}
/* 输出:
=== 多态演示 ===
这是一只狗,它在旺财汪汪叫
这是一只猫,它在咪咪喵喵叫
这是一只鸟,它在啾啾叽叽喳喳
旺财被销毁
咪咪被销毁
啾啾被销毁
*/
总结
一、虚函数核心规则速查表
| 规则 | 说明 |
|---|---|
| 声明方式 | 使用 virtual 关键字 |
| 调用方式 | 通过指针或引用调用才能实现多态 |
| 重写要求 | 函数签名必须完全相同(返回值、函数名、参数) |
| 构造函数 | 不能是虚函数 |
| 析构函数 | 基类析构函数应该是虚函数 |
| 静态函数 | 不能是虚函数 |
| 默认参数 | 不会动态绑定,不要重新定义 |
| override | 推荐使用,编译器会检查重写是否正确 |
| final | 禁止重写或禁止继承 |
二、虚函数 vs 普通函数
| 特性 | 普通函数 | 虚函数 |
|---|---|---|
| 绑定时机 | 编译时 | 运行时 |
| 调用方式 | 通过对象或指针 | 通常通过指针/引用 |
| 派生类重写 | 隐藏(不推荐) | 重写(override) |
| 性能 | 快(直接调用) | 稍慢(通过虚表) |
| 多态支持 | 不支持 | 支持 |
三、使用建议
-
基类析构函数必须设为虚函数(如果类会被继承)
-
重写虚函数时使用
override关键字(C++11) -
不需要被继承的类可以使用
final -
不要重新定义虚函数的默认参数
-
通过基类指针/引用调用虚函数才能实现多态
虚函数是实现运行时多态的基础。理解虚函数的规则,是掌握C++面向对象编程的关键一步。
核心记忆:
-
virtual告诉编译器:这个函数需要动态绑定 -
override告诉编译器:我要重写基类的虚函数 -
final告诉编译器:到此为止,不能再重写/继承
下一节,我们将深入讲解虚函数的底层实现原理------虚函数表(vtable)和虚函数指针(vptr),敬请期待!