C++必看:C++构造函数的初始化顺序

关键规则

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

1. 对基类进行处理

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

2. 对成员对象进行处理

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

3.派生类自己的构造函数

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

特殊情况

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

  • 在父类的构造函数中调用虚函数,还是会执行父类的构造函数,不会跑到子类中去,即使有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

  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。
  3. vtable切换
    • 基类构造:指向A。
    • 基类完成后,进入B::B()前:指向B。
  4. 输出
    • 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;
    }
    ~MyClass() { // 析构函数
        cout << 4;
    }
};

int main() {
    MyClass obj1(1), obj2(2); // 创建 obj1 和 obj2
    MyClass obj3 = 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:

  • 对基类无影响,基类调用仍是静态绑定。
  • 从子类开始,函数成为虚函数,后续派生类可以实现多态,(即便是后续没有加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() {
    Base* 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() 不是虚函数。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值