📌 核心结论

在构造函数和析构函数中调用虚函数不构成多态,无论该函数是否声明为 virtual,都会调用当前正在构造/析构的类的版本。


🔍 原因分析

1. 对象的动态类型是分阶段的

class Base {
public:
    Base() { 
        func();  // 此时对象类型是 Base
    }
    virtual void func() { std::cout << "Base::func()" << std::endl; }
    virtual ~Base() { func(); }
};

class Derived : public Base {
public:
    Derived() { 
        func();  // 此时对象类型才是 Derived
    }
    void func() override { std::cout << "Derived::func()" << std::endl; }
    ~Derived() { func(); }
};

时间线:

new Derived() 过程:
├─ Base 构造    → 对象类型: Base     → 调用 Base::func()
└─ Derived 构造 → 对象类型: Derived  → 调用 Derived::func()

delete ptr 过程:
├─ Derived 析构 → 对象类型: Derived  → 调用 Derived::func()
└─ Base 析构    → 对象类型: Base     → 调用 Base::func()

2. 虚函数表指针(vptr)的动态切换

构造过程中 vptr 的变化:
┌──────────────────────────┐
│ 1. 分配内存              │
│    vptr = 未初始化        │
├──────────────────────────┤
│ 2. Base 构造函数         │
│    vptr → Base::vtable   │ ⚠️ 指向 Base 的虚函数表
│    调用 func()           │
│    → 查表 → Base::func() │
├──────────────────────────┤
│ 3. Derived 构造函数      │
│    vptr → Derived::vtable│ ✅ 更新为 Derived 的虚函数表
│    现在多态才生效        │
└──────────────────────────┘

析构过程(逆序):
┌──────────────────────────┐
│ 1. Derived 析构函数      │
│    vptr → Derived::vtable│ ✅ 还在 Derived
│    调用 func()           │
│    → Derived::func()     │
├──────────────────────────┤
│ 2. Derived 析构结束      │
│    vptr → Base::vtable   │ ⚠️ 退回 Base
├──────────────────────────┤
│ 3. Base 析构函数         │
│    vptr → Base::vtable   │
│    调用 func()           │
│    → Base::func()        │
└──────────────────────────┘

3. 安全性考虑(最重要的原因)

如果允许在构造/析构时多态调用,会导致严重问题:

class Base {
public:
    Base() { init(); }  // 假设能调用 Derived::init()
    virtual void init() {}
};

class Derived : public Base {
    std::vector<int> data;  // ⚠️ 在 Base() 执行时还未构造
    
public:
    void init() override { 
        data.push_back(42);  // ❌ 崩溃!data 不存在
    }
};

问题时间线:

1. new Derived()
2. 调用 Base()
3. Base() 中调用 init()
4. 【如果多态】→ 调用 Derived::init()
5. 访问 data 成员
6. ❌ data 还没构造 → 未定义行为

📊 完整示例

#include <iostream>

class Base {
public:
    Base() { 
        std::cout << "Base 构造" << std::endl;
        func(); 
    }
    
    virtual void func() { 
        std::cout << "Base::func()" << std::endl; 
    }
    
    virtual ~Base() { 
        std::cout << "Base 析构" << std::endl;
        func(); 
    }
};

class Derived : public Base {
public:
    Derived() { 
        std::cout << "Derived 构造" << std::endl;
        func(); 
    }
    
    void func() override { 
        std::cout << "Derived::func()" << std::endl; 
    }
    
    ~Derived() { 
        std::cout << "Derived 析构" << std::endl;
        func(); 
    }
};

int main() {
    std::cout << "=== 创建对象 ===" << std::endl;
    Base* ptr = new Derived();
    
    std::cout << "\n=== 正常调用(多态) ===" << std::endl;
    ptr->func();  // ✅ 多态:调用 Derived::func()
    
    std::cout << "\n=== 销毁对象 ===" << std::endl;
    delete ptr;
    
    return 0;
}

输出:

=== 创建对象 ===
Base 构造
Base::func()        ← Base() 中调用,非多态
Derived 构造
Derived::func()     ← Derived() 中调用,非多态

=== 正常调用(多态) ===
Derived::func()     ← ✅ 多态调用

=== 销毁对象 ===
Derived 析构
Derived::func()     ← ~Derived() 中调用,非多态
Base 析构
Base::func()        ← ~Base() 中调用,非多态

📋 对比表

调用场景 是否多态 调用版本
Base() 中调用 func() ❌ 否 Base::func()
Derived() 中调用 func() ❌ 否 Derived::func()
ptr->func() (完整对象) ✅ 是 Derived::func()
~Derived() 中调用 func() ❌ 否 Derived::func()
~Base() 中调用 func() ❌ 否 Base::func()

💡 关键要点

  1. 对象类型分阶段

    • 构造时:逐步"成为"派生类
    • 析构时:逐步"退化"为基类
  2. vptr 动态切换

    • 构造:Base vtable → Derived vtable
    • 析构:Derived vtable → Base vtable
  3. 安全第一

    • 防止访问未构造的成员
    • 防止访问已析构的成员
  4. 标准规定

    • C++11 §12.7/3 明确定义此行为
    • 构造/析构时调用的是当前类的版本

Q: 为什么不构成多态?

因为在构造/析构期间,对象的动态类型是当前正在构造/析构的类,而不是最终的派生类。虚函数表指针会在构造/析构过程中动态切换,确保只调用已构造部分的函数,这是为了安全性设计。

Q: 这是 bug 吗?

不是,这是 C++ 的设计特性。如果允许多态,会导致访问未构造或已销毁的成员,造成未定义行为。

Q: 如何避免这个问题?

  1. 不在构造/析构函数中调用虚函数
  2. 使用工厂方法在对象完全构造后初始化
  3. 使用两阶段初始化模式
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐