15、多态与虚函数:虚函数、纯虚函数、抽象类、虚函数表、运行时类型识别(RTTI)
多态,说白了就是「同一个接口,多种形态」。这是面向对象编程里最核心、也最迷人的特性之一。我刚开始学C++时,觉得虚函数就是个加了个virtual关键字的函数,后来在项目中踩过几次坑,才真正理解了它背后的设计哲学。
这一章,我会带你从虚函数的基础用法,一路深入到虚函数表的底层原理,再到纯虚函数、抽象类,以及运行时类型识别(RTTI)。嗯,内容不少,但每一点都是实战中绕不开的。
15.1 虚函数:让基类指针调用派生类方法
先看一个最经典的例子:
class Animal {
public:
virtual void speak() {
cout << "动物发出声音" << endl;
}
};
class Dog : public Animal {
public:
void speak() override {
cout << "汪汪!" << endl;
}
};
int main() {
Animal* ptr = new Dog();
ptr->speak(); // 输出:汪汪!
delete ptr;
return 0;
}
如果没有virtual,ptr->speak()会调用Animal::speak(),因为编译器只看指针的静态类型。加了virtual后,运行时才会根据对象的实际类型去调用对应的函数。这就是动态绑定。
override关键字。这能帮编译器检查你是否真的重写了基类的虚函数,避免拼写错误导致的隐蔽bug。我在代码审查时,看到没有override的代码,都会提醒加上。
15.2 虚函数表(vtable):多态的底层秘密
为什么会这样?虚函数是怎么找到正确的函数的?
每个包含虚函数的类,编译器都会为它生成一张虚函数表(vtable)。这张表是一个函数指针数组,里面存放着该类所有虚函数的地址。每个对象内部还藏着一个虚函数指针(vptr),指向所属类的虚函数表。
我画了一张图,帮你理解这个结构:
15.3 纯虚函数与抽象类
有时候,基类中的某个函数根本没有合理的默认实现。比如「形状」类的「计算面积」方法——你没法给一个抽象的「形状」定义面积公式。这时候就需要纯虚函数。
class Shape {
public:
virtual double area() const = 0; // 纯虚函数
virtual ~Shape() = default;
};
class Circle : public Shape {
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override {
return 3.14159 * radius * radius;
}
};
含有纯虚函数的类就是抽象类。抽象类不能实例化——你不能写Shape s;,编译器会报错。它的作用就是定义接口,强制派生类去实现。
15.4 运行时类型识别(RTTI)
RTTI 允许你在运行时获取对象的实际类型信息。主要用两个操作符:typeid 和 dynamic_cast。
15.4.1 typeid 操作符
Animal* animal = new Dog();
const type_info& info = typeid(*animal);
cout << info.name() << endl; // 输出可能为 "class Dog"
typeid返回一个type_info对象,包含类型名称等信息。注意,如果对指针本身使用typeid,得到的是指针的类型,而不是指向对象的类型。
15.4.2 dynamic_cast 安全向下转型
Animal* animal = new Dog();
Dog* dog = dynamic_cast<Dog*>(animal);
if (dog) {
dog->fetchBall(); // 只有 Dog 类有的方法
} else {
cout << "转型失败,不是 Dog 类型" << endl;
}
dynamic_cast会在运行时检查转型是否合法。如果转型失败,对指针返回nullptr,对引用抛出std::bad_cast异常。
dynamic_cast或typeid,这往往说明你的类设计有问题——多态应该通过虚函数来解决,而不是靠手动判断类型。我在项目中见过有人用dynamic_cast写出一大串if-else链,那还不如直接用switch语句呢。
15.5 虚函数与多态的注意事项
| 注意事项 | 说明 |
|---|---|
| 构造函数不能是虚函数 | 虚函数表在构造完成后才建立,构造函数调用时vptr还未初始化 |
| 不要在构造函数/析构函数中调用虚函数 | 此时对象的动态类型是当前正在构造/析构的类,不是最终派生类 |
| 虚函数有性能开销 | 每次调用需要多一次间接寻址,且无法内联 |
| 静态函数不能是虚函数 | 静态函数属于类,不属于对象,不参与动态绑定 |
| 内联函数可以是虚函数吗? | 可以声明,但动态调用时不会内联,只有通过对象直接调用时才可能内联 |
15.6 知识体系总览
下面这张图总结了本章的核心知识点和它们之间的关系:
多态是C++面向对象编程的精华所在。理解了虚函数表,你就理解了动态绑定的本质;掌握了纯虚函数和抽象类,你就能设计出清晰的接口层次;而RTTI则是在必要时给你一把「安全向下转型」的钥匙。
嗯,这一章的内容就到这里。记住,多态不是为了炫技,而是为了让代码更灵活、更可扩展。下次写代码时,想想能不能用多态来替代那些冗长的if-else或switch——你会发现,世界一下子清爽了很多。