C++ 虚函数、虚函数表、静态绑定与动态绑定笔记
C++ 虚函数、虚函数表、静态绑定与动态绑定笔记
1. 什么是虚函数?
- 虚函数(virtual function)是类的成员函数,由
virtual
关键字修饰。 - 虚函数的调用方式在运行时通过对象的实际类型决定,而不是在编译时由指针或引用的静态类型决定。
- 作用:
- 支持 运行时多态。
- 允许通过基类指针或引用调用派生类的重写函数。
- 语法:
class Base { public: virtual void func(); // 定义虚函数 };
2. 虚函数表(V-Table)
虚函数表是 C++ 编译器实现动态绑定的核心机制。
虚函数表的定义
- 每个包含虚函数的类都有一张 虚函数表 (Virtual Table, V-Table)。
- 表中存储:
- RTTI (Run-Time Type Information):运行时类型信息,存储类的类型信息。
- 虚函数地址:类中所有虚函数的函数地址。
虚函数表的生成
- 基类:当类中定义了虚函数时,编译器会为该类生成一张虚函数表,存储该类虚函数的地址。
- 派生类:派生类继承基类时会生成自己的虚函数表:
- 如果派生类未重写基类的虚函数,则直接继承基类的虚函数表。
- 如果派生类重写了基类的虚函数,则其虚函数表中对应位置的地址会被派生类的实现覆盖。
3. 虚函数表指针(V-Ptr)
- 每个对象在内存中包含一个 虚函数表指针 (V-Ptr),指向该类的虚函数表。
- 位置:
- 对象的前 4 个字节(在 32 位系统下)或前 8 个字节(在 64 位系统下)存储 V-Ptr。
- 作用:
- 指向所属类的虚函数表,用于动态绑定。
4. 虚函数表的内存布局
假设有如下类结构:
class Base {
public:
virtual void func1();
virtual void func2();
};
class Derived : public Base {
public:
void func1() override;
};
-
基类
Base
的虚函数表:V-Table for Base: ----------------- | RTTI Pointer | --> 存储 "Base" 类型信息 | Address of func1 --> &Base::func1 | Address of func2 --> &Base::func2
-
派生类
Derived
的虚函数表:V-Table for Derived: --------------------- | RTTI Pointer | --> 存储 "Derived" 类型信息 | Address of func1 | --> &Derived::func1 (重写后的地址) | Address of func2 | --> &Base::func2 (未重写的函数地址)
5. 静态绑定与动态绑定
绑定类型 | 绑定时间 | 适用函数类型 | 特点 |
---|---|---|---|
静态绑定 | 编译时 (Compile-time) | 普通函数 | 函数的调用地址在编译阶段确定,直接绑定到函数地址。 |
动态绑定 | 运行时 (Run-time) | 虚函数 | 函数的调用地址在运行时通过虚函数表查找,根据对象实际类型决定。 |
6. 静态绑定与动态绑定的代码对比
示例代码
#include <iostream>
using namespace std;
class Base {
public:
void staticFunc() {
cout << "Base::staticFunc()" << endl;
}
virtual void dynamicFunc() {
cout << "Base::dynamicFunc()" << endl;
}
};
class Derived : public Base {
public:
void staticFunc() {
cout << "Derived::staticFunc()" << endl;
}
void dynamicFunc() override {
cout << "Derived::dynamicFunc()" << endl;
}
};
int main() {
Base* basePtr = new Derived();
basePtr->staticFunc(); // 静态绑定,调用 Base::staticFunc()
basePtr->dynamicFunc(); // 动态绑定,调用 Derived::dynamicFunc()
delete basePtr;
return 0;
}
输出结果
Base::staticFunc()
Derived::dynamicFunc()
代码解析
basePtr->staticFunc()
:- 静态绑定:调用
Base
中的staticFunc
,因为静态绑定与指针的静态类型相关。
- 静态绑定:调用
basePtr->dynamicFunc()
:- 动态绑定:调用
Derived
中的dynamicFunc
,通过虚函数表实现。
- 动态绑定:调用
7. 虚函数表的工作流程
编译阶段
- 为包含虚函数的类生成虚函数表。
- 如果派生类重写了虚函数,则更新虚函数表中对应的函数地址。
- 对象在内存中存储虚函数表指针。
运行阶段
- 通过对象的虚函数表指针访问虚函数表。
- 根据虚函数表中的地址,调用对应的函数。
8. 动态绑定的实现机制
动态绑定的汇编实现
以以下代码为例:
class Base {
public:
virtual void func() {
cout << "Base::func()" << endl;
}
};
class Derived : public Base {
public:
void func() override {
cout << "Derived::func()" << endl;
}
};
int main() {
Base* basePtr = new Derived();
basePtr->func();
delete basePtr;
return 0;
}
对应的汇编代码:
mov eax, [basePtr] ; 将对象的地址加载到寄存器 eax
mov edx, [eax] ; 通过 V-Ptr 获取虚函数表地址
mov ecx, [edx + offset] ; 从虚函数表中读取虚函数地址
call ecx ; 调用虚函数
过程解析
basePtr
是一个Derived
类型的对象,[basePtr]
的前 4 字节存储虚函数表指针。mov edx, [eax]
:将虚函数表地址加载到寄存器edx
。mov ecx, [edx + offset]
:通过偏移量查找虚函数地址。call ecx
:调用从虚函数表中找到的虚函数地址。
9. 虚函数对内存的影响
对象的内存布局
对象类型 | 内存组成 |
---|---|
无虚函数对象 | 仅包含类的成员变量。 |
有虚函数对象 | 成员变量 + 虚函数表指针 (V-Ptr)。 |
虚函数表对内存的影响
- 虚函数表存储在只读数据区(
.rodata
),在运行时不可修改。 - 每个类对应一张虚函数表,虚函数数量决定了虚函数表的大小。
- 注意:虚函数的数量不影响对象的大小,但会增加虚函数表的大小。
10. 覆盖、隐藏与重载
概念 | 定义 | 适用范围 |
---|---|---|
覆盖 | 派生类中重写基类的虚函数,要求返回值、函数名、参数列表均相同。 | 虚函数 |
隐藏 | 派生类中定义了与基类同名但参数列表不同的函数,基类的函数被隐藏。 | 普通函数、非虚函数 |
重载 | 同一类中定义的同名函数,通过参数列表的不同实现区分。 | 所有成员函数 |
11. 面试常见问题总结
-
虚函数表的作用是什么?
- 用于实现动态绑定。
- 存储虚函数地址和 RTTI 类型信息。
-
虚函数会影响对象的大小吗?
- 对象大小增加一个虚函数表指针(V-Ptr)。
- 虚函数数量不会直接影响对象大小,但会增加虚函数表的大小。
-
动态绑定的核心机制是什么?
- 通过虚函数表在运行时查找函数地址并调用。
-
静态绑定与动态绑定的区别?
- 静态绑定在编译时确定调用函数,适用于普通函数。
- 动态绑定在运行时通过虚函数表查找函数地址,适用于虚函数。