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

C++构造函数与析构函数的调用顺序

2023-12-166.8k 阅读

C++构造函数与析构函数的调用顺序基础概念

在C++ 中,构造函数和析构函数是两个非常重要的概念,它们分别用于对象的初始化和清理工作。构造函数在对象创建时被自动调用,而析构函数则在对象销毁时被自动调用。理解它们的调用顺序对于编写正确、高效且健壮的C++ 代码至关重要。

1. 构造函数

构造函数是一种特殊的成员函数,它的名字与类名相同,没有返回类型(包括void)。构造函数的主要作用是在对象创建时对对象的数据成员进行初始化。例如,考虑以下简单的类Point

class Point {
public:
    int x;
    int y;
    Point(int a, int b) {
        x = a;
        y = b;
    }
};

在上述代码中,Point(int a, int b)就是Point类的构造函数。当我们创建Point类的对象时,构造函数会被自动调用,例如:

Point p(10, 20);

这里,p对象被创建,构造函数Point(int a, int b)被调用,p.x被初始化为10,p.y被初始化为20。

2. 析构函数

析构函数也是一种特殊的成员函数,它的名字是在类名前加上波浪号~,同样没有返回类型。析构函数主要用于在对象销毁时释放对象所占用的资源,比如动态分配的内存等。还是以Point类为例,我们可以添加一个简单的析构函数:

class Point {
public:
    int x;
    int y;
    Point(int a, int b) {
        x = a;
        y = b;
    }
    ~Point() {
        // 这里可以添加清理资源的代码,对于Point类,暂时没有动态资源需要清理
    }
};

Point类的对象生命周期结束时,析构函数~Point()会被自动调用。

单一对象的构造与析构顺序

当我们创建并使用单一对象时,构造函数和析构函数的调用顺序是比较直观的。对象创建时,构造函数被调用;对象销毁时,析构函数被调用。以下面的代码为例:

#include <iostream>
class SingleObject {
public:
    SingleObject() {
        std::cout << "SingleObject constructor called." << std::endl;
    }
    ~SingleObject() {
        std::cout << "SingleObject destructor called." << std::endl;
    }
};
int main() {
    SingleObject obj;
    std::cout << "Inside main function." << std::endl;
    return 0;
}

在上述代码中,main函数中创建了SingleObject类的对象obj。程序执行时,首先调用SingleObject的构造函数,输出"SingleObject constructor called."。然后执行std::cout << "Inside main function." << std::endl;输出相应信息。当main函数结束,obj对象的生命周期结束,析构函数被调用,输出"SingleObject destructor called."。运行程序,输出结果如下:

SingleObject constructor called.
Inside main function.
SingleObject destructor called.

这清晰地展示了单一对象的构造函数和析构函数的调用顺序。

多个对象的构造与析构顺序

当存在多个对象时,情况会稍微复杂一些。对象的构造顺序与它们在代码中声明的顺序一致,而析构顺序则与构造顺序相反,遵循“后构造,先析构”的原则。

1. 同一作用域内多个对象

#include <iostream>
class ObjectA {
public:
    ObjectA() {
        std::cout << "ObjectA constructor called." << std::endl;
    }
    ~ObjectA() {
        std::cout << "ObjectA destructor called." << std::endl;
    }
};
class ObjectB {
public:
    ObjectB() {
        std::cout << "ObjectB constructor called." << std::endl;
    }
    ~ObjectB() {
        std::cout << "ObjectB destructor called." << std::endl;
    }
};
int main() {
    ObjectA a;
    ObjectB b;
    std::cout << "Inside main function." << std::endl;
    return 0;
}

在上述代码中,main函数内先声明了ObjectA类型的对象a,后声明了ObjectB类型的对象b。构造时,先调用ObjectA的构造函数,输出"ObjectA constructor called.",再调用ObjectB的构造函数,输出"ObjectB constructor called."。当main函数结束,对象销毁时,先调用ObjectB的析构函数,输出"ObjectB destructor called.",再调用ObjectA的析构函数,输出"ObjectA destructor called."。运行程序,输出结果如下:

ObjectA constructor called.
ObjectB constructor called.
Inside main function.
ObjectB destructor called.
ObjectA destructor called.

这体现了同一作用域内多个对象构造与析构的顺序。

2. 不同作用域内多个对象

#include <iostream>
class ObjectX {
public:
    ObjectX() {
        std::cout << "ObjectX constructor called." << std::endl;
    }
    ~ObjectX() {
        std::cout << "ObjectX destructor called." << std::endl;
    }
};
class ObjectY {
public:
    ObjectY() {
        std::cout << "ObjectY constructor called." << std::endl;
    }
    ~ObjectY() {
        std::cout << "ObjectY destructor called." << std::endl;
    }
};
int main() {
    {
        ObjectX x;
        std::cout << "Inner scope." << std::endl;
    }
    ObjectY y;
    std::cout << "Outer scope." << std::endl;
    return 0;
}

在上述代码中,ObjectX对象x在内部作用域中声明,ObjectY对象y在外部作用域中声明。程序执行时,先进入内部作用域,调用ObjectX的构造函数,输出"ObjectX constructor called.",然后输出"Inner scope."。当内部作用域结束,x对象销毁,调用ObjectX的析构函数,输出"ObjectX destructor called."。接着,在外部作用域创建ObjectY对象y,调用ObjectY的构造函数,输出"ObjectY constructor called.",再输出"Outer scope."。当main函数结束,y对象销毁,调用ObjectY的析构函数,输出"ObjectY destructor called."。运行程序,输出结果如下:

ObjectX constructor called.
Inner scope.
ObjectX destructor called.
ObjectY constructor called.
Outer scope.
ObjectY destructor called.

这表明不同作用域内的对象,构造顺序按照声明顺序,析构顺序则是构造顺序的逆序,即使不同作用域也遵循这一原则。

类中成员对象的构造与析构顺序

当一个类包含其他类的对象作为成员时,成员对象的构造与析构顺序也遵循特定规则。成员对象的构造顺序与它们在类定义中声明的顺序一致,而不是按照构造函数初始化列表中的顺序。析构顺序则与构造顺序相反。

#include <iostream>
class MemberObject {
public:
    MemberObject() {
        std::cout << "MemberObject constructor called." << std::endl;
    }
    ~MemberObject() {
        std::cout << "MemberObject destructor called." << std::endl;
    }
};
class OuterObject {
public:
    MemberObject member1;
    MemberObject member2;
    OuterObject() {
        std::cout << "OuterObject constructor called." << std::endl;
    }
    ~OuterObject() {
        std::cout << "OuterObject destructor called." << std::endl;
    }
};
int main() {
    OuterObject outer;
    std::cout << "Inside main function." << std::endl;
    return 0;
}

在上述代码中,OuterObject类包含两个MemberObject类型的成员对象member1member2。在OuterObject的构造函数执行前,会先按照成员对象在类定义中的声明顺序,依次调用member1member2的构造函数。所以输出结果为:

MemberObject constructor called.
MemberObject constructor called.
OuterObject constructor called.
Inside main function.
OuterObject destructor called.
MemberObject destructor called.
MemberObject destructor called.

可以看到,member1member2先构造,然后OuterObject构造。销毁时,先调用OuterObject的析构函数,然后按照与构造相反的顺序,依次调用member2member1的析构函数。

继承体系下的构造与析构顺序

在继承体系中,构造函数和析构函数的调用顺序更为复杂。当创建一个派生类对象时,首先调用基类的构造函数,然后按照成员对象在派生类中声明的顺序调用成员对象的构造函数,最后调用派生类自身的构造函数。析构顺序则与构造顺序相反。

1. 简单继承

#include <iostream>
class Base {
public:
    Base() {
        std::cout << "Base constructor called." << std::endl;
    }
    ~Base() {
        std::cout << "Base destructor called." << std::endl;
    }
};
class Derived : public Base {
public:
    Derived() {
        std::cout << "Derived constructor called." << std::endl;
    }
    ~Derived() {
        std::cout << "Derived destructor called." << std::endl;
    }
};
int main() {
    Derived d;
    std::cout << "Inside main function." << std::endl;
    return 0;
}

在上述代码中,Derived类继承自Base类。当创建Derived类的对象d时,首先调用Base类的构造函数,输出"Base constructor called.",然后调用Derived类的构造函数,输出"Derived constructor called."。当d对象销毁时,先调用Derived类的析构函数,输出"Derived destructor called.",再调用Base类的析构函数,输出"Base destructor called."。运行程序,输出结果如下:

Base constructor called.
Derived constructor called.
Inside main function.
Derived destructor called.
Base destructor called.

2. 继承与成员对象

#include <iostream>
class Member {
public:
    Member() {
        std::cout << "Member constructor called." << std::endl;
    }
    ~Member() {
        std::cout << "Member destructor called." << std::endl;
    }
};
class Base {
public:
    Base() {
        std::cout << "Base constructor called." << std::endl;
    }
    ~Base() {
        std::cout << "Base destructor called." << std::endl;
    }
};
class Derived : public Base {
public:
    Member member;
    Derived() {
        std::cout << "Derived constructor called." << std::endl;
    }
    ~Derived() {
        std::cout << "Derived destructor called." << std::endl;
    }
};
int main() {
    Derived d;
    std::cout << "Inside main function." << std::endl;
    return 0;
}

在这个例子中,Derived类继承自Base类,并且包含一个Member类型的成员对象member。创建Derived类对象d时,首先调用Base类的构造函数,输出"Base constructor called.",然后按照成员对象在Derived类中的声明顺序,调用Member类的构造函数,输出"Member constructor called.",最后调用Derived类自身的构造函数,输出"Derived constructor called."。销毁d对象时,先调用Derived类的析构函数,输出"Derived destructor called.",接着调用Member类的析构函数,输出"Member destructor called.",最后调用Base类的析构函数,输出"Base destructor called."。运行程序,输出结果如下:

Base constructor called.
Member constructor called.
Derived constructor called.
Inside main function.
Derived destructor called.
Member destructor called.
Base destructor called.

动态内存分配与构造析构顺序

当使用new运算符动态分配对象时,构造函数会在分配内存后立即被调用。而使用delete运算符释放动态分配的对象时,析构函数会在释放内存前被调用。

#include <iostream>
class DynamicObject {
public:
    DynamicObject() {
        std::cout << "DynamicObject constructor called." << std::endl;
    }
    ~DynamicObject() {
        std::cout << "DynamicObject destructor called." << std::endl;
    }
};
int main() {
    DynamicObject* ptr = new DynamicObject();
    std::cout << "Object created dynamically." << std::endl;
    delete ptr;
    std::cout << "Object deleted." << std::endl;
    return 0;
}

在上述代码中,使用new DynamicObject()动态创建了DynamicObject类的对象,并将其地址赋给指针ptr。此时,DynamicObject的构造函数被调用,输出"DynamicObject constructor called."。接着输出"Object created dynamically."。当执行delete ptr;时,DynamicObject的析构函数被调用,输出"DynamicObject destructor called.",然后内存被释放,最后输出"Object deleted."。运行程序,输出结果如下:

DynamicObject constructor called.
Object created dynamically.
DynamicObject destructor called.
Object deleted.

如果是动态分配数组,情况会有所不同。例如:

#include <iostream>
class ArrayObject {
public:
    ArrayObject() {
        std::cout << "ArrayObject constructor called." << std::endl;
    }
    ~ArrayObject() {
        std::cout << "ArrayObject destructor called." << std::endl;
    }
};
int main() {
    ArrayObject* arr = new ArrayObject[3];
    std::cout << "Array of objects created." << std::endl;
    delete[] arr;
    std::cout << "Array of objects deleted." << std::endl;
    return 0;
}

这里使用new ArrayObject[3]动态分配了一个包含3个ArrayObject对象的数组。构造时,会依次调用3次ArrayObject的构造函数。析构时,使用delete[] arr;会依次调用3次ArrayObject的析构函数。运行程序,输出结果如下:

ArrayObject constructor called.
ArrayObject constructor called.
ArrayObject constructor called.
Array of objects created.
ArrayObject destructor called.
ArrayObject destructor called.
ArrayObject destructor called.
Array of objects deleted.

异常处理与构造析构顺序

在C++ 中,异常处理也会影响构造函数和析构函数的调用顺序。当在构造函数中抛出异常时,已经构造的成员对象和基类对象会被正确地析构。

#include <iostream>
class MemberForException {
public:
    MemberForException() {
        std::cout << "MemberForException constructor called." << std::endl;
    }
    ~MemberForException() {
        std::cout << "MemberForException destructor called." << std::endl;
    }
};
class ExceptionClass {
public:
    MemberForException member;
    ExceptionClass() {
        std::cout << "ExceptionClass constructor start." << std::endl;
        throw std::runtime_error("Exception in constructor");
        std::cout << "ExceptionClass constructor end." << std::endl;
    }
    ~ExceptionClass() {
        std::cout << "ExceptionClass destructor called." << std::endl;
    }
};
int main() {
    try {
        ExceptionClass obj;
    } catch (const std::runtime_error& e) {
        std::cout << "Caught exception: " << e.what() << std::endl;
    }
    return 0;
}

在上述代码中,ExceptionClass的构造函数在执行过程中抛出了一个std::runtime_error异常。在抛出异常前,MemberForException类的对象member已经构造完成,所以会先输出"MemberForException constructor called.""ExceptionClass constructor start."。当异常抛出时,ExceptionClass的构造函数未完全执行完毕,不会输出"ExceptionClass constructor end."。同时,member对象会被析构,输出"MemberForException destructor called."。在catch块中捕获异常并输出异常信息"Caught exception: Exception in constructor"。运行程序,输出结果如下:

MemberForException constructor called.
ExceptionClass constructor start.
MemberForException destructor called.
Caught exception: Exception in constructor

这表明在构造函数中抛出异常时,已经构造的成员对象会被正确析构,避免了资源泄漏等问题。

构造函数与析构函数调用顺序的应用场景

理解构造函数和析构函数的调用顺序在实际编程中有很多重要的应用场景。

1. 资源管理

在涉及到动态内存分配、文件操作、网络连接等资源管理的场景中,构造函数用于获取资源,析构函数用于释放资源。例如,一个用于文件操作的类:

#include <iostream>
#include <fstream>
class FileHandler {
public:
    std::ofstream file;
    FileHandler(const char* filename) {
        file.open(filename);
        if (!file) {
            throw std::runtime_error("Failed to open file");
        }
        std::cout << "File opened successfully." << std::endl;
    }
    ~FileHandler() {
        file.close();
        std::cout << "File closed." << std::endl;
    }
};
int main() {
    try {
        FileHandler handler("test.txt");
        // 进行文件写入操作等
    } catch (const std::runtime_error& e) {
        std::cout << "Caught exception: " << e.what() << std::endl;
    }
    return 0;
}

在上述代码中,FileHandler类的构造函数打开文件,如果打开失败则抛出异常。析构函数关闭文件。由于构造和析构顺序的确定性,确保了文件在使用完毕后一定会被关闭,避免了文件资源泄漏。

2. 日志记录与调试

通过在构造函数和析构函数中添加日志输出,可以方便地追踪对象的生命周期,有助于调试。例如:

#include <iostream>
class DebugClass {
public:
    DebugClass() {
        std::cout << "DebugClass constructor called. Object created." << std::endl;
    }
    ~DebugClass() {
        std::cout << "DebugClass destructor called. Object destroyed." << std::endl;
    }
};
int main() {
    DebugClass obj;
    // 其他代码
    return 0;
}

在上述代码中,通过构造函数和析构函数的日志输出,可以清楚地看到DebugClass对象何时创建和销毁,有助于定位程序中的问题。

3. 数据一致性维护

在一些复杂的数据结构或系统中,对象之间存在依赖关系。构造函数和析构函数的正确调用顺序有助于维护数据的一致性。例如,在一个数据库连接池的实现中,连接池对象在构造时初始化连接,析构时释放连接,确保在程序运行过程中数据库连接的有效管理和数据一致性。

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

  1. 单一对象:构造函数在对象创建时调用,析构函数在对象销毁时调用。
  2. 多个对象:构造顺序与声明顺序一致,析构顺序与构造顺序相反。
  3. 类中成员对象:成员对象构造顺序按类定义中声明顺序,析构顺序相反。
  4. 继承体系:创建派生类对象时,先调用基类构造函数,再按顺序调用成员对象构造函数,最后调用派生类构造函数;析构顺序相反。
  5. 动态内存分配new分配对象时构造函数立即调用,delete释放对象前析构函数调用;动态分配数组时,构造和析构按数组元素顺序。
  6. 异常处理:构造函数中抛出异常,已构造的成员对象和基类对象会被析构。

通过深入理解C++ 中构造函数与析构函数的调用顺序,开发者可以编写出更加健壮、高效且易于维护的代码,避免资源泄漏、数据不一致等常见问题。在实际编程中,应根据具体需求合理利用构造和析构函数,确保程序的正确性和稳定性。