LiSheng's blog LiSheng's blog
首页
笔记
个人简历
随笔集
GitHub (opens new window)
首页
笔记
个人简历
随笔集
GitHub (opens new window)
  • golang

  • cplus

    • 内存相关

    • 面向对象

      • 相关问题
      • 构造、析构顺序
      • 对象内存布局
      • 静态、动态多态
      • 虚函数表
      • 重载、重写、覆盖
      • 构造函数不能是虚函数
        • CRTP模式的原理和用途
        • 移动语义move
        • 构造函数初始化列表
        • 静态成员变量和函数
        • 多重继承
        • 菱形问题与虚继承
        • 对象池
      • STL相关

      • 内置数据结构
      • 数据结构示例
      • go和c++对比
      • 关键字

    • leetcode

    • 存储技术

    • 分布式系统

    • 计算机网络

    • Linux操作系统

    • Redis

    • 其他

    • 笔记
    • cplus
    • 面向对象
    lisheng
    2024-09-10
    目录

    构造函数不能是虚函数

    在C++中,构造函数不能是虚函数,这是一个常见的面试题。原因主要涉及C++对象模型、虚函数的工作机制以及构造函数的作用。以下是详细的解释。

    # 一、构造函数的作用

    构造函数的主要作用是初始化对象的成员变量和进行必要的资源分配。在创建对象时,构造函数是第一个被调用的成员函数,用于设置对象的初始状态。

    # 二、虚函数的工作机制

    虚函数允许在基类中定义一个接口,并在派生类中实现不同的版本。虚函数的实现依赖于虚函数表(vtable) 和 虚函数表指针(vptr),这些机制在对象的构造期间被初始化。

    • vtable 是一个指向虚函数的指针数组,每个包含虚函数的类都会有一个vtable。
    • vptr 是一个隐藏在每个对象中的指针,指向该对象所属类的vtable。

    在构造派生类对象时,基类的构造函数会先于派生类的构造函数执行。在这个过程中,vptr 会首先指向基类的vtable,只有在派生类的构造函数执行时,vptr 才会被更新为指向派生类的vtable。

    # 三、为什么构造函数不能是虚函数

    1. 虚函数依赖于vtable,但在构造函数期间,vtable尚未准备好:

    • 构造函数在对象创建的初期执行,此时对象的 vptr 还未完全设置好。如果构造函数是虚函数,那么调用该虚函数时,C++编译器无法确定调用哪个版本的虚函数,因为此时 vptr 可能指向基类的vtable,而不是派生类的vtable。

    2. 构造函数的调用顺序与虚函数的概念相冲突:

    • 在创建派生类对象时,首先调用基类的构造函数,再调用派生类的构造函数。此时,基类的构造函数不能依赖派生类的行为,因为派生类还未被完全构造。虚函数允许在基类中调用派生类的实现,而在构造过程中,这种行为是不可行的,因为派生类的部分尚未初始化。

    3. 构造函数的本质:

    • 构造函数的主要目的是确保对象处于有效状态,这个过程不涉及多态性。多态性通常是在对象完全构造好之后才有意义,因此在构造函数中使用虚函数没有实际意义。

    # 四、替代方案

    虽然构造函数不能是虚函数,但C++提供了其他机制来实现类似的功能:

    1. 使用虚函数在构造函数之后执行多态行为:

    • 虽然构造函数不能是虚函数,但你可以在构造函数之外调用虚函数,以便在对象完全构造后实现多态行为。
    class Base {
    public:
        Base() {
            initialize(); // 调用虚函数
        }
        virtual void initialize() {
            std::cout << "Base initialization" << std::endl;
        }
        virtual ~Base() {}
    };
    
    class Derived : public Base {
    public:
        void initialize() override {
            std::cout << "Derived initialization" << std::endl;
        }
    };
    
    int main() {
        Derived d;
        // Output: "Base initialization"
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22

    上面的代码中,由于在基类构造函数中调用了虚函数 initialize(),但由于此时 vptr 仍指向基类的vtable,调用的是基类的 initialize() 函数,而不是派生类的版本。

    2. 使用工厂方法(Factory Method):

    • 可以将对象的创建和初始化分开,通过工厂方法在对象构造后执行派生类特定的初始化逻辑。
    class Base {
    public:
        static Base* create();
        virtual void initialize() = 0;
        virtual ~Base() {}
    };
    
    class Derived : public Base {
    public:
        void initialize() override {
            std::cout << "Derived initialization" << std::endl;
        }
    };
    
    Base* Base::create() {
        Base* obj = new Derived();
        obj->initialize(); // 调用派生类的初始化方法
        return obj;
    }
    
    int main() {
        Base* b = Base::create();
        delete b;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24

    # 总结

    • 构造函数不能是虚函数 的主要原因是:在对象的构造阶段,虚函数表指针(vptr)尚未完全设置,无法支持多态行为。
    • 虚函数的概念与构造函数的调用顺序相冲突,因为派生类的部分尚未被初始化,基类的构造函数不能依赖派生类的实现。
    • 可以通过在构造函数之后调用虚函数或使用工厂方法等方式,间接实现类似的功能。

    理解这一概念有助于深入理解C++对象模型和构造/析构机制,也是C++面试中的常见考点。

    编辑 (opens new window)
    上次更新: 2024/09/13, 11:59:12
    重载、重写、覆盖
    CRTP模式的原理和用途

    ← 重载、重写、覆盖 CRTP模式的原理和用途→

    最近更新
    01
    ceph分布式存储-对象存储(RGW)搭建
    10-27
    02
    ceph分布式存储-集群客户端连接
    10-27
    03
    ceph分布式存储-管理crushmap
    10-27
    更多文章>
    Theme by Vdoing
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式