三、析构函数
3.1 概念
通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象随函数栈帧的销毁而销毁。而对象在销毁前会自动调用析构函数,完成对象中资源的清理工作。
3.2 特性
析构函数是特殊的成员函数,其特征如下:
- 析构函数名是在类名前加上字符 ~(表示与构造函数功能相反)。
- 无参数 无返回值类型。(不用写void)
- 对象在销毁前会自动调用析构函数,完成对象中资源的清理工作
- 对于对象中自定义类型的成员,编译器会自动调用其对应的析构函数。
- 一个类只能有一个析构函数,析构函数不能重载。
以下是Stack类的析构函数的实现:
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 3)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(DataType data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
// 其他方法...
//Stack类的析构函数,完成对象中动态内存的释放
~Stack()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
void TestStack()
{
Stack s;
s.Push(1);
s.Push(2);
}
提示:需要自己写析构函数的情况:类成员中有malloc,fopen,new等操作的需求。
3.3 编译器自动生成的析构函数
若未显式定义,系统会自动生成析构函数。
编译器自动生成的析构函数,是否会完成一些事情呢?
- 跟默认生成的构造函数类似,编译器生成的析构函数,对内置类型成员不做处理,对自定义类型成员调用它的析构函数。
还记得那道用栈实现队列的题吗【Leetcode.232】用栈实现队列:
class MyQueue{
//内置类型成员不做处理
int sz;
//对自定义类型成员调用它的析构函数
Stack input;
Stack output;
public:
//其他方法...
};
int main()
{
MyQueue mq;
return 0;
}
// main函数中并没有直接调用Stack类析构函数,而是先是调用了编译器为MyQueue类生成的默认析构函数
// 再由MyQueue类生成的析构函数调用两个Stack类成员变量的析构函数。
总结:
- 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;
- 有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
- 像Myqueue这样没有直接申请空间的类也可以不写析构函数,因为默认生成的析构函数会调用自定义成员的析构函数,完成资源的释放。
3.3 构造析构的顺序
局部顺序:
-
类的构造顺序:先定义的先构造;
-
类的析构顺序:先定义的后析构;
-
测试结果:
全局顺序:
- 全局的类最先进行初始化,在main函数之前就会创建并初始化。
- 局部的类按照定义的顺序进行初始化
- 全局类和局部静态类的生命周期都是在整个程序结束后才销毁。(静态区数据的特点)
- 而局部栈区类的生命周期随函数的返回而结束销毁。(栈区数据的特点)
- 无论是在栈区还是静态区存储的类都符合先定义的后析构的析构顺序。
- 测试结果:
提示:
- 静态变量(类)只在第一次定义的时候初始化他(调用析构函数)。
- 测试结果: