这一篇解决什么问题
上一篇弄清了虚表机制。这一篇看三个和虚表机制直接相关的”坑点”:
- 在构造函数里调虚函数,为什么不多态?
- 通过基类指针 delete 派生对象,为什么基类析构必须是 virtual?
- 纯虚函数和
__purecall报错是怎么回事。
原理:vptr 是在构造过程中被逐层换掉的
上一篇说”对象的 vptr 指向本类的虚表”,但构造一个 Derived 对象时,vptr 不是一开始就是 Derived 的表。编译器在每一层构造函数的入口处把 vptr 设置成当前这一层的表:
1 | new Derived 的过程: |
析构对称:进入 ~Derived 时 vptr 还是 Derived 的表,跑完后被设回 Base 的表,再执行 ~Base。
“构造中虚函数不多态”不是语言的特殊规定,而是 vptr 逐层切换的直接后果:在 Base 构造函数执行期间,这个对象的 vptr 指向的就是 Base 的表,f() 当然落在 Base::f。语言标准只是把这个实现事实规定成了行为。
虚析构:delete 基类指针的正确性
delete p(p 是 Base* 指向 Derived 对象)要做两件事:调析构、释放内存。调谁的析构?
- 基类析构是 virtual:查 vptr 的虚表 → 找到
Derived::~Derived→ 先跑派生析构,再链条式跑基类析构。正确。 - 基类析构不是 virtual:编译器按 p 的静态类型直接 call
Base::~Base,Derived 的析构被跳过——派生部分泄漏,行为未定义。
经验法则:一个类只要打算被基类指针 delete,析构就必须是 virtual。有虚函数的类基本都在此列。
纯虚函数与 __purecall
virtual void f() = 0; 让类成为抽象类,不能实例化。但有一种路径仍能调到纯虚函数:在 Base 的构造/析构里调 f()(此时 vptr 是 Base 的表,表项里没有实现)。MSVC 给这种表项填的是 __purecall 处理函数的地址,一旦调到就报运行时错误 “pure virtual function call” 并终止程序。遇到这个报错,第一嫌疑就是构造/析构链条里有人调了虚函数。
验证工程
1 | // 虚函数机制(下): 构造/析构中 vptr 切换, 虚析构, 纯虚函数. |
预测输出
动手跑之前先推一遍:
- Base 构造函数里 f() 输出
Base::f:此刻 vptr 是 Base 的表,尽管这个对象”将来”是 Derived。 - 两行 vptr 打印地址不同:构造过程中 vptr 被换了一次,肉眼可见。
- delete 后先
Derived dtor再Base dtor:虚析构让 delete 找到了正确的入口。 - 追加实验:把
~Base()的virtual去掉再跑一次,预测输出只剩Base dtor,Derived dtor消失——派生析构被静默跳过。
实测输出
基类析构是虚函数(工程原样):
1 | --- new Derived --- |
基类析构不是虚函数(去掉 virtual 再跑):
1 | --- new Derived --- |
逐行比对
Base ctor calls f() -> Base::f:命中。此刻 vptr 指向 Base 的表,对象”将来”是 Derived 也没用。- 两行 vptr 不同(
...ACB0→...AD38):命中,构造过程中 vptr 被逐层换掉,从打印直接看到。 - 虚析构版本先
Derived dtor再Base dtor:命中。 - 非虚析构版本只剩
Base dtor:命中,Derived dtor被静默跳过——编译器按指针的静态类型Base*直接 callBase::~Base,根本没查表。
有意思的是对比两次运行:上半部分(vptr 切换)一字不差,唯一的差别就在 delete 那两行。”基类析构必须是 virtual”这条经验法则,背后是删不删得干净的问题。
小结
1 | vptr 逐层切换: 每层 ctor 入口把 vptr 设为本层的表 |