多态:虚函数与动态绑定

多态,说白了就是「同一个接口,多种实现」。我刚开始学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允许你在运行时获取对象的实际类型信息。主要用两个操作符:typeiddynamic_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。一是调试时打印类型信息,二是某些框架代码中确实需要根据具体类型做不同处理。日常业务代码中,尽量用虚函数解决问题。

知识体系总览

下面这张图总结了本章的核心知识点和它们之间的关系:

多态:虚函数与动态绑定 虚函数 (virtual) 动态绑定 (vtable/vptr) 纯虚函数 (= 0) 虚析构函数 RTTI (typeid/dynamic_cast) 抽象类(不可实例化) 定义接口规范 防止资源泄漏 运行时类型安全转换 获取运行时类型信息 虚函数是多态的基石,纯虚函数定义接口,虚析构函数保障资源安全,RTTI提供运行时类型信息

这张图展示了虚函数作为核心,向下延伸出三个重要分支。每个分支都有其特定的应用场景和注意事项。我个人建议你先把虚函数和动态绑定的机制理解透,再去研究RTTI——基础打牢了,上层建筑才稳当。

小技巧:调试时可以用typeid(*ptr).name()快速查看对象的实际类型,这在排查多态相关bug时特别有用。


公众号:蓝海资料掘金营,微信deep3321