MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++构造函数调用顺序的深入解析

2023-03-303.9k 阅读

C++构造函数调用顺序概述

在C++编程中,构造函数是一种特殊的成员函数,用于在创建对象时初始化对象的成员变量。当涉及到复杂的类层次结构和对象组合时,理解构造函数的调用顺序至关重要。正确掌握构造函数的调用顺序,不仅有助于确保对象被正确初始化,还能避免潜在的运行时错误和内存泄漏。

C++中构造函数的调用顺序遵循一定的规则,这些规则与类的继承关系、成员变量的声明顺序以及对象的创建方式密切相关。接下来我们将逐步深入探讨这些规则。

单一类对象创建时构造函数的调用

当创建一个单一类的对象时,构造函数的调用相对简单。构造函数会按照其在类定义中声明的顺序,依次初始化类的成员变量,然后执行构造函数体中的代码。

下面是一个简单的示例代码:

#include <iostream>

class SimpleClass {
private:
    int num;
    double dbl;

public:
    SimpleClass() : num(0), dbl(0.0) {
        std::cout << "SimpleClass constructor called." << std::endl;
    }
};

int main() {
    SimpleClass obj;
    return 0;
}

在上述代码中,SimpleClass类有两个成员变量numdbl。构造函数SimpleClass()使用成员初始化列表对这两个成员变量进行初始化,并在构造函数体中输出一条消息。当在main函数中创建SimpleClass对象obj时,构造函数被调用,输出相应的消息。

继承体系下构造函数的调用顺序

当存在类的继承关系时,构造函数的调用顺序变得更加复杂。在创建派生类对象时,首先会调用基类的构造函数,然后按照声明顺序调用派生类的成员变量的构造函数,最后执行派生类构造函数体中的代码。

假设有一个基类BaseClass和一个派生类DerivedClass,代码如下:

#include <iostream>

class BaseClass {
public:
    BaseClass() {
        std::cout << "BaseClass constructor called." << std::cout;
    }
};

class DerivedClass : public BaseClass {
private:
    int num;

public:
    DerivedClass() : num(0) {
        std::cout << "DerivedClass constructor called." << std::endl;
    }
};

int main() {
    DerivedClass obj;
    return 0;
}

在这个例子中,当创建DerivedClass对象obj时,首先会调用BaseClass的构造函数,输出“BaseClass constructor called.”,然后初始化DerivedClass的成员变量num,最后执行DerivedClass构造函数体中的代码,输出“DerivedClass constructor called.”。

多层继承下的构造函数调用

如果存在多层继承,例如BaseClass -> IntermediateClass -> FinalClass,构造函数的调用顺序是从最顶层的基类开始,依次向下调用。即先调用BaseClass的构造函数,接着调用IntermediateClass的构造函数,最后调用FinalClass的构造函数。

以下是多层继承的代码示例:

#include <iostream>

class BaseClass {
public:
    BaseClass() {
        std::cout << "BaseClass constructor called." << std::endl;
    }
};

class IntermediateClass : public BaseClass {
public:
    IntermediateClass() {
        std::cout << "IntermediateClass constructor called." << std::endl;
    }
};

class FinalClass : public IntermediateClass {
public:
    FinalClass() {
        std::cout << "FinalClass constructor called." << std::endl;
    }
};

int main() {
    FinalClass obj;
    return 0;
}

在这个例子中,创建FinalClass对象obj时,会依次输出“BaseClass constructor called.”、“IntermediateClass constructor called.”和“FinalClass constructor called.”。

虚继承下构造函数的调用顺序

虚继承是C++中用于解决菱形继承问题的一种机制。在虚继承体系中,构造函数的调用顺序有所不同。对于虚基类,其构造函数由最底层的派生类调用,而且只调用一次。

假设有如下虚继承结构:

#include <iostream>

class VirtualBase {
public:
    VirtualBase() {
        std::cout << "VirtualBase constructor called." << std::endl;
    }
};

class Derived1 : virtual public VirtualBase {
public:
    Derived1() {
        std::cout << "Derived1 constructor called." << std::endl;
    }
};

class Derived2 : virtual public VirtualBase {
public:
    Derived2() {
        std::cout << "Derived2 constructor called." << std::endl;
    }
};

class FinalDerived : public Derived1, public Derived2 {
public:
    FinalDerived() {
        std::cout << "FinalDerived constructor called." << std::endl;
    }
};

int main() {
    FinalDerived obj;
    return 0;
}

在上述代码中,FinalDerived类继承自Derived1Derived2,而Derived1Derived2都虚继承自VirtualBase。当创建FinalDerived对象obj时,首先会调用VirtualBase的构造函数,然后按照继承列表的顺序依次调用Derived1Derived2的构造函数,最后调用FinalDerived的构造函数。输出结果为:

VirtualBase constructor called.
Derived1 constructor called.
Derived2 constructor called.
FinalDerived constructor called.

类中包含对象成员时构造函数的调用顺序

当一个类包含其他类的对象作为成员变量时,构造函数的调用顺序也遵循特定规则。在创建包含对象成员的类的对象时,首先会调用基类(如果有)的构造函数,然后按照对象成员在类中声明的顺序调用它们的构造函数,最后执行包含类的构造函数体。

以下是一个包含对象成员的类的示例:

#include <iostream>

class MemberClass {
public:
    MemberClass() {
        std::cout << "MemberClass constructor called." << std::endl;
    }
};

class ContainingClass {
private:
    MemberClass member;

public:
    ContainingClass() {
        std::cout << "ContainingClass constructor called." << std::endl;
    }
};

int main() {
    ContainingClass obj;
    return 0;
}

在这个例子中,ContainingClass类包含一个MemberClass类型的对象成员member。当创建ContainingClass对象obj时,首先会调用MemberClass的构造函数,输出“MemberClass constructor called.”,然后执行ContainingClass构造函数体中的代码,输出“ContainingClass constructor called.”。

复杂对象组合下的构造函数调用

如果一个类包含多个对象成员,并且这些对象成员之间存在依赖关系,那么构造函数的调用顺序就显得尤为重要。例如,假设ClassA依赖于ClassB,在ClassC中同时包含ClassAClassB对象成员,并且ClassB对象成员在ClassA对象成员之前声明,那么ClassB的构造函数会先被调用,从而确保ClassA在构造时可以依赖已初始化的ClassB对象。

以下是一个复杂对象组合的示例代码:

#include <iostream>

class ClassB {
public:
    ClassB() {
        std::cout << "ClassB constructor called." << std::endl;
    }
};

class ClassA {
private:
    ClassB& b;

public:
    ClassA(ClassB& b) : b(b) {
        std::cout << "ClassA constructor called." << std::endl;
    }
};

class ClassC {
private:
    ClassB b;
    ClassA a;

public:
    ClassC() : a(b) {
        std::cout << "ClassC constructor called." << std::endl;
    }
};

int main() {
    ClassC obj;
    return 0;
}

在上述代码中,ClassC包含ClassBClassA对象成员,ClassA依赖于ClassB。在ClassC的构造函数中,先按照声明顺序调用ClassB的构造函数,然后调用ClassA的构造函数,并将已构造的ClassB对象传递给ClassA的构造函数。输出结果为:

ClassB constructor called.
ClassA constructor called.
ClassC constructor called.

动态内存分配与构造函数调用

当使用new运算符动态分配对象时,构造函数的调用顺序与静态对象创建时类似。new运算符首先分配内存,然后调用构造函数对分配的内存进行初始化。

以下是动态分配对象的示例代码:

#include <iostream>

class DynamicClass {
public:
    DynamicClass() {
        std::cout << "DynamicClass constructor called." << std::endl;
    }
};

int main() {
    DynamicClass* ptr = new DynamicClass();
    delete ptr;
    return 0;
}

在这个例子中,new DynamicClass()首先为DynamicClass对象分配内存,然后调用DynamicClass的构造函数,输出“DynamicClass constructor called.”。当使用delete释放内存时,会调用析构函数(这里未展示析构函数相关内容,但析构函数的调用顺序与构造函数相反)。

数组动态分配时构造函数的调用

当动态分配对象数组时,构造函数会为数组中的每个元素依次调用。例如:

#include <iostream>

class ArrayClass {
public:
    ArrayClass() {
        std::cout << "ArrayClass constructor called." << std::endl;
    }
};

int main() {
    ArrayClass* arr = new ArrayClass[3];
    delete[] arr;
    return 0;
}

在上述代码中,new ArrayClass[3]为包含3个ArrayClass对象的数组分配内存,并依次为每个元素调用ArrayClass的构造函数,输出三次“ArrayClass constructor called.”。

构造函数调用顺序与初始化列表

初始化列表在构造函数调用顺序中起着重要作用。通过初始化列表,我们可以指定成员变量的初始化顺序,这可能会影响到实际的构造函数调用顺序。

例如,在以下代码中:

#include <iostream>

class InitListClass {
private:
    int num1;
    int num2;

public:
    InitListClass() : num2(0), num1(num2 + 1) {
        std::cout << "InitListClass constructor called." << std::endl;
    }
};

int main() {
    InitListClass obj;
    return 0;
}

虽然在初始化列表中num2先被初始化,但由于成员变量按照声明顺序初始化,实际上num1会先被初始化(使用其默认值),然后num2被初始化为0,最后num1被重新初始化为num2 + 1。因此,理解成员变量声明顺序和初始化列表的关系对于正确掌握构造函数调用顺序至关重要。

异常处理与构造函数调用顺序

当构造函数中发生异常时,构造函数的调用顺序会受到影响。如果在构造函数执行过程中抛出异常,已经构造的对象成员和基类对象会按照构造的相反顺序被析构。

以下是一个在构造函数中抛出异常的示例:

#include <iostream>
#include <stdexcept>

class ExceptionClass {
public:
    ExceptionClass() {
        std::cout << "ExceptionClass constructor start." << std::endl;
        throw std::runtime_error("Exception in constructor");
        std::cout << "ExceptionClass constructor end." << std::endl;
    }
};

class OuterClass {
private:
    ExceptionClass inner;

public:
    OuterClass() {
        std::cout << "OuterClass constructor start." << std::endl;
        try {
            inner = ExceptionClass();
        } catch (const std::runtime_error& e) {
            std::cerr << "Caught exception in OuterClass: " << e.what() << std::endl;
        }
        std::cout << "OuterClass constructor end." << std::endl;
    }
};

int main() {
    try {
        OuterClass obj;
    } catch (const std::runtime_error& e) {
        std::cerr << "Caught exception in main: " << e.what() << std::endl;
    }
    return 0;
}

在上述代码中,OuterClass包含ExceptionClass对象成员inner。当在OuterClass构造函数中尝试创建inner对象时,ExceptionClass构造函数抛出异常。此时,ExceptionClass对象尚未完全构造,OuterClass构造函数捕获异常并输出相应信息。由于ExceptionClass对象未完全构造,不会有相应的析构操作(如果ExceptionClass构造成功,在异常发生时会调用其析构函数)。

总结构造函数调用顺序要点

  1. 单一类:按照成员变量声明顺序初始化,然后执行构造函数体。
  2. 继承体系:先调用基类构造函数,再按声明顺序初始化派生类成员变量,最后执行派生类构造函数体。多层继承时从最顶层基类开始依次向下调用。虚继承时虚基类构造函数由最底层派生类调用且仅一次。
  3. 对象成员:先调用基类(如果有)构造函数,再按对象成员声明顺序调用其构造函数,最后执行包含类构造函数体。
  4. 动态内存分配new运算符先分配内存,再调用构造函数;动态分配对象数组时为每个元素依次调用构造函数。
  5. 初始化列表:成员变量按声明顺序初始化,初始化列表影响初始化值但不一定改变初始化顺序。
  6. 异常处理:构造函数中抛出异常时,已构造的对象成员和基类对象按构造相反顺序析构。

通过深入理解这些构造函数调用顺序的规则和要点,C++开发者能够编写出更健壮、可靠的代码,避免因构造函数调用不当而导致的各种问题。无论是简单的类还是复杂的继承和对象组合结构,清晰掌握构造函数调用顺序都是编写高质量C++程序的关键。