多态与虚函数:让代码“活”起来

多态,这个词听起来挺唬人的。说白了,就是“同一个接口,多种形态”。

我刚开始学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),指向所属类的虚函数表。

我画了一张图,帮你理解这个机制:

虚函数表(vtable)工作原理 Animal 对象 vptr 其他成员... Animal vtable speak() → Animal::speak ...其他虚函数 Dog 对象 vptr 其他成员... Dog vtable speak() → Dog::speak ...其他虚函数 调用过程 1. 通过 vptr 找到 vtable 2. 在 vtable 中查找函数地址 3. 调用实际函数

当调用animal->speak()时,实际流程是:

  1. 通过对象的vptr找到虚函数表
  2. 在虚函数表中找到speak函数的地址
  3. 调用该地址指向的函数

这个过程叫动态绑定。它发生在运行时,所以叫运行时多态。

小提示:虚函数表是类级别的,不是对象级别的。同一个类的所有对象共享同一个虚函数表。每个对象只保存一个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允许你在运行时获取对象的实际类型。主要用两个操作符:typeiddynamic_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是“最后的手段”,不是常规武器。

知识体系总览

最后,我用一张图总结本章的核心内容:

多态与虚函数知识体系 多态 虚函数 & vtable 动态绑定机制 纯虚函数 & 抽象类 接口规范定义 虚析构函数 防止内存泄漏 RTTI 运行时类型识别

多态是C++面向对象编程的核心特性之一。它让代码更灵活、更易扩展。我个人觉得,理解虚函数表的工作原理,是掌握多态的关键。一旦你明白了vptr和vtable的机制,很多问题就迎刃而解了。

嗯,今天就到这里。记住:多态不是炫技,而是解决实际问题的工具。用对了地方,代码会变得优雅而强大。


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