C++类构造函数与析构函数的调用时机与手动调用分析
2023-05-267.3k 阅读
C++ 类构造函数与析构函数的调用时机
构造函数的调用时机
- 对象创建时
- 在 C++ 中,当定义一个类的对象时,构造函数会被自动调用。例如,定义一个简单的
Point
类:
- 在 C++ 中,当定义一个类的对象时,构造函数会被自动调用。例如,定义一个简单的
class Point {
public:
int x;
int y;
Point() {
x = 0;
y = 0;
std::cout << "Default constructor called" << std::endl;
}
};
- 当在主函数中创建
Point
对象时:
int main() {
Point p;
return 0;
}
- 输出结果为:
Default constructor called
。这里,在Point p;
这一行,构造函数Point()
被调用,对p
对象进行初始化。
- 动态内存分配时
- 使用
new
关键字动态分配对象时,构造函数也会被调用。例如:
- 使用
class Rectangle {
public:
int width;
int height;
Rectangle() {
width = 0;
height = 0;
std::cout << "Rectangle default constructor called" << std::endl;
}
};
- 在主函数中动态分配
Rectangle
对象:
int main() {
Rectangle* rect = new Rectangle();
delete rect;
return 0;
}
- 输出为:
Rectangle default constructor called
。这里new Rectangle()
调用了构造函数,为新分配的Rectangle
对象进行初始化。
- 函数参数传递时
- 当以值传递的方式将对象作为函数参数传递时,构造函数会被调用。考虑如下函数和类定义:
class Circle {
public:
int radius;
Circle() {
radius = 0;
std::cout << "Circle default constructor called" << std::endl;
}
Circle(const Circle& other) {
radius = other.radius;
std::cout << "Circle copy constructor called" << std::endl;
}
};
void printCircle(Circle c) {
std::cout << "Circle radius: " << c.radius << std::endl;
}
- 在主函数中调用
printCircle
函数:
int main() {
Circle c1;
printCircle(c1);
return 0;
}
- 输出为:
Circle default constructor called
Circle copy constructor called
Circle radius: 0
- 首先
Circle c1;
调用了默认构造函数,然后printCircle(c1);
以值传递方式传递c1
,调用了拷贝构造函数。
- 函数返回对象时
- 当函数返回一个对象时,构造函数(通常是拷贝构造函数或移动构造函数,C++11 引入移动语义后)会被调用。例如:
class Triangle {
public:
int side1;
int side2;
int side3;
Triangle() {
side1 = 0;
side2 = 0;
side3 = 0;
std::cout << "Triangle default constructor called" << std::endl;
}
Triangle(const Triangle& other) {
side1 = other.side1;
side2 = other.side2;
side3 = other.side3;
std::cout << "Triangle copy constructor called" << std::endl;
}
Triangle(Triangle&& other) noexcept {
side1 = other.side1;
side2 = other.side2;
side3 = other.side3;
other.side1 = 0;
other.side2 = 0;
other.side3 = 0;
std::cout << "Triangle move constructor called" << std::endl;
}
};
Triangle createTriangle() {
Triangle t;
t.side1 = 3;
t.side2 = 4;
t.side3 = 5;
return t;
}
- 在主函数中调用
createTriangle
函数:
int main() {
Triangle t = createTriangle();
return 0;
}
- 输出为:
Triangle default constructor called
Triangle move constructor called
- 在
createTriangle
函数中Triangle t;
调用了默认构造函数,return t;
调用了移动构造函数(因为 C++11 的返回值优化(RVO),这里优先调用移动构造函数)。
析构函数的调用时机
- 对象生命周期结束时
- 对于在栈上创建的对象,当对象所在的作用域结束时,析构函数会被自动调用。例如:
class Box {
public:
int length;
int width;
int height;
Box() {
length = 0;
width = 0;
height = 0;
std::cout << "Box constructor called" << std::endl;
}
~Box() {
std::cout << "Box destructor called" << std::endl;
}
};
- 在主函数中定义
Box
对象:
int main() {
{
Box b;
}
std::cout << "After box scope" << std::endl;
return 0;
}
- 输出为:
Box constructor called
Box destructor called
After box scope
- 当
Box b;
定义时,构造函数被调用,当b
所在的花括号作用域结束时,析构函数被调用。
- 动态分配对象被释放时
- 使用
delete
关键字释放通过new
分配的对象时,析构函数会被调用。例如:
- 使用
class Book {
public:
std::string title;
Book(const std::string& t) : title(t) {
std::cout << "Book constructor called for " << title << std::endl;
}
~Book() {
std::cout << "Book destructor called for " << title << std::endl;
}
};
- 在主函数中动态分配和释放
Book
对象:
int main() {
Book* book = new Book("C++ Primer");
delete book;
return 0;
}
- 输出为:
Book constructor called for C++ Primer
Book destructor called for C++ Primer
new Book("C++ Primer")
调用构造函数,delete book
调用析构函数。
- 容器中对象移除时
- 当从容器(如
std::vector
、std::list
等)中移除对象时,析构函数会被调用。例如:
- 当从容器(如
#include <vector>
class Fruit {
public:
std::string name;
Fruit(const std::string& n) : name(n) {
std::cout << "Fruit constructor called for " << name << std::endl;
}
~Fruit() {
std::cout << "Fruit destructor called for " << name << std::endl;
}
};
- 在主函数中使用
std::vector
操作Fruit
对象:
int main() {
std::vector<Fruit> fruits;
fruits.emplace_back("Apple");
fruits.emplace_back("Banana");
fruits.erase(fruits.begin());
return 0;
}
- 输出为:
Fruit constructor called for Apple
Fruit constructor called for Banana
Fruit destructor called for Apple
fruits.emplace_back("Apple")
和fruits.emplace_back("Banana")
调用构造函数,fruits.erase(fruits.begin())
移除Apple
对象,调用其析构函数。
C++ 类构造函数与析构函数的手动调用分析
构造函数手动调用的情况
- placement new 中的构造函数手动调用
- placement new 是一种特殊的
new
语法,它允许在已分配的内存上构造对象。在这种情况下,构造函数是手动调用的。例如:
- placement new 是一种特殊的
#include <new>
class Widget {
public:
int data;
Widget() {
data = 0;
std::cout << "Widget constructor called" << std::endl;
}
~Widget() {
std::cout << "Widget destructor called" << std::endl;
}
};
- 在主函数中使用 placement new:
int main() {
char buffer[sizeof(Widget)];
Widget* w = new (buffer) Widget();
w->~Widget();
return 0;
}
- 输出为:
Widget constructor called
Widget destructor called
- 这里
new (buffer) Widget();
手动调用了Widget
的构造函数,在预分配的buffer
内存上构造Widget
对象。之后手动调用析构函数w->~Widget();
来清理对象。
- 继承与委托构造函数中的特殊情况(非严格手动调用,但有手动控制部分)
- 在继承体系中,派生类构造函数可以调用基类构造函数。例如:
class Animal {
public:
std::string name;
Animal(const std::string& n) : name(n) {
std::cout << "Animal constructor called for " << name << std::endl;
}
~Animal() {
std::cout << "Animal destructor called for " << name << std::endl;
}
};
class Dog : public Animal {
public:
int age;
Dog(const std::string& n, int a) : Animal(n), age(a) {
std::cout << "Dog constructor called for " << name << " with age " << age << std::endl;
}
~Dog() {
std::cout << "Dog destructor called for " << name << " with age " << age << std::endl;
}
};
- 在主函数中创建
Dog
对象:
int main() {
Dog d("Buddy", 3);
return 0;
}
- 输出为:
Animal constructor called for Buddy
Dog constructor called for Buddy with age 3
Dog destructor called for Buddy with age 3
Animal destructor called for Buddy
- 这里
Dog
构造函数Dog(const std::string& n, int a) : Animal(n), age(a)
手动调用了Animal
构造函数Animal(n)
。虽然不是直接使用构造函数名调用,但对基类构造函数的调用有一定的手动控制成分。
析构函数手动调用的情况
- 配合 placement new 使用
- 如前面提到的 placement new 的例子,当使用 placement new 在已分配内存上构造对象后,需要手动调用析构函数来清理对象。例如:
#include <new>
class Gadget {
public:
double value;
Gadget() {
value = 0.0;
std::cout << "Gadget constructor called" << std::endl;
}
~Gadget() {
std::cout << "Gadget destructor called" << std::endl;
}
};
- 在主函数中:
int main() {
char buf[sizeof(Gadget)];
Gadget* g = new (buf) Gadget();
g->~Gadget();
return 0;
}
- 输出为:
Gadget constructor called
Gadget destructor called
- 手动调用析构函数
g->~Gadget();
清理通过 placement new 构造的Gadget
对象。
- 在对象提前清理场景下
- 有时候,可能需要在对象生命周期未正常结束时手动调用析构函数。例如,在一个复杂的数据结构中,当需要提前释放某个子对象的资源时。考虑如下场景:
class Node {
public:
int data;
Node* next;
Node(int d) : data(d), next(nullptr) {
std::cout << "Node constructor called with data " << data << std::endl;
}
~Node() {
std::cout << "Node destructor called with data " << data << std::endl;
}
};
class LinkedList {
public:
Node* head;
LinkedList() : head(nullptr) {
std::cout << "LinkedList constructor called" << std::endl;
}
void insert(int data) {
Node* newNode = new Node(data);
newNode->next = head;
head = newNode;
}
void removeFirst() {
if (head != nullptr) {
Node* temp = head;
head = head->next;
temp->~Node();
delete temp;
}
}
~LinkedList() {
while (head != nullptr) {
Node* temp = head;
head = head->next;
temp->~Node();
delete temp;
}
std::cout << "LinkedList destructor called" << std::endl;
}
};
- 在主函数中操作
LinkedList
:
int main() {
LinkedList list;
list.insert(10);
list.removeFirst();
return 0;
}
- 输出为:
LinkedList constructor called
Node constructor called with data 10
Node destructor called with data 10
LinkedList destructor called
- 在
removeFirst
函数中,temp->~Node();
手动调用了Node
的析构函数,提前清理即将被删除的节点资源。在LinkedList
的析构函数中,也手动调用了每个节点的析构函数来清理链表中的所有节点。
手动调用构造函数和析构函数的注意事项
- 内存管理
- 当手动调用构造函数(如使用 placement new)时,必须确保所使用的内存已经正确分配且大小合适。如果内存大小不足,构造函数可能会写入未分配的内存,导致未定义行为。同样,在手动调用析构函数后,对于动态分配的内存,需要正确使用
delete
操作符释放内存,否则会导致内存泄漏。例如:
- 当手动调用构造函数(如使用 placement new)时,必须确保所使用的内存已经正确分配且大小合适。如果内存大小不足,构造函数可能会写入未分配的内存,导致未定义行为。同样,在手动调用析构函数后,对于动态分配的内存,需要正确使用
#include <new>
class MyClass {
public:
int* arr;
MyClass() {
arr = new int[10];
std::cout << "MyClass constructor called" << std::endl;
}
~MyClass() {
delete[] arr;
std::cout << "MyClass destructor called" << std::endl;
}
};
- 在主函数中使用 placement new 错误示例:
int main() {
char buf[sizeof(int)];
MyClass* obj = new (buf) MyClass();
// 这里 buf 大小不足,构造函数 new int[10] 会导致未定义行为
obj->~MyClass();
return 0;
}
- 对象状态一致性
- 手动调用构造函数和析构函数时,要注意保持对象状态的一致性。例如,在手动调用析构函数后,对象应该处于一个合理的可销毁状态。如果在析构函数中只是部分清理资源,而对象后续还可能被访问,会导致错误。例如:
class DatabaseConnection {
public:
bool isConnected;
DatabaseConnection() {
isConnected = true;
std::cout << "DatabaseConnection constructor called" << std::endl;
}
~DatabaseConnection() {
isConnected = false;
std::cout << "DatabaseConnection destructor called" << std::endl;
}
void query(const std::string& q) {
if (isConnected) {
std::cout << "Executing query: " << q << std::endl;
} else {
std::cout << "Not connected, cannot execute query" << std::endl;
}
}
};
- 在主函数中错误使用手动调用析构函数:
int main() {
DatabaseConnection* conn = new DatabaseConnection();
conn->query("SELECT * FROM users");
conn->~DatabaseConnection();
conn->query("SELECT * FROM products");
// 这里在调用析构函数后,对象状态已改变,再次调用 query 会导致错误
delete conn;
return 0;
}
- 输出为:
DatabaseConnection constructor called
Executing query: SELECT * FROM users
DatabaseConnection destructor called
Not connected, cannot execute query
- 这种情况下,在调用析构函数后,对象不应该再被使用
query
方法,否则会产生逻辑错误。
- 继承体系中的注意事项
- 在继承体系中,手动调用构造函数和析构函数时要遵循正确的顺序。当手动调用派生类构造函数时,要确保先调用基类构造函数(如在派生类构造函数初始化列表中调用)。在手动调用析构函数时,析构函数的调用顺序与构造函数相反,先调用派生类析构函数,再调用基类析构函数。例如:
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;
}
};
- 在主函数中手动调用构造函数和析构函数(模拟 placement new 场景):
int main() {
char buf[sizeof(Derived)];
Derived* d = new (buf) Derived();
d->~Derived();
// 这里实际上也应该调用 Base 的析构函数,虽然在标准 placement new 场景下系统会处理,但手动模拟时要注意
return 0;
}
- 输出为:
Base constructor called
Derived constructor called
Derived destructor called
// 这里缺少 Base 析构函数调用输出,手动处理时应补充
- 在实际代码中,如果手动模拟这种场景,需要确保调用派生类析构函数后,也调用基类析构函数以正确清理对象资源。
通过对 C++ 类构造函数与析构函数调用时机及手动调用的详细分析,我们可以更深入地理解对象的生命周期管理,从而编写出更健壮、高效且内存安全的 C++ 程序。无论是在简单的对象创建与销毁场景,还是复杂的继承体系和内存管理场景下,准确把握构造函数与析构函数的行为都是至关重要的。在实际编程中,要谨慎使用手动调用构造函数和析构函数的方式,遵循内存管理和对象状态维护的原则,以避免出现未定义行为和逻辑错误。同时,利用现代 C++ 的特性,如智能指针等,可以更好地管理对象生命周期,减少手动管理的复杂性和错误风险。例如,在使用智能指针时,智能指针会在其生命周期结束时自动调用所指向对象的析构函数,大大简化了资源管理。
#include <memory>
class Resource {
public:
Resource() {
std::cout << "Resource constructor called" << std::endl;
}
~Resource() {
std::cout << "Resource destructor called" << std::endl;
}
};
int main() {
std::unique_ptr<Resource> res = std::make_unique<Resource>();
// 当 res 离开作用域时,Resource 的析构函数会自动被调用
return 0;
}
- 输出为:
Resource constructor called
Resource destructor called
- 这种方式避免了手动调用析构函数可能出现的错误,提高了代码的可靠性和可维护性。但了解手动调用的原理和场景仍然是必要的,因为在一些底层库开发或特殊需求场景下,可能需要更精细的控制对象的构造和析构过程。