虚函数表(Virtual Table,简称 vtable)是 C++ 中实现 多态性 的关键机制之一。它用于支持类的虚函数的动态绑定(也叫运行时多态)。通过虚函数表,C++ 可以在程序运行时动态地调用不同子类的函数,而不是在编译时决定调用哪个函数。
以下是虚函数表的详细介绍,包括其工作原理和常见的面试问题。
1. 虚函数的概念
在 C++ 中,虚函数(virtual function)是指在基类中声明为 virtual
的函数,其目的是在派生类中可以进行重写(override)。虚函数可以让基类的指针或引用在运行时动态调用派生类的实现,而不是基类的实现。这样可以实现多态性。
class Base {
public:
virtual void show() {
std::cout << "Base class show()" << std::endl;
}
};
class Derived : public Base {
public:
void show() override {
std::cout << "Derived class show()" << std::endl;
}
};
在上面的例子中,Base
类的 show()
是虚函数,Derived
类重写了这个函数。如果我们通过 Base
类的指针指向 Derived
类的对象,调用 show()
时,会动态地调用 Derived
类的 show()
实现,而不是 Base
类的实现。
2. 虚函数表的概念
虚函数表(vtable)是编译器为每个含有虚函数的类自动生成的一个指针表,它记录了与该类相关的虚函数指针。虚函数表通过对象的 虚指针(vptr)来实现函数的动态调度。
每个含有虚函数的类都有一个虚函数表:
- 虚函数表中存储的是该类中所有虚函数的函数指针。
- 对象的每一个实例都包含一个指向该虚函数表的指针,称为虚指针(vptr)。
虚函数表的工作原理
- 当一个类包含虚函数时,编译器会为这个类生成一个虚函数表
vtable
。 - 每个对象的内存布局中会多一个虚指针
vptr
,它指向该类的虚函数表。 - 虚函数表中每个槽位存储着指向该类虚函数的函数指针。
- 当调用虚函数时,程序首先通过对象的
vptr
查找虚函数表,定位到相应的函数指针,并调用该函数。
虚函数表的例子
class Base {
public:
virtual void show() {
std::cout << "Base class show()" << std::endl;
}
virtual void display() {
std::cout << "Base class display()" << std::endl;
}
};
class Derived : public Base {
public:
void show() override {
std::cout << "Derived class show()" << std::endl;
}
};
在这个例子中:
Base
类有两个虚函数:show()
和display()
。Derived
类重写了show()
函数。
生成的虚函数表可能如下:
-
Base 类的虚函数表 (
Base::vtable
):函数序号 函数指针 0 Base::show
1 Base::display
-
Derived 类的虚函数表 (
Derived::vtable
):函数序号 函数指针 0 Derived::show
1 Base::display
当调用 show()
时,如果对象是 Base
类的实例,vptr
会指向 Base::vtable
,因此会调用 Base::show()
。而如果对象是 Derived
类的实例,vptr
会指向 Derived::vtable
,因此会调用 Derived::show()
。display()
没有在 Derived
类中重写,因此无论是 Base
对象还是 Derived
对象,都调用 Base::display()
。
3. 虚指针(vptr)的概念
虚指针(vptr
)是编译器为每个对象自动生成的一个指针,它指向对象所属类的虚函数表。每个对象都有自己的虚指针,但是指向同一个虚函数表。
- 当一个对象创建时,编译器会在对象的内存布局中插入一个指向虚函数表的指针
vptr
。 - 当调用虚函数时,程序通过
vptr
找到该对象所属的类的虚函数表,再通过表中的函数指针调用实际的函数。
4. 虚函数表和多继承
在多继承的情况下,C++ 可能会为类创建多个虚函数表。例如:
class Base1 {
public:
virtual void func1() { std::cout << "Base1::func1" << std::endl; }
};
class Base2 {
public:
virtual void func2() { std::cout << "Base2::func2" << std::endl; }
};
class Derived : public Base1, public Base2 {
public:
void func1() override { std::cout << "Derived::func1" << std::endl; }
};
在这种情况下,Derived
类可能会有两个虚函数表,一个是用于 Base1
的虚函数,另一个是用于 Base2
的虚函数。
5. 虚函数表的内存布局
对象的内存布局在含有虚函数时,会发生以下变化:
- 对象的开头处会增加一个指向虚函数表的指针
vptr
。 vptr
指向虚函数表,该表中记录了该类的虚函数的函数指针。- 如果类不含有虚函数,内存中就没有
vptr
。
6. 常见面试问题
(1) 什么是虚函数表?
- 虚函数表(vtable)是编译器在实现虚函数(virtual function)时生成的一个表格,用于存储类中所有虚函数的函数指针。
- 每个含有虚函数的类都有一个虚函数表,虚函数表的每一项是虚函数的入口地址。
(2) 虚函数表是如何实现多态的?
- 当基类指针或引用指向派生类对象时,程序通过虚指针(vptr)找到对应的虚函数表,然后根据虚函数表中的函数指针调用派生类中重写的虚函数,从而实现多态性。
- 动态绑定通过虚函数表实现,它允许在运行时根据实际对象类型调用对应的虚函数。
(3) 虚函数表在多继承中的实现?
- 在多继承的情况下,编译器通常为每个基类创建一个虚函数表,因此派生类的对象可能包含多个虚指针,每个指针指向各自基类的虚函数表。
(4) 虚函数和普通函数的区别?
- 普通函数是在编译时确定调用哪个函数,而虚函数是在运行时通过虚函数表动态确定调用哪个函数。
- 普通函数没有虚指针和虚函数表的开销,而虚函数需要通过虚指针找到对应的虚函数表,增加了调用的间接性。
(5) 虚函数表的性能开销?
- 虚函数表的使用引入了一定的开销,因为每次调用虚函数时,必须通过虚指针查找虚函数表,再调用相应的函数。这增加了一次间接寻址操作。
- 此外,虚指针占用了一定的内存,每个对象会多出一个指针的空间来存储虚指针。
(6) 如何避免虚函数的开销?
- 如果不需要运行时的多态行为,可以避免使用虚函数。例如,当类的行为不需要通过继承和重写来扩展时,可以不使用虚函数,这样可以避免虚函数表和虚指针的开销。