虚函数与虚表

虚函数(Virtual Function)和虚函数表(Virtual Table,简称 vtable)是 C++ 实现运行时多态的核心机制。通过虚函数,基类可以定义通用接口,派生类可以重写基类中的虚函数以实现不同的行为。虚表则是管理虚函数调用的一个关键数据结构。

1. 虚函数

虚函数是在基类中使用关键字 virtual 声明的函数,它可以在派生类中被重写。当通过基类指针或引用调用虚函数时,实际调用的是派生类中的重写函数。这种行为称为运行时多态。

#include <iostream>

class Base {
public:
    virtual void show() {
        std::cout << "Base class" << std::endl;
    }
};

class Derived : public Base {
public:
    void show() override {
        std::cout << "Derived class" << std::endl;
    }
};

void display(Base& obj) {
    obj.show();  // 调用的函数取决于传入对象的类型
}

int main() {
    Base b;
    Derived d;
    display(b);  // 调用 Base::show
    display(d);  // 调用 Derived::show
    return 0;
}

2. 虚函数表(vtable)

虚函数表是编译器为每个包含虚函数的类生成的一个内部数据结构。虚函数表中包含指向该类的虚函数的指针。当一个类包含虚函数时,编译器会在每个对象中添加一个隐藏的指针,称为虚表指针(vptr),该指针指向该类的虚函数表。

2.1 虚表指针(vptr)

每个包含虚函数的对象在其内存布局中都有一个隐藏的指针,指向对应类的虚函数表。通过这个指针,可以在运行时确定实际调用的函数。

2.2 虚函数调用

当通过基类指针或引用调用虚函数时,程序会通过对象的虚表指针(vptr)找到对应的虚函数表(vtable),然后从虚函数表中找到实际要调用的函数指针,并调用该函数。这种机制确保了调用的是实际对象类型的函数,而不是基类的函数。

3. 虚函数表的实现细节

#include <iostream>

class Base {
public:
    virtual void f1() { std::cout << "Base::f1" << std::endl; }
    virtual void f2() { std::cout << "Base::f2" << std::endl; }
    void f3() { std::cout << "Base::f3" << std::endl; }
};

class Derived : public Base {
public:
    void f1() override { std::cout << "Derived::f1" << std::endl; }
    void f2() override { std::cout << "Derived::f2" << std::endl; }
    void f3() { std::cout << "Derived::f3" << std::endl; }
};

int main() {
    Base *b = new Derived();
    b->f1();  // 调用 Derived::f1
    b->f2();  // 调用 Derived::f2
    b->f3();  // 调用 Base::f3,因为 f3 不是虚函数
    delete b;
    return 0;
}

4. 虚函数与性能

虚函数在运行时通过虚表进行间接调用,相比非虚函数有一定的性能开销。这种开销主要体现在以下方面:

  • 额外的内存开销:每个包含虚函数的对象都需要一个虚表指针,占用额外的内存空间。
  • 间接调用开销:调用虚函数需要通过虚表查找函数指针,然后再进行实际调用,比直接调用非虚函数多了一次间接寻址的过程。

尽管有这些开销,虚函数提供的运行时多态性在很多情况下是非常必要且有价值的,特别是在设计灵活和可扩展的程序时。

5. 纯虚函数与抽象类

纯虚函数是指在基类中声明但不提供实现的虚函数,纯虚函数以 = 0 结尾。包含纯虚函数的类称为抽象类,不能实例化,只能作为基类使用。

#include <iostream>

class AbstractBase {
public:
    virtual void display() = 0;  // 纯虚函数
};

class ConcreteDerived : public AbstractBase {
public:
    void display() override {
        std::cout << "ConcreteDerived class" << std::endl;
    }
};

int main() {
    // AbstractBase a;  // 错误:不能实例化抽象类
    ConcreteDerived d;
    AbstractBase& ref = d;
    ref.display();  // 调用 ConcreteDerived::display
    return 0;
}

6. 总结

虚函数和虚表是 C++ 实现运行时多态的关键机制。通过虚函数,C++ 提供了一种灵活的方式来定义和使用多态接口,而虚表则在运行时确保了对正确函数的调用。尽管虚函数有一定的性能开销,但其带来的灵活性和可扩展性在很多情况下是值得的。在设计复杂系统时,合理使用虚函数可以大大提高代码的模块化和可维护性。