一、析构函数基础概念

1.1 析构函数的定义

析构函数是 C++ 面向对象编程中用于对象销毁时进行资源清理的特殊成员函数。当对象生命周期结束时,析构函数会被自动调用,负责释放对象所占用的资源。

语法特征

  • 函数名与类名相同,前面加波浪号~

  • 没有返回值,也不指定 void

  • 没有参数,因此无法重载

  • 不能被显式调用(编译器自动调用)

1
2
3
4
5
6
7
8
9
10
11
12
class MyClass {
public:
// 构造函数
MyClass() {
std::cout << "构造函数被调用" << std::endl;
}

// 析构函数
~MyClass() {
std::cout << "析构函数被调用" << std::endl;
}
};

1.2 析构函数的调用时机

析构函数在以下情况会被自动调用:

  • 栈上创建的对象超出其作用域时

  • 堆上创建的对象被delete运算符删除时

  • 临时对象生命周期结束时

  • 程序结束时,全局对象和静态对象被销毁时

1
2
3
4
5
6
void demoFunction() {
MyClass obj1; // 栈上对象

MyClass* obj2 = new MyClass(); // 堆上对象
delete obj2; // 手动释放,触发析构函数
} // obj1超出作用域,触发析构函数

二、析构函数的实现原理

2.1 对象销毁的完整过程

当对象被销毁时,C++ 会执行以下操作:

  1. 执行析构函数体
  2. 销毁对象的非静态数据成员(按声明顺序的逆序)
  3. 释放对象所占用的内存

对于派生类对象,销毁过程遵循 "先派生类,后基类" 的顺序:

  • 先调用派生类析构函数

  • 再调用基类析构函数

1
2
3
4
5
6
7
8
9
10
11
12
13
class Base {
public:
~Base() { std::cout << "Base析构函数" << std::endl; }
};

class Derived : public Base {
public:
~Derived() { std::cout << "Derived析构函数" << std::endl; }
};

// 输出顺序:
// Derived析构函数
// Base析构函数

2.2 析构函数与内存管理

析构函数是 C++ 内存管理的重要机制,尤其对动态分配的资源至关重要:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class FileHandler {
private:
FILE* file; // 文件指针资源
public:
// 构造函数打开文件
FileHandler(const char* filename) {
file = fopen(filename, "r");
if (!file) {
throw std::runtime_error("无法打开文件");
}
}

// 析构函数关闭文件
~FileHandler() {
if (file) {
fclose(file); // 确保资源被释放
}
}
};

三、析构函数的高级应用

3.1 虚析构函数

当通过基类指针删除派生类对象时,若基类析构函数不是虚函数,会导致未定义行为(通常只调用基类析构函数,而不调用派生类析构函数)。

解决方法:将基类析构函数声明为虚函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Base {
public:
// 虚析构函数
virtual ~Base() {
std::cout << "Base虚析构函数" << std::endl;
}
};

class Derived : public Base {
private:
int* data;
public:
Derived() { data = new int[100]; }

// 派生类析构函数自动成为虚函数
~Derived() {
delete[] data; // 释放派生类资源
std::cout << "Derived析构函数" << std::endl;
}
};

// 正确用法
Base* obj = new Derived();
delete obj; // 先调用Derived析构函数,再调用Base析构函数

3.2 资源获取即初始化(RAII)

RAII 是 C++ 中管理资源的核心技术,其核心思想是:将资源的生命周期与对象的生命周期绑定

析构函数是 RAII 的关键实现机制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 智能指针的简化实现
template <typename T>
class SmartPointer {
private:
T* ptr;
public:
// 构造函数获取资源
explicit SmartPointer(T* p = nullptr) : ptr(p) {}

// 析构函数自动释放资源
~SmartPointer() {
delete ptr;
}

// 禁止复制(防止double free)
SmartPointer(const SmartPointer&) = delete;
SmartPointer& operator=(const SmartPointer&) = delete;

// 其他成员函数...
};

3.3 异常安全与析构函数

析构函数中不应该抛出异常,因为:

  • 若析构函数在栈展开过程中抛出异常,会导致程序终止

  • 可能导致资源泄露

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Resource {
public:
~Resource() {
try {
// 可能抛出异常的操作
releaseResource();
}
catch (...) {
// 捕获并处理异常,避免传播出去
std::cerr << "释放资源时发生错误" << std::endl;
// 可以记录日志或采取其他补救措施
}
}
};

四、析构函数的最佳实践

4.1 遵循 "零规则" 和 "三规则"

  • 零规则:如果类不需要手动管理资源,则不需要定义析构函数、复制构造函数和复制赋值运算符

  • 三规则:如果需要定义析构函数、复制构造函数或复制赋值运算符中的任何一个,通常需要定义全部三个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 遵循三规则的类示例
class Buffer {
private:
char* data;
size_t size;

public:
// 构造函数
Buffer(size_t s) : size(s), data(new char[s]) {}

// 析构函数
~Buffer() {
delete[] data;
}

// 复制构造函数
Buffer(const Buffer& other) : size(other.size), data(new char[other.size]) {
std::copy(other.data, other.data + other.size, data);
}

// 复制赋值运算符
Buffer& operator=(const Buffer& other) {
if (this != &other) {
delete[] data;
size = other.size;
data = new char[size];
std::copy(other.data, other.data + other.size, data);
}
return *this;
}
};

4.2 避免在析构函数中执行复杂操作

析构函数应保持简单,仅执行必要的资源释放操作:

  • 避免长时间运行的操作

  • 避免调用可能修改其他对象状态的函数

  • 避免递归调用可能导致析构函数再次被调用的函数

4.3 明确默认析构函数

当需要保留默认析构函数但又要将其声明为虚函数时,可以使用default关键字:

1
2
3
4
5
class Base {
public:
// 声明为虚函数的默认析构函数
virtual ~Base() = default;
};