多态与虚函数:让代码“活”起来
多态,这个词听起来挺唬人的。说白了,就是“同一个接口,多种形态”。
我刚开始学C++时,对多态的理解很浅。觉得不就是父类指针调用子类方法嘛。直到我在一个项目中,需要处理几十种不同的图形绘制——圆形、矩形、三角形……如果每种图形都写一个独立的绘制函数,那代码会膨胀到没法看。多态帮我解决了这个问题。
今天我们就来聊聊,多态到底怎么用,背后的机制是什么。
多态的概念与实现
多态分为两种:编译时多态(函数重载、模板)和运行时多态(虚函数)。我们重点讲运行时多态。
运行时多态的核心是:通过基类指针或引用,调用派生类的重写函数。
看个例子:
class Animal {
public:
virtual void speak() {
cout << "动物叫" << endl;
}
};
class Dog : public Animal {
public:
void speak() override {
cout << "汪汪" << endl;
}
};
class Cat : public Animal {
public:
void speak() override {
cout << "喵喵" << endl;
}
};
void makeSound(Animal* animal) {
animal->speak(); // 这里调用哪个函数?运行时决定
}
int main() {
Dog dog;
Cat cat;
makeSound(&dog); // 输出:汪汪
makeSound(&cat); // 输出:喵喵
return 0;
}
你看,makeSound函数根本不知道传入的是狗还是猫。它只认Animal指针。但实际调用时,却执行了正确的派生类方法。这就是多态的魔力。
关键点:多态必须满足三个条件——继承、虚函数、基类指针/引用调用。
虚函数与虚函数表(vtable)
为什么基类指针能调用到派生类的方法?这背后是虚函数表(vtable)在起作用。
每个包含虚函数的类,编译器都会为它生成一个虚函数表。这个表是一个函数指针数组,存储了该类所有虚函数的地址。每个对象内部还藏着一个虚指针(vptr),指向所属类的虚函数表。
我画了一张图,帮你理解这个机制:
当调用animal->speak()时,实际流程是:
- 通过对象的vptr找到虚函数表
- 在虚函数表中找到speak函数的地址
- 调用该地址指向的函数
这个过程叫动态绑定。它发生在运行时,所以叫运行时多态。
小提示:虚函数表是类级别的,不是对象级别的。同一个类的所有对象共享同一个虚函数表。每个对象只保存一个vptr指针(4或8字节)。
纯虚函数与抽象类
有时候,基类的某个函数根本没有合理的默认实现。比如“形状”类的“面积”函数——你说形状的面积怎么算?没法算。
这时候就用纯虚函数:
class Shape {
public:
virtual double area() = 0; // 纯虚函数
virtual void draw() = 0; // 纯虚函数
};
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double area() override {
return 3.14159 * radius * radius;
}
void draw() override {
cout << "画一个圆" << endl;
}
};
含有纯虚函数的类就是抽象类。抽象类不能实例化。你写Shape s;会编译报错。
抽象类的意义在于:定义接口规范。它告诉派生类:“你必须实现这些方法,否则你也不能实例化。”
注意:我曾经见过有人把抽象类当普通类用,试图创建抽象类的对象。编译器直接报错。记住,抽象类只能作为基类,用来派生新类。
虚析构函数
这个知识点,我吃过亏。有一次写代码,基类指针指向派生类对象,然后delete这个指针。结果派生类的析构函数没被调用,内存泄漏了。
为什么会这样?因为析构函数不是虚函数时,delete基类指针只会调用基类的析构函数。派生类的资源没释放。
解决方案很简单:把基类的析构函数声明为虚函数。
class Base {
public:
virtual ~Base() {
cout << "Base 析构" << endl;
}
};
class Derived : public Base {
private:
int* data;
public:
Derived() { data = new int[100]; }
~Derived() override {
delete[] data;
cout << "Derived 析构" << endl;
}
};
int main() {
Base* ptr = new Derived();
delete ptr; // 正确调用 Derived 的析构函数
return 0;
}
记住一条规则:只要类中有虚函数,就把析构函数也设为虚函数。这是C++编程的黄金法则。
运行时类型识别(RTTI)
RTTI允许你在运行时获取对象的实际类型。主要用两个操作符:typeid和dynamic_cast。
class Base {
public:
virtual ~Base() {}
};
class Derived1 : public Base {};
class Derived2 : public Base {};
void identify(Base* ptr) {
// typeid 返回 type_info 对象
if (typeid(*ptr) == typeid(Derived1)) {
cout << "这是 Derived1 类型" << endl;
} else if (typeid(*ptr) == typeid(Derived2)) {
cout << "这是 Derived2 类型" << endl;
}
// dynamic_cast 安全向下转型
Derived1* d1 = dynamic_cast<Derived1*>(ptr);
if (d1) {
cout << "成功转换为 Derived1" << endl;
}
}
不过我得提醒你:RTTI有性能开销。每次typeid或dynamic_cast都需要查虚函数表。我一般只在调试或处理特殊逻辑时用RTTI。正常的多态设计,应该尽量避免依赖具体类型。
最佳实践:能用虚函数解决的问题,就别用RTTI。RTTI是“最后的手段”,不是常规武器。
知识体系总览
最后,我用一张图总结本章的核心内容:
多态是C++面向对象编程的核心特性之一。它让代码更灵活、更易扩展。我个人觉得,理解虚函数表的工作原理,是掌握多态的关键。一旦你明白了vptr和vtable的机制,很多问题就迎刃而解了。
嗯,今天就到这里。记住:多态不是炫技,而是解决实际问题的工具。用对了地方,代码会变得优雅而强大。
公众号:蓝海资料掘金营,微信deep3321