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