目录
一、虚函数(Virtual Function)的概念
1.1 虚函数的定义
- 虚函数是通过
virtual
关键字声明的成员函数,允许在派生类中重写(覆盖)基类的实现。 - 核心作用:实现运行时多态(动态绑定),即通过基类指针或引用调用派生类的重写函数。
class Base {
public:
virtual void func() { cout << "Base::func" << endl; } // 虚函数
};
class Derived : public Base {
public:
void func() override { cout << "Derived::func" << endl; } // 重写虚函数
};
二、虚函数表(Virtual Table)的工作原理
2.1 虚函数表的基本结构
- **虚函数表(vtable)**是编译器为每个包含虚函数的类生成的一个静态数组。
- 虚函数表项:每个虚函数表项存储一个指向虚函数的函数指针。
- 虚函数表的生成规则:
- 每个类(包括派生类)都有自己的虚函数表。
- 如果派生类重写了基类的虚函数,则虚函数表中对应的函数指针会被更新为派生类的实现。
- 如果派生类没有重写某个虚函数,则虚函数表中保留基类的函数指针。
class Base {
public:
virtual void func1() {}
virtual void func2() {}
};
class Derived : public Base {
public:
void func1() override {} // 重写 func1
// func2 未重写
};
虚函数表示例:
Base
的虚函数表:[Base::func1, Base::func2]
Derived
的虚函数表:[Derived::func1, Base::func2]
2.2 虚函数指针(vptr)
- **虚函数指针(vptr)**是编译器自动添加到每个对象的隐藏指针。
- 作用:指向对象所属类的虚函数表。
- 特点:
- 在对象构造时初始化。
- 在继承链中被继承,指向当前类的虚函数表。
Base* b = new Derived(); // b->vptr 指向 Derived 的虚函数表
b->func1(); // 动态绑定到 Derived::func1
三、虚函数的调用流程
3.1 调用步骤
- 获取虚函数指针:通过对象的
vptr
找到虚函数表。 - 查找虚函数表项:根据函数在虚函数表中的偏移量(索引)找到对应的函数指针。
- 调用函数:通过函数指针跳转到实际的函数实现。
示意图:
对象内存布局:
+----------------+
| vptr | --> 虚函数表地址
+----------------+
| 其他成员变量 |
+----------------+
虚函数表:
+----------------+
| &Derived::func1| --> 函数指针
+----------------+
| &Base::func2 | --> 函数指针
+----------------+
四、虚函数与多态的关系
4.1 多态的实现条件
- 前提条件:
- 基类中声明虚函数。
- 派生类重写虚函数。
- 通过基类指针或引用调用虚函数。
Base* ptr = new Derived();
ptr->func(); // 动态绑定到 Derived::func
4.2 静态绑定 vs 动态绑定
- 静态绑定(早期绑定):在编译时确定函数调用(普通函数)。
- 动态绑定(晚期绑定):在运行时通过虚函数表确定函数调用(虚函数)。
五、虚函数的注意事项
5.1 性能开销
- 调用成本:虚函数调用需要两次间接寻址(
vptr
→ 虚函数表 → 函数地址),比普通函数稍慢。 - 内存占用:每个对象需要额外存储一个
vptr
(通常为 4 或 8 字节)。
5.2 纯虚函数与抽象类
- 纯虚函数:声明为
virtual void func() = 0;
的虚函数。 - 抽象类:包含至少一个纯虚函数的类,不能实例化对象。
- 作用:强制派生类实现接口。
class AbstractBase {
public:
virtual void pureFunc() = 0; // 纯虚函数
};
class Concrete : public AbstractBase {
public:
void pureFunc() override { /* 实现 */ }
};
5.3 构造函数与虚函数
- 构造函数中的虚函数调用:在构造函数中调用虚函数时,调用的是当前正在构造的类的版本(而非派生类的版本)。
- 原因:构造函数执行时,对象尚未完全构造完成,
vptr
指向当前类的虚函数表。
class Base {
public:
Base() { func(); } // 调用 Base::func
virtual void func() { cout << "Base::func" << endl; }
};
class Derived : public Base {
public:
Derived() { func(); } // 调用 Derived::func
void func() override { cout << "Derived::func" << endl; }
};
// 输出:
// Base::func (在 Base 构造函数中)
// Derived::func (在 Derived 构造函数中)
六、多继承与虚函数表
6.1 多继承下的虚函数表
- 每个基类的虚函数表独立存在:派生类会继承多个基类的虚函数表。
- 虚函数表的数量:取决于继承的基类数量(每个基类生成一个虚函数表)。
class Base1 {
public:
virtual void func1() {}
};
class Base2 {
public:
virtual void func2() {}
};
class Derived : public Base1, public Base2 {
public:
void func1() override {}
void func2() override {}
};
虚函数表结构:
Base1
的虚函数表:[Base1::func1]
Base2
的虚函数表:[Base2::func2]
Derived
的虚函数表:Base1
部分:[Derived::func1]
Base2
部分:[Derived::func2]
6.2 菱形继承问题与虚继承
- 菱形继承问题:两个基类继承自同一个祖先类,派生类中会有多个祖先类的副本。
- 解决方案:使用 虚继承(
virtual
关键字)共享祖先类。
class A {};
class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};
虚继承的虚函数表:
- 共享的祖先类
A
只生成一份虚函数表,避免冗余。
七、虚函数表的调试与查看
7.1 使用调试工具查看虚函数表
- GDB 命令:
info vtbl <对象>
查看虚函数表。 - LLDB 命令:
x/4wx <对象地址>
查看内存中的虚函数表指针。
7.2 代码示例
#include <iostream>
using namespace std;
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
};
class Derived : public Base {
public:
void func1() override { cout << "Derived::func1" << endl; }
};
int main() {
Base* b = new Derived();
b->func1(); // 输出 Derived::func1
b->func2(); // 输出 Base::func2
return 0;
}
调试输出:
Base::func1 (Derived::func1)
Base::func2 (Base::func2)
八、总结
8.1 核心要点
- 虚函数是实现运行时多态的核心机制。
- 虚函数表是编译器为每个类生成的静态数组,存储虚函数的地址。
- 虚函数指针(
vptr
)是对象的隐藏指针,指向虚函数表。 - 多继承下,对象可能包含多个虚函数表,虚继承可解决菱形继承问题。
8.2 设计建议
- 优先使用虚函数:当需要动态绑定时,使用虚函数。
- 避免过度使用虚函数:普通函数的调用效率更高。
- 合理设计抽象类:使用纯虚函数定义接口,强制派生类实现。