29

根本原因一句话

执行父类构造函数期间,对象的 vptr 指向【父类虚表】,尚未更新为子类虚表;动态绑定依赖vptr,因此只能调用父类版本,无法触发子类重写函数。

完整时序(Base <- Derived)
Derived d;

  1. 开辟内存(原始内存,无有效vptr)

  2. 调用 Base::Base()
    ✅ 编译器隐式代码:设置 vptr = Base 的虚表
    ✅ 运行 Base 构造函数体,此时在构造内调用虚函数 → 查询Base虚表,执行Base版本

  3. Base构造完全结束

  4. 调用 Derived::Derived()
    ✅ 编译器隐式代码:覆盖 vptr = Derived 的虚表
    ✅ 运行 Derived 构造函数体,此时调用虚函数才会走Derived版本

两个关键本质概念

  1. 多态(动态绑定)的前提
    通过vptr查找虚函数;vptr存着当前对象“类型标签”。

  2. 对象是逐步构造出来的
    C++标准视角:
    在派生类构造函数开始执行前,这个对象还只是一个基类对象,派生部分还未初始化,派生类成员变量完全未构造。
    C++标准直接规定语义:
    当处于基类构造/析构函数中,将对象视作基类类型实例,动态绑定解析到基类。
    就算底层不谈vptr,从标准设计逻辑上也禁止多态。

一个极其重要的安全逻辑(设计层面的根源)

假设允许在父类构造里调用子类虚函数:
子类函数可能访问子类的成员变量,但是!子类成员还没有构造,内存未初始化,直接访问 = 未定义行为、崩溃。

C++委员会为了规避毁灭性的未定义行为,做出两条约束:

  1. 构造、析构过程中,vptr阶段性切换;

  2. 此时虚函数动态绑定锁定为当前正在构造/析构的类。
    底层实现(vptr切换)是实现手段;
    防止访问未构造的派生类成员,避免UB 才是语言设计的根本动机。
    配套:析构函数一模一样的逻辑
    virtual ~Base()
    {
    func(); // 只会调用Base::func
    }
    析构顺序:
    先执行Derived析构(vptr=Derived)→ 然后切换vptr=Base → 执行Base析构。
    Base析构内部vptr已经切回基类,不会调用子类函数。

面试标准分层回答(直接背)

  1. 底层实现角度
    继承构造时,先执行基类构造,此时vptr指向基类虚表,尚未更新为派生类虚表;虚函数依靠vptr动态绑定,因此只能调用基类版本。

  2. C++语言设计根本原因(加分项,面试官最爱听)
    派生类对象是分步构造的,基类构造执行阶段,派生类所有成员还未初始化。
    如果此时允许调用派生类虚函数,函数内部极有可能读写派生类未初始化的成员,引发未定义行为。
    为保证程序安全,C++标准规定:在基类构造/析构期间,对象视为基类实例,虚函数不会触发派生类实现。

高频延伸面试题

问:能不能在构造函数里使用多态?
答:语法层面无法直接实现;工程上采用工厂模式,先构造完成对象,再调用虚函数,保证vptr稳定。

避坑小误区

❌错误说法:“因为对象还没创建完”
不够精准。内存已经分配,但是派生子对象没有构造、vptr还没切换完成。

如果你需要,我提供一段可直接运行的代码,配合注释打印,直观展示vptr变化带来的现象。

posted @ 2024-01-01 00:01  hellsino  阅读(17)  评论(0)    收藏  举报