多态:虚函数与动态绑定
多态,说白了就是「同一个接口,多种实现」。我刚开始学C++时,觉得这东西挺玄乎的。后来在项目中写了一个图形绘制系统,才真正体会到它的威力——你想想看,如果每个形状都要单独写一个绘制函数,那代码得多臃肿?
虚函数与动态绑定
虚函数是实现多态的核心机制。用virtual关键字修饰的成员函数,允许派生类重新定义它。调用虚函数时,编译器不会在编译期就决定调用哪个版本,而是等到运行时才根据对象的实际类型去决定——这就是动态绑定。
核心要点:动态绑定依赖于虚函数表(vtable)。每个包含虚函数的类都有一个虚函数表,对象中则有一个指向该表的指针(vptr)。调用虚函数时,程序通过vptr找到vtable,再找到正确的函数地址。
class Shape {
public:
virtual void draw() const {
std::cout << "绘制一个形状" << std::endl;
}
};
class Circle : public Shape {
public:
void draw() const override {
std::cout << "绘制一个圆形" << std::endl;
}
};
class Rectangle : public Shape {
public:
void draw() const override {
std::cout << "绘制一个矩形" << std::endl;
}
};
void renderShape(const Shape& shape) {
shape.draw(); // 动态绑定,运行时决定调用哪个draw()
}
int main() {
Circle c;
Rectangle r;
renderShape(c); // 输出:绘制一个圆形
renderShape(r); // 输出:绘制一个矩形
return 0;
}
我在项目中遇到过一个问题:忘记在派生类中加override关键字。结果基类改了接口,派生类没同步更新,编译通过了但行为完全不对。嗯,从那以后我养成了习惯——凡是重写虚函数,一定加上override,让编译器帮我检查。
个人建议:使用override关键字。它不仅是文档,更是编译期的安全检查。如果基类没有对应的虚函数,编译器会报错,避免你踩坑。
纯虚函数与抽象类
有时候,基类的某个函数根本没有合理的默认实现。比如「形状」的draw()——你没法画一个抽象的「形状」,对吧?这时候就需要纯虚函数。
纯虚函数在声明末尾加上= 0,表示这个函数没有实现,必须由派生类提供。包含纯虚函数的类就是抽象类,不能实例化。
class Shape {
public:
virtual void draw() const = 0; // 纯虚函数
virtual ~Shape() = default; // 虚析构函数
};
// 错误:不能实例化抽象类
// Shape s; // 编译错误
class Circle : public Shape {
public:
void draw() const override {
std::cout << "绘制一个圆形" << std::endl;
}
};
int main() {
// Shape* p = new Shape(); // 错误
Shape* p = new Circle(); // 正确
p->draw(); // 输出:绘制一个圆形
delete p;
return 0;
}
抽象类的作用,说白了就是定义接口规范。我参与过一个插件系统,每个插件必须实现initialize()、execute()、cleanup()三个纯虚函数。这样主程序就能统一调度,不用关心具体插件怎么实现。
注意:抽象类不能实例化,但可以定义指针或引用。派生类必须实现所有纯虚函数,否则它自己也会变成抽象类。
虚析构函数
这个问题我踩过坑。你想想看,如果基类指针指向派生类对象,删除时只调用了基类的析构函数,派生类的资源就泄漏了。为什么会这样?因为析构函数不是虚函数时,调用的是静态绑定。
class Base {
public:
~Base() { std::cout << "Base析构" << std::endl; }
};
class Derived : public Base {
public:
int* data;
Derived() { data = new int[100]; }
~Derived() {
delete[] data;
std::cout << "Derived析构" << std::endl;
}
};
int main() {
Base* p = new Derived();
delete p; // 只调用了Base析构,Derived析构没调用!
// 内存泄漏!
return 0;
}
解决方案很简单:把基类的析构函数声明为虚函数。
class Base {
public:
virtual ~Base() { std::cout << "Base析构" << std::endl; }
};
// 现在delete p会先调用Derived析构,再调用Base析构
// 输出:
// Derived析构
// Base析构
黄金法则:只要类中有虚函数,就应该把析构函数也声明为虚函数。这是C++的经典实践,别问我为什么,问就是血的教训。
运行时类型识别(RTTI)
RTTI允许你在运行时获取对象的实际类型信息。主要用两个操作符:typeid和dynamic_cast。
typeid返回一个type_info对象,包含类型名称等信息。dynamic_cast用于安全地将基类指针或引用转换为派生类指针或引用,转换失败时返回空指针(对指针)或抛出异常(对引用)。
#include <typeinfo>
class Base {
public:
virtual ~Base() = default;
};
class Derived : public Base {};
int main() {
Base* b = new Derived();
// typeid示例
std::cout << typeid(*b).name() << std::endl; // 输出:class Derived
// dynamic_cast示例
Derived* d = dynamic_cast<Derived*>(b);
if (d) {
std::cout << "转换成功" << std::endl;
}
// 转换失败的情况
Base* b2 = new Base();
Derived* d2 = dynamic_cast<Derived*>(b2);
if (!d2) {
std::cout << "转换失败,d2为空" << std::endl;
}
delete b;
delete b2;
return 0;
}
我曾经踩过的坑:滥用RTTI。有些人拿到基类指针就喜欢用dynamic_cast转来转去,这其实违背了多态的初衷。能用虚函数解决的问题,就别用RTTI。RTTI有性能开销,而且代码会变得难以维护。
我个人习惯:只在两种情况下用RTTI。一是调试时打印类型信息,二是某些框架代码中确实需要根据具体类型做不同处理。日常业务代码中,尽量用虚函数解决问题。
知识体系总览
下面这张图总结了本章的核心知识点和它们之间的关系:
这张图展示了虚函数作为核心,向下延伸出三个重要分支。每个分支都有其特定的应用场景和注意事项。我个人建议你先把虚函数和动态绑定的机制理解透,再去研究RTTI——基础打牢了,上层建筑才稳当。
小技巧:调试时可以用typeid(*ptr).name()快速查看对象的实际类型,这在排查多态相关bug时特别有用。
公众号:蓝海资料掘金营,微信deep3321