Skip to content

C++ OOP 面试高频知识点 - 09

虚函数、虚表 vtable、虚指针 vptr 底层原理

1. 虚函数的基本概念

虚函数是 C++ 实现动态多态的核心机制。在基类中,通过 virtual 关键字声明的成员函数称为虚函数。当子类重新实现这些虚函数时,会发生函数重写(Override)。

class Animal {
public:
    virtual void makeSound() {
        cout << "Animal makes a sound" << endl;
    }
};

class Dog : public Animal {
public:
    void makeSound() override {
        cout << "Dog barks" << endl;
    }
};

int main() {
    Animal* animal = new Dog();
    animal->makeSound(); // 运行时调用 Dog::makeSound()
    delete animal;
    return 0;
}

2. 虚函数的实现机制

C++ 中虚函数的实现主要通过以下两个关键结构:

  1. 虚函数表(Virtual Table,简称 vtable):每个包含虚函数的类都有一个虚函数表,存储该类所有虚函数的地址
  2. 虚指针(Virtual Pointer,简称 vptr):每个对象都有一个虚指针,指向该类的虚函数表

2.1 虚函数表(vtable)

虚函数表是一个指针数组,其中每个元素是类的虚函数地址。当类被继承时,子类会继承基类的虚函数表,并可以修改其中的条目。

class Base {
public:
    virtual void foo() { cout << "Base::foo" << endl; }
    virtual void bar() { cout << "Base::bar" << endl; }
};

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

// Base 类的虚函数表
// [&Base::foo, &Base::bar]

// Derived 类的虚函数表
// [&Derived::foo, &Base::bar]

2.2 虚指针(vptr)

每个对象都有一个虚指针(通常位于对象内存的起始位置),它指向该对象所属类的虚函数表。

Base baseObj;
// 内存布局:vptr -> 指向 Base 类的 vtable

Derived derivedObj;
// 内存布局:vptr -> 指向 Derived 类的 vtable

3. 虚函数调用的过程

当通过指针或引用调用虚函数时,编译器会生成代码执行以下步骤:

  1. 获取对象的虚指针(vptr)
  2. 通过虚指针找到虚函数表(vtable)
  3. 根据虚函数的偏移量找到函数地址
  4. 调用该函数
Animal* animal = new Dog();
animal->makeSound();

// 底层执行过程:
// 1. 获取 animal 指向的对象的 vptr
// 2. 找到 Dog 类的 vtable
// 3. 在 vtable 中找到 makeSound() 的地址
// 4. 调用该函数(Dog::makeSound())

4. 虚函数表的层次结构

在继承关系中,子类的虚函数表会继承基类的虚函数表,并可以修改其中的条目。

class Base {
public:
    virtual void foo() = 0;
};

class Derived1 : public Base {
public:
    void foo() override { cout << "Derived1::foo" << endl; }
};

class Derived2 : public Base {
public:
    void foo() override { cout << "Derived2::foo" << endl; }
};

5. 虚析构函数的重要性

如果类中有虚函数,析构函数应该是虚函数,否则在通过基类指针删除子类对象时,只会调用基类的析构函数,而不会调用子类的析构函数,导致内存泄漏。

class Base {
public:
    virtual void foo() = 0;
    ~Base() { cout << "Base destructor" << endl; } // 不是虚析构函数
};

class Derived : public Base {
public:
    Derived() { data = new int[100]; }
    ~Derived() { 
        delete[] data; 
        cout << "Derived destructor" << endl; 
    }
    void foo() override {}

private:
    int* data;
};

int main() {
    Base* ptr = new Derived();
    delete ptr; // 只会调用 Base 的析构函数!
    return 0;
}

6. 虚函数的性能开销

虚函数调用会带来一定的性能开销,主要包括:

  1. 间接调用:通过 vptr 和 vtable 查找函数地址
  2. 阻止内联:虚函数通常不能被内联,因为调用的函数在编译时不确定
  3. 内存开销:每个对象都需要存储一个虚指针

7. 常见问题和回答

问题 1:虚函数可以是静态成员函数吗?

不可以,静态成员函数不能是虚函数,因为静态函数没有 this 指针,无法访问对象的虚指针。

class Base {
public:
    static virtual void foo() { // 错误:静态函数不能是虚函数
        cout << "Base::foo" << endl;
    }
};

问题 2:虚函数可以是构造函数吗?

不可以,构造函数不能是虚函数,因为对象在构造时虚指针尚未初始化。

class Base {
public:
    virtual Base() { // 错误:构造函数不能是虚函数
        cout << "Base constructor" << endl;
    }
};

问题 3:虚函数可以是友元函数吗?

不可以,友元函数不是成员函数,所以不能是虚函数。

class Base {
public:
    friend virtual void foo(Base& b) { // 错误:友元函数不能是虚函数
        cout << "foo" << endl;
    }
};

问题 4:虚函数可以被多次继承吗?

可以,虚函数支持多次继承,但需要注意菱形继承问题(使用虚继承解决)。

8. 虚函数的最佳实践

  1. 使用 override 关键字:明确重写意图,防止错误
  2. 确保虚析构函数:包含虚函数的类应该有虚析构函数
  3. 避免过度使用虚函数:只有在需要多态时才使用虚函数
  4. 考虑性能:虚函数会带来开销,在需要高性能的地方要谨慎使用

9. 底层原理实验

让我们通过一个简单的程序来观察虚指针的存在:

class Base {
public:
    int x;
};

class VirtualBase {
public:
    virtual void foo() {}
    int x;
};

int main() {
    cout << "Size of Base: " << sizeof(Base) << endl;         // 4 bytes
    cout << "Size of VirtualBase: " << sizeof(VirtualBase) << endl; // 8 bytes (包含 vptr)

    return 0;
}

总结

虚函数是 C++ 实现动态多态的核心机制,它通过虚函数表(vtable)和虚指针(vptr)实现运行时绑定。每个包含虚函数的类都有一个虚函数表,每个对象都有一个虚指针指向该类的虚函数表。当调用虚函数时,会通过虚指针和虚函数表找到正确的函数地址。

虚函数虽然强大,但也会带来一定的性能开销,并且需要正确使用虚析构函数以避免内存泄漏。


练习建议: 1. 实现一个继承层次,包含虚函数 2. 使用调试工具观察虚指针和虚函数表 3. 测试虚析构函数的重要性 4. 比较虚函数和普通函数的性能差异