目录
析构函数
概念
通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由
编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。类似于之前常写的destroy函数。
特性
析构函数是特殊的成员函数,其特征如下:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载(本质原因就是清理结果是确定的,不像初始化的结果是多样的。构造函数只要想写,可以写一堆出来,但是析构函数是对资源的清理,说白就是各种置“零”)
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
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 CheckCapacity()
{
if (_capacity == _size)
{
int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
_capacity = newcapacity;
DataType* tmp = (DataType*)malloc(sizeof(DataType)*newcapacity);
if (tmp == NULL)
{
perror("malloc fail");
exit(-1);
}
_array = tmp;
}
}
void Push(DataType data)
{
CheckCapacity();
_array[_size] = data;
_size++;
}
// 其他方法...
~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);
}
int main()
{
TestStack();
return 0;
}
通过调试,我们发现析构函数确实做到了在对象的生命周期结束的时候,由编译器自动调用,完成了对对象资源的清理。
5. 关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器
生成的默认析构函数,对自定类型成员调用它的析构函数
class Time
{
public:
~Time()
{
cout << "~Time()" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
程序运行结束后输出:~Time()
在main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?因为:main方法中创建了Date对象d,而d中包含4个成员变量,其中_year, _month,
_day三个是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而_t是Time类对象,d销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。但是:main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构函数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部。所以这里的time函数是被编译器产生的Date对象的析构函数所间接调用的,而非直接调用。
调用Time类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁
main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析
构函数
注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数
6. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如
Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。 申请资源具体指的就是有没有一些自定义类型向内存申请空间了,如果不去写自定义的析构函数的话,可能会造成内存泄漏。这里的Date类就很经典,因为其内部全是内置型的对象,系统不会对其进行资源清理,系统最后直接将其内存收回即可。
析构函数的调用顺序
下面来看一道例题:
设已经有A,B,C,D4个类的定义,程序中A,B,C,D析构函数调用顺序为?( )
C c;
int main()
{
A a;
B b;
static D d;
return 0;
}
A.D B A C
B.B A D C
C.C D B A
D.A B D C
析构函数的调用顺序要从两个方面考察:首先是在程序运行时对象的声明周期,其次就是函数栈帧开辟与销毁时栈后进先出的特性。
基于这两点可得:
局部对象的生命周期最短,因此也是最先去调用析构函数的,局部对象是在栈上分配的,后创建的对象先被销毁,这样可以确保资源的正确释放。其次就是局部的静态变量,局部静态对象的生命周期贯穿整个程序的执行过程,直到程序结束。再其次是全局对象,全局对象是在静态存储区分配的,后定义的对象先被销毁。
因此题目调用顺序是B A D C ,选B选项。