C++构造函数与析构函数的调用顺序
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
类型的成员对象member1
和member2
。在OuterObject
的构造函数执行前,会先按照成员对象在类定义中的声明顺序,依次调用member1
和member2
的构造函数。所以输出结果为:
MemberObject constructor called.
MemberObject constructor called.
OuterObject constructor called.
Inside main function.
OuterObject destructor called.
MemberObject destructor called.
MemberObject destructor called.
可以看到,member1
和member2
先构造,然后OuterObject
构造。销毁时,先调用OuterObject
的析构函数,然后按照与构造相反的顺序,依次调用member2
和member1
的析构函数。
继承体系下的构造与析构顺序
在继承体系中,构造函数和析构函数的调用顺序更为复杂。当创建一个派生类对象时,首先调用基类的构造函数,然后按照成员对象在派生类中声明的顺序调用成员对象的构造函数,最后调用派生类自身的构造函数。析构顺序则与构造顺序相反。
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. 数据一致性维护
在一些复杂的数据结构或系统中,对象之间存在依赖关系。构造函数和析构函数的正确调用顺序有助于维护数据的一致性。例如,在一个数据库连接池的实现中,连接池对象在构造时初始化连接,析构时释放连接,确保在程序运行过程中数据库连接的有效管理和数据一致性。
总结构造函数与析构函数调用顺序要点
- 单一对象:构造函数在对象创建时调用,析构函数在对象销毁时调用。
- 多个对象:构造顺序与声明顺序一致,析构顺序与构造顺序相反。
- 类中成员对象:成员对象构造顺序按类定义中声明顺序,析构顺序相反。
- 继承体系:创建派生类对象时,先调用基类构造函数,再按顺序调用成员对象构造函数,最后调用派生类构造函数;析构顺序相反。
- 动态内存分配:
new
分配对象时构造函数立即调用,delete
释放对象前析构函数调用;动态分配数组时,构造和析构按数组元素顺序。 - 异常处理:构造函数中抛出异常,已构造的成员对象和基类对象会被析构。
通过深入理解C++ 中构造函数与析构函数的调用顺序,开发者可以编写出更加健壮、高效且易于维护的代码,避免资源泄漏、数据不一致等常见问题。在实际编程中,应根据具体需求合理利用构造和析构函数,确保程序的正确性和稳定性。