目录
[四、静态绑定 vs 动态绑定](#四、静态绑定 vs 动态绑定)
[1. 忘记写virtual,导致静态绑定](#1. 忘记写virtual,导致静态绑定)
[2. 重写时签名不匹配(但忘了override)](#2. 重写时签名不匹配(但忘了override))
[3. 通过对象(而非指针/引用)调用虚函数](#3. 通过对象(而非指针/引用)调用虚函数)
[4. 在构造函数或析构函数中调用虚函数](#4. 在构造函数或析构函数中调用虚函数)
一、一个没有多态的问题
假设你有一个图形绘制程序,需要处理多种形状:圆形、矩形、三角形。
没有多态的写法可能是这样:
cpp
class Circle {
public:
void draw() { cout << "画一个圆" << endl; }
};
class Rectangle {
public:
void draw() { cout << "画一个矩形" << endl; }
};
class Triangle {
public:
void draw() { cout << "画一个三角形" << endl; }
};
// 需要一个一个处理,每种形状单独写代码
Circle c;
Rectangle r;
Triangle t;
c.draw();
r.draw();
t.draw();
如果有一个"形状数组",想统一调用draw(),就麻烦了------因为编译器不知道每个位置到底是什么类型。
多态的解决方案 :让所有形状继承自一个共同的基类Shape,然后通过基类指针来操作。
cpp
class Shape {
public:
virtual void draw() { cout << "画一个形状" << endl; }
};
class Circle : public Shape {
public:
void draw() override { cout << "画一个圆" << endl; }
};
class Rectangle : public Shape {
public:
void draw() override { cout << "画一个矩形" << endl; }
};
// 现在可以统一处理了
Shape* shapes[] = {new Circle(), new Rectangle()};
for (int i = 0; i < 2; i++) {
shapes[i]->draw(); // 输出:画一个圆 / 画一个矩形
}
这就是多态的魅力:写代码时不知道具体类型,运行时决定调用哪个函数。
二、虚函数的基本语法
声明虚函数
在基类中,用virtual关键字声明一个成员函数为虚函数:
cpp
class Base {
public:
virtual void show() { cout << "Base::show" << endl; }
};
重写虚函数
在派生类中,定义一个签名完全相同的函数来重写:
cpp
class Derived : public Base {
public:
void show() override { cout << "Derived::show" << endl; }
};
通过基类指针/引用调用
cpp
Base* ptr = new Derived();
ptr->show(); // 输出 Derived::show(动态绑定)
Base& ref = *new Derived();
ref.show(); // 也是 Derived::show
关键:只有通过指针或引用调用虚函数时,才会发生动态绑定。直接通过对象调用,是静态绑定。
cpp
Derived d;
d.show(); // 静态绑定,编译时就确定调用Derived::show
Base b = d; // 对象切片!
b.show(); // 静态绑定,调用Base::show(因为b是Base对象)
三、override关键字(C++11)
override是C++11引入的关键字,强烈建议使用。
它的作用
-
明确表达意图:告诉读者这个函数是要重写基类的虚函数
-
让编译器帮你检查:如果基类没有对应的虚函数(签名不匹配),编译报错
常见错误被拦截
cpp
class Base {
public:
virtual void func(int x) {}
};
class Derived : public Base {
public:
void func(double x) override { // ❌ 编译错误!
// 基类没有 func(double),签名不匹配
}
};
没有override的话,这只是一个隐藏 (不是重写),不会报错,程序逻辑就错了。有了override,编译器会帮你发现。
正确写法
cpp
class Derived : public Base {
public:
void func(int x) override { // ✅ 正确重写
// ...
}
};
四、静态绑定 vs 动态绑定
这是理解虚函数的关键。
静态绑定(编译时确定)
cpp
Base obj;
obj.show(); // 编译时就确定了:调用Base::show
编译器看到obj的类型是Base,直接生成调用Base::show的代码。
动态绑定(运行时确定)
cpp
Base* ptr = getShape(); // 运行时才知道ptr指向什么
ptr->show(); // 运行时才知道调用哪个show
编译器不知道ptr到底指向Base、Circle还是Rectangle,所以它生成一段代码:通过虚函数表在运行时查找真正的函数地址(下一讲详细讲)。
对比表
| 特性 | 静态绑定 | 动态绑定 |
|---|---|---|
| 决定时机 | 编译时 | 运行时 |
| 函数类型 | 非虚函数 | 虚函数 |
| 调用方式 | 对象名调用 | 指针/引用调用 |
| 性能 | 快(直接调用) | 稍慢(查表开销) |
五、完整例子:动物园的动物叫声
cpp
#include <iostream>
#include <vector>
#include <string>
using namespace std;
// 基类:动物
class Animal {
protected:
string name;
public:
Animal(string n) : name(n) {}
// 虚函数,每个动物叫法不同
virtual void speak() const {
cout << name << "发出某种声音" << endl;
}
// 虚析构函数(重要!下篇详细讲)
virtual ~Animal() {}
};
// 派生类:狗
class Dog : public Animal {
public:
Dog(string n) : Animal(n) {}
void speak() const override {
cout << name << "汪汪叫:汪汪!" << endl;
}
void wagTail() const {
cout << name << "摇尾巴" << endl;
}
};
// 派生类:猫
class Cat : public Animal {
public:
Cat(string n) : Animal(n) {}
void speak() const override {
cout << name << "喵喵叫:喵~" << endl;
}
void climb() const {
cout << name << "爬树" << endl;
}
};
// 派生类:鸟
class Bird : public Animal {
public:
Bird(string n) : Animal(n) {}
void speak() const override {
cout << name << "叽叽喳喳:啾啾!" << endl;
}
void fly() const {
cout << name << "飞翔" << endl;
}
};
// 让动物们依次叫(多态的核心用法)
void makeAllSpeak(const vector<Animal*>& animals) {
cout << "\n=== 动物大合唱 ===" << endl;
for (const auto* animal : animals) {
animal->speak(); // 动态绑定,调用实际类型的版本
}
}
int main() {
// 创建动物数组,全部用基类指针指向
vector<Animal*> zoo;
zoo.push_back(new Dog("旺财"));
zoo.push_back(new Cat("咪咪"));
zoo.push_back(new Bird("小小"));
zoo.push_back(new Animal("未知生物"));
// 统一调用speak,每个动物发出自己的叫声
makeAllSpeak(zoo);
// 注意:通过基类指针无法访问派生类特有函数
// zoo[0]->wagTail(); // ❌ 编译错误,Animal没有wagTail
// 如果需要访问派生类特有函数,需要用dynamic_cast(后续章节)
// 释放内存
for (auto* animal : zoo) {
delete animal;
}
return 0;
}
输出:
text
=== 动物大合唱 ===
旺财汪汪叫:汪汪!
咪咪喵喵叫:喵~
小小叽叽喳喳:啾啾!
未知生物发出某种声音
注意到没有:makeAllSpeak函数只知道参数是Animal*,但实际执行时,每个动物都发出了自己特有的叫声。这就是多态的力量。
六、虚函数的限制
不是所有函数都可以是虚函数:
| 函数类型 | 可以是虚函数吗 | 原因 |
|---|---|---|
| 普通成员函数 | ✅ 可以 | 最常见的虚函数 |
| 析构函数 | ✅ 可以 | 非常推荐(下篇讲) |
| 静态函数 | ❌ 不可以 | 静态函数属于类,不属于对象,没有this |
| 构造函数 | ❌ 不可以 | 对象还没构造完,虚表未建立 |
| 内联函数 | ⚠️ 不保证 | 虚函数动态绑定,内联是编译时展开,通常会被忽略 |
| 友元函数 | ❌ 不可以 | 友元不是成员函数 |
七、常见错误
1. 忘记写virtual,导致静态绑定
cpp
class Base {
public:
void show() { cout << "Base" << endl; } // 忘了virtual
};
class Derived : public Base {
public:
void show() { cout << "Derived" << endl; }
};
Base* p = new Derived();
p->show(); // 输出"Base"!因为静态绑定,没有多态
2. 重写时签名不匹配(但忘了override)
cpp
class Base {
public:
virtual void func(int x) {}
};
class Derived : public Base {
public:
void func(double x) {} // 参数不同,这是隐藏,不是重写
// 没有override,编译器不报错,但逻辑错了
};
3. 通过对象(而非指针/引用)调用虚函数
cpp
Derived d;
Base b = d; // 对象切片!派生类部分被切掉了
b.show(); // 调用Base::show,不是多态
4. 在构造函数或析构函数中调用虚函数
cpp
class Base {
public:
Base() { show(); } // 调用Base::show,不会调用Derived::show
virtual void show() { cout << "Base" << endl; }
};
class Derived : public Base {
public:
void show() override { cout << "Derived" << endl; }
};
在构造期间,派生类部分还没构造完成,虚表指向基类,所以不会发生多态。
八、这一篇的收获
你现在应该理解:
-
用
virtual声明虚函数,用override标记重写 -
通过基类指针或引用调用虚函数,实现动态绑定(多态)
-
动态绑定的决策在运行时 ,静态绑定的决策在编译时
-
多态让代码可以对扩展开放(加新形状不用改旧代码),符合开闭原则
💡 小作业:写一个
MediaPlayer基类,有虚函数play()。派生类MP3Player、VideoPlayer、StreamingPlayer各自重写play()。写一个函数playMedia(MediaPlayer&),传入不同播放器时播放各自的内容。
下一篇预告:第15篇《多态(二):虚函数表(vtable)内存布局揭秘》------虚函数到底是怎么实现的?为什么会有性能开销?虚函数表(vtable)和虚指针(vptr)的原理是什么?了解这些,你才算真正精通C++多态。