构造函数中调用虚函数是不安全的,因动态类型此时仅为当前类,vptr指向基类虚表,导致静态绑定而非动态分派。

不能安全调用。C++ 允许语法上写,但行为确定是静态绑定——调用的是当前正在构造的类的虚函数版本,而非派生类重写的版本。这不是 bug,是语言机制强制规定的结果。
构造函数里调用 virtual 函数实际走的是实调用
编译器在构造函数内对虚函数的调用不做动态查表,而是直接绑定到当前类的实现。原因很底层:vptr(虚表指针)在基类构造阶段只指向基类的 vtable,此时派生类部分尚未初始化,连成员变量都未构造,更别说虚函数重写逻辑了。
- 常见错误现象:
Base::Base()中调用virtual void init(),即使Derived重写了它,输出仍是Base::init() - 若该虚函数是纯虚且基类没提供定义,程序会在运行时报
Pure virtual function called错误并终止 - 调试时断点进不去派生类版本,容易误判为“虚函数没生效”或“重写失败”
Derived 对象构造时,Base 构造函数里的 this 动态类型就是 Base
C++ 标准明确要求:对象的动态类型在构造过程中是逐步演化的。进入 Base::Base() 时,对象的动态类型就是 Base;只有等 Base 构造完成、开始执行 Derived::Derived() 时,动态类型才“提升”为 Derived。
- 这意味着所有依赖动态类型的机制(包括虚函数分发、
dynamic_cast、typeid)在此阶段都只看到Base -
sizeof(*this)在Base构造函数里返回的是sizeof(Base),不是sizeof(Derived) - 哪怕你在
Base构造函数里把this转成Derived*,解引用未初始化的派生类成员也会触发未定义行为
替代方案必须绕开构造函数内的虚调用
想让派生类参与初始化逻辑,就得把虚函数调用挪到对象完全构造之后。没有“打补丁式”的技巧能绕过这个限制,异步延迟(比如 std::thread 或事件循环)反而更危险——线程可能在 Derived 成员初始化前就访问它们。
- ✅ 推荐用两阶段初始化:把
init()或setup()声明为虚函数,在构造完成后由用户显式调用 - ✅ 工厂函数封装:用
make_unique创建对象后立即调用虚函数,确保顺序可控() - ✅ 模板策略替代虚函数:如果行为差异在编译期可知,用模板参数传入策略类,彻底避开虚表
- ❌ 不要用
shared_from_this()在构造函数里获取shared_ptr——这是未定义行为
真正难处理的不是“怎么写”,而是“什么时候意识到不能写”。很多问题暴露在派生类需要读取自己刚初始化的成员、或注册回调时——那时虚函数早已被静态绑定了,而你还在调试为什么重写没进。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











