虚函数调用在编译期确定“查vtable+调用”指令模板,运行时通过this对象的vptr和偏移量动态决定具体函数;非虚函数则编译期直接绑定地址。

虚函数调用时,编译器到底在哪儿决定调哪个函数?
虚函数的“动态绑定”不是运行时才临时查表,而是编译器在生成调用指令时就写死了“去 vtable 里取第 N 个函数指针”——真正不确定的是那个“N”和“vtable 地址”,它们得等运行时对象确定了才能拿到。
比如 obj->func() 是虚函数,编译器不直接生成跳转到 Derived::func 的指令,而是生成类似“取 this 指向对象的首地址 → 读前 8 字节(vptr)→ 加偏移查 vtable → 调用该地址函数”的固定模式。这个模式是静态决定的,但具体调谁,取决于 this 当时指向的对象类型。
- 非虚函数:编译期直接绑定到符号地址,连 vtable 都不碰
- 虚函数:编译期绑定到“查 vtable + 调用”这一操作模板,不绑定目标函数体
- 如果
func()是内联虚函数?编译器通常放弃内联,因为无法在编译期确认目标实现
为什么父类指针调子类对象,sizeof 却不变?
因为虚函数机制靠的是隐式插入的 vptr(虚函数表指针),不是靠改变对象内存布局的“大小”。所有含虚函数的类,编译器自动在对象开头加一个 void* 大小的 vptr 字段——不管它有多少个虚函数,也不管它继承了几层。
这意味着:Base 和 Derived 如果都含虚函数,sizeof(Base) 和 sizeof(Derived) 可能相等(除非 Derived 自己新增了成员变量);但它们的 vtable 内容不同,vptr 指向的地址也不同。
立即学习“C++免费学习笔记(深入)”;
- vptr 插入位置固定:对象内存起始处(MSVC/GCC/Clang 一致)
- 没有虚函数的类,
sizeof不额外加 vptr 空间 - 多重继承下 vptr 可能不止一个,但
sizeof仍只反映“最外层对象”的布局,不体现中间基类的 vptr
static_cast 和 dynamic_cast 对虚函数调用的影响差别在哪?
关键不在“能不能调虚函数”,而在于“你传给虚函数调用的 this 指针是否合法”。虚函数本身不关心指针怎么来,只关心它指向的对象有没有正确的 vptr 和 vtable。
static_cast 做的是编译期偏移计算,不验证对象真实类型;dynamic_cast 运行时检查 vtable 是否匹配目标类型——如果 cast 失败返回空指针,此时再调虚函数就是未定义行为(大概率 crash 在 vptr 解引用那步)。
- 错误示例:
Base* p = new Base; Derived* d = static_cast→(p); d->virt_func(); d的 vptr 实际指向Base的 vtable,但Derived::virt_func的偏移可能越界,调用会跳错地址 - 安全做法:用
dynamic_cast后判空,再调用 - 注意:
dynamic_cast要求源类型至少有一个虚函数(否则编译不过),本质是依赖 vtable 存在
虚析构函数没写,为什么有时程序也不崩溃?
不崩溃 ≠ 正确。析构函数是否虚,只影响“通过基类指针 delete 派生类对象”这一种场景下的行为。如果没写虚析构,delete 时只会调用基类析构函数,派生类部分的资源(如堆内存、文件句柄)不会被清理——但这些资源泄漏或未关闭,不一定立刻触发 crash。
常见掩盖现象:派生类没分配额外资源、析构函数为空、资源释放逻辑在其他地方做了补偿……这些会让问题延迟暴露,直到换编译器、开优化、改内存布局后突然崩在奇怪的地方。
- 只要类设计为被继承,且有指针指向它(尤其多态使用),就必须声明虚析构
- 纯虚析构函数也要提供定义:
virtual ~Base() = 0; Base::~Base() {} - 现代 C++ 推荐用
std::unique_ptr管理多态对象,它会在编译期检查析构函数是否可访问,间接帮你发现漏写的虚析构
虚函数绑定的时机藏在汇编指令结构里,而不是语言描述中;最容易忽略的,是以为“没 crash 就没问题”,其实资源泄漏和未定义行为往往在脱离调试环境后才浮现。










