开发者

C++中构造函数的初始化顺序说明

目录
  • 关键规则
    • 对基类进行处理
    • 对成员对象进行处理
    • 派生类自己的构造函数
  • 特殊情况
    • 如果父类构造函数含有虚函数调用
    • 核心结论
  • 关于拷贝构造函数的初始化问题
    • 关于子类和父类的虚函数问题
      • 总结

        关键规则

        • 如果派生类有基类(单继承或多继承),基类的构造函数会首先被调用。

        对基类进行处理

        • 多继承时,按照派生类继承列表中声明的顺序(从左到右)依次调用基类的构造函数。
        • 如果有虚继承,虚基类的构造函数优先于非虚基类调用,且只调用一次
        • 虚基类只在最远派生类中进行处理,并且只有最远派生类调用,其他虚继承的派生类调用被忽略,并且只执行一次

        对成员对象进行处理

        • 在基类构造函数调用完成后,派生类中声明的成员对象的构造函数会被调用。
        • 成员对象的构造顺序遵循它们在类中声明的顺序(而不是初始化列表中的顺序)。
        • 如果成员对象有自己的构造函数,C++ 会根据初始化列表(如果提供)或默认构造函数来调用。

        派生类自己的构造函数

        • 最后,派生类自己的构造函数体被执行。

        特殊情况

        如果父类构造函数含有虚函数调用

        • 在父类的构造函数中调用虚函数,还是会执行父类的构造函数,不会跑到子类中去,即使有vitual,因为此时父类都还没有构造完成,子类也就还没有构造。
        #include <IOStream>
        using namespace std;
         
        class A{
          public:
            A ():m_iVal(0){test();}//这里的test会调用父类的virtual void func()
            virtual void func() { std::cout<<m_iVal<<' ';}
            void test(){func();}
          public:
              int m_iVal;
        };
        class B : public A{
          public:
            B(){test();}//这里的test会调用父类的test()进而通过指针匹配
            virtual void func(){
              ++m_iVal;
              std::cout << m_iVal << ' ';
              }
        };
        int main(int argc ,char* argv[]){
          A*p = new B;
          p->test();
          return 0;
        }
        

        输出结果

        0 1 2

        虚函数调用规则

        • 构造函数中:绑定到当前构造的类版本
        • 构造完成后:动态绑定到实际对象类型

        执行流程

        • A::A() → test() → A::func() → 输出0(vtable指向A)。
        • B::B() → test() → B::func() → m_iVal++,输出1(vtable指向B)。
        • p->test() → B::func() → m_iVal++,输出2。

        vtable切换

        • 基类构造:指向A。
        • 基类完成后,进入B::B()前:指向B。

        输出

        0 1 2。

        核心结论

        • 构造函数中虚函数调用取决于当前类类型,非最终类型。
        • vtable在基类构造后、派生类构造函数体前更新。
        • 注意:在执行构造函数体之前,可以认为构造函数**已经完全完成了相应对象的初始化工作,**在C++的实现中,虚函数表的切换发生在进入派生类构造函数体之前,而不是等到整个构造函数结束。

        关于拷贝构造函数的初始化问题

        • 在初始化时如果对象不存在,且没有声明explict禁用赋值操作
        • 编译器默认调用拷贝构造函数
        #include<iostream>
        using namespace std;
        
        class MyClass {
        public:
            MyClass(int i = 0) { // 构造函数
                cout << i;
            }
            MyClass(const MyClass &x) { // 拷贝构造函数
                cout << 2;
            }
            MyClass &operator=(const MyClass &x) { // 赋值运算符
                cout << 3;
                return *this;
            }
            javascript~MyClass() { // 析构函数
                cout << 4;
            }
        };
        
        int main() {
            MyClass obj1(1), obj2(2); /编程/ 创建 obj1 和 obj2
            MyClass objjavascript3 = obj1;      // 创建 obj3 并初始化
            return 0;
        }
        
        MyClass obj3 = obj1;

        由于obj3为被创建,那么调用拷贝构造函数,称为复制初始化

        MyClass obj3; // 先默认构造
        obj3 = obj1; // 再赋值

        这时会导致调用赋值运算符

        拷贝构造函数的什么时候被调用呢?

        场景示例代码说明
        对象初始化MyClass b = a;或者MyClass b(a);用已有对象初始化新对象
        按值传递参数void func(MyClass x);函数参数创建副本
        按值返回对象MyClass func() { … }返回局部对象(可能被优化,现如今的C++编译器普遍采用了RVO返回值优化导致返回时候不会进行拷贝)
        容器操作vec.push_back(a);插入对象到容器
        显式调用new MyClass(a);动态分配时拷贝

        关于子类和父类的虚函数问题

        如果父类函数不是 virtual,子类将其声明为 virtual:

        • 对基类无影响,基类调用仍javascript是静态绑定。
        • 从子类开始,函数成为虚函数,后续派生类可以实现多态,(即便是后续没有加virtual关键字也是多态)
        #include <iostream>
        class Base {
        public:
            void foo() { // 非虚函数
                std::cout << "Base::foo()" << std::endl;
            }
        };
        
        class Derived : public Base {
        public:
            virtual void foo() { // 子类声明为虚函数
                std::cout << "Derived::foo()" << std::endl;
            }
        };
        
        class GrandDerived : public Derived {
        public:
            void foo() { // 重写 Derived 中的虚函数
                std::cout << "GrandDerived::foo()" << std::endl;
            }
        };
        
        int main() {
            BasebWqHNALA* b1 = new Derived();
            b1->foo(); // 输出 "Base::foo()"
        
            Derived* d1 = new Derived();
            d1->foo(); // 输出 "Derived::foo()"
        
            Base* b2 = new GrandDerived();
            b2->foo(); // 输出 "Base::foo()"
        
            Derived* d2 = new GrandDerived();
            d2->foo(); // 输出 "GrandDerived::foo()"
            delete b1; delete d1; delete b2; delete d2;
            return 0;
        }
        

        父类函数非虚,没有多态

        • 因为 Base::foo() 不是虚函数,通过 Base* 指针调用 foo() 时**,总是执行 Base::foo()**,不会发生运行时多态。
        • 即使 Derived::foo() 被声明为 virtual,它对 Base 的函数没有影响,因为多态性需要从基类开始启用。

        子类声明为虚,影响后续继承

        • 在 Derived 中将 foo() 声明为 virtual,意味着从 Derived 开始,这个函数变成了虚函数。
        • 后续的派生类(如 GrandDerived)可以重写 Derived::foo(),并通过 Derived* 或 Derived& 调用时实现多态。
        • 但这种多态仅限于 Derived 及其子类,不追溯到 Base。

        隐藏而非重写

        • Derived::foo() 只是隐藏了 Base::foo(),而不是重写它。
        • 当通过 Base* 调用时,调用的仍然是 Base::foo(),因为 Base::foo() 不是虚函数。

        总结

        以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程客栈(www.devze.com)。

        0

        上一篇:

        下一篇:

        精彩评论

        暂无评论...
        验证码 换一张
        取 消

        最新开发

        开发排行榜