大家好,在这一小节中
我们来介绍一下析构函数。在正式介绍析构函数之前呢,
我想跟大家简单回顾一下郭老师在之前介绍的构造函数。
那么构造函数呢它是一种特殊的成员函数。 它的名称呢和类名是相同的,
那么对于构造函数而言呢,可以有参数,但是呢不能有返回值。
你可以在一个类里面设计多个构造函数,用于不同的初始化,
所以我们说呢,构造函数主要就是用来初始化对象的。
那么每生成一个新的对象都调用构造函数来对它进行初始化,
那么就会有人想到说,哎,那当这个对象消亡的时候
是不是也会有一个函数对这个消亡的工作,操作呢进行一些处理呢?
对了。所以C++语法呢就定义了析构函数来完成这些事。
那么析构函数呢它也是一个成员函数。 那么它的名称呢也和类名是相同的。
那么怎么样区别析构函数和构造函数呢?
析构函数会在函数名之前呢再添加一个一弯,
来表明说它是析构函数。此外呢,析构函数是没有参数和返回值的。
这一点呢,也是和构造函数相区别的。
另外呢,对于一个类而言,最多只能有一个析构函数。
这样的设计呢其实也是有道理的。构造函数呢因为可能会去初始化
不同的对象,所以需要通过函数重载生成多个构造函数。
但是析构函数呢,因为它只是对对象消亡前的一些善后的工作做处理,
所以一个析构函数就可以完成所有相应的操作。
那么,析构函数呢在对象消亡的时候
会被自动的调用到。在对象消亡前呢,去完成一些善后的工作。
比方说,去释放一些分配的内存空间,是吧? 那么在对象消亡的时候,如果你只是去
这个取消了这个对象的名称,而还保留了
相应的内存空间的话,那么内存是会被大量的泄露掉的。
那么,析构函数呢,就可以有效的去完成这样的一个工作。 那么在此之前的话呢,如果你需要去
用一个new动态分配的内存,那么在各个地方去编写ii语句
的时候,你就要确保程序的每条执行路径上
都能去释放掉这些内存。这样的过程呢,当然是比较复杂和麻烦的。
那么有了析构函数之后呢,只需要在析构函数里面去使用delete语句
就能够确保对象运行中,使用new分配的空间呢,在对象消亡的时候
都被释放掉了。我们在定义类的时候,如果没有写析构函数,
那么编译器呢会自动的生成一个缺省的析构函数。
当然这个缺省的析构函数它本身是不会做什么的,它不会去帮助你去释放用户申请的一些内存,
如果呢,你可以定义一个自己的析构函数的话,那么由于析构函数本身
只有一个,所以编译器就不再生成缺省的析构函数了。
我们来看一个具体的例子。我们有一个类呢,叫做class
String,
那么class String本身呢在它的构造函数里它有一个
new出了一个数组,啊,cahr型的数组,
那么,它始终一个char新去指向这个数组的首地址。
那么在你new出了这样的一片数组之后呢, 那么在析构函数里面还要对这个
内存空间进行释放的话,我们就需要去写一个相应的delete语句。
那么在调用析构函数的时候,它就会去自动的delete掉
相应你生成的这片内存空间。
那么要注意呢,在对于一个数组而言的话呢,要去delete掉的话呢要使用这个方括号,
否则如果只是写delete p的话, 那么它只是去delete掉了一个对象。
对于析构函数和数组而言的话呢,
因为数组里面实际上包含了若干个对象,
那么当这个对象数组本身的生命周期结束的时候,
那么它里面的每个元素的析构函数都会被相应的调用到,
我们来看一个具体的例子。 对于一个叫做class Ctest这样的一个类,我们为了去标记说,
哎,看一看什么时候这个析构函数被调用到,并且被调用多少次,
那么我们在自己定义的这个析构函数当中呢,cout这样的一个destructor
called这样的一个语句, 那么在main函数里头的话呢,我们首先去
这个定义了一个Ctest类下面的一个对象数组,啊,这个对象数组呢叫做array,
它包含了两个对象,之后的话呢,就打印输出End Main这样的一个语句。
那么这个程序呢,就到结束了。结束的时候之前呢,
我们就要释放相应的对象,那么这时候呢,程序就回去调用
相应的析构函数,那么这时候就应该会首先去输出
End Main,完了之后,我们因为调用了析构函数,那么析构函数呢,
会被调用几次呢?是因为我这里面有两个数组,
呃,有两个数组对象,啊,对象数组。
那么这时候的话呢,我们就可以看到说,array这个数组
在消亡的时候,其中呢包含了两个对象,
那么对应呢就会有两次析构函数的调用。
所以呢对应的这个输出的这个 结果的话呢就保护了这样三条语句。啊。其中这两个
输出语句呢对应的就是两个数组
元素中间的这个对象的消亡和产生的这个析构函数的调用。
那么除此之外的话呢,我们使用delete语句啊,也可以
导致析构函数的准格尔调用。那么本身的话呢,如果我们去定义了一个
Ctest的新的一个指针,
pTest,那么它指向谁呢?它指向了你new出来的一个Ctest,
那么每一次new的这个调用呢,都会使得
构造函数被调用到, 那么你new出了一片内存空间之后,你要释放掉的时候,我们就需要delete掉,
那么只要使用到delete运算,就有一个相应的析构函数被调用到。
那么这时候的话呢,pTest所指向的那片内存空间呢,就会被释放掉了。
同样的,如果你是new了一个数组, 那么,这个析构函数呢,构造函数相应的就会
相应的被调用三次。因为这个数组里面包含了有三个对象。
同样的,在delete的时候,我们需要调用析构函数三次,
才能完全释放掉pTest这个指针所指向的那片内存空间。
在了解了构造函数和析构函数之后呢,我们来看一个具体的例题,
看一看这两种函数都分别在什么样的时机下会被调用到。
我们有一个类呢称之为叫做class Demo,
那么这个类呢它有一个private的成员对象,变量,称为叫做int
id, 因为这个成员变量呢它是私有的,
所以我们要对它进行初始化的时候呢,我们通常利用这个构造函数来进行
这个初始化的传递。那么我们通过去使用这样的一个参数int i呢,
来将相应的值初始化给 这个私有的成员变量id,
之后的话呢,打印输出id等于相应的初始化的值,Constructed,
啊,这就是我们涉及的一个构造函数, 也就是说,我们当相应的这样一个对象在生成的时候,我们就可以看到说,
它对应的这个值,id的这个值的这个构造函数呢会被调用到。
同时呢,我们去设计了一个析构函数。
在这个析构函数里头的话呢,你要打印输出相应的id等于 具体的这个初始化的一个值,
Destructed,啊,也就是说对应的这个id等于某一个值的对象会被消亡掉。
我们来看有了这样的析构和构造函数之后的话呢,
它分别在程序实行的过程中间会被多次的调用到,分别在什么时候被调用到。
那么整个程序的话呢,首先我们看到了它会定义了一个
全局的变量d1,啊,那么同时它用1这样的一个常量来进行
初始化对象。 由于呢,这个d1本身就是一个对象,
啊,不管它是全局的还是局部的,那么它都要去对应调用。
构造函数,啊,它就会打印输出id=1 Constructed,id赋值为了1,
之后呢,程序就进入到了main函数。
那么在main函数里头的话呢,首先就定义了一个局部的变量d4,
同样的,它被初始化为4,那么 我们就会调,定义这个
调用构造函数,啊,id=4 Constructed
有了这样定义好了一个d4之后,这样的一个对象之后呢,我们执行下一条语句,
d4=6,我们把一个常量6赋给d4
这样的一个对象,这个语句呢看起来有一点点似曾相识,对吧,因为我们之前给大家介绍过叫做
类型转换构造函数,啊,这是一种特殊的构造函数。那么它本身呢,
会使得赋值语句的左右两个类型不一致的赋值呢,
变得可以一致化。这就是因为 编译器呢,会将这个邮编的常量
通过类型转换构造函数呢来进行实现。
通过生成定义一个临时的对象,啊,
来生成一个对应相应的一个
临时的demo对象, 把6呢赋给相应的d4,就可以实现了。
那么这时候的话呢,因为要生成一个临时的对象,
所以呢,我们会调用id=6 Constructed这样的一个 构造函数。同时呢,
当这个赋值完成临时对象呢,消亡的时候呢,我们又要对应去
使得这个临时对象呢被析构掉。
所以呢,在这个语句里面,
我们同时去调用了构造和析构两个函数来完成了这样一个赋值的操作。
所以这时候呢,d4的,d4的id呢,
就等于6。然后呢,我们就可以进一步
执行cout main这样的一个语句。之后呢,
我们进入到下一条。啊,demo d5,
那么对于d5这样的一个全局变量的话呢,它呢会有点特殊,区别于d4,
那么它有一个自己的作用于,看到没有,这有一对花括号,那么我们语法规定说呢,
这个离对象最近的一对花括号中间的范围呢, 就是它相应的作用域。
那么这个作用域呢就标志着这个对象
的生命,生命周期,啊,也就是说这个对象在离开作用域之后呢,就需要消亡掉。
所以呢我们对应在这个d5里头呢,首先是对应生成了构造函数。啊,初始化id=5,
当这个程序执行到下一条命令, 跃出花括号这个作用域之后呢,这个对象就要
相应的去析构掉,所以又调用id=5 Destructed,
除此之外呢,我们就进入到了func这样的一个函数里头,
那么这个函数首先定义了一个static静态的
变量,d2,d2的话呢用2来进行初始化,
所以呢,我们又调用了id=2 Constructed啊,构造函数,
之外呢,我们又定义了一个d3这样的一个全局的变量,
所以相应的呢,也去调用id=3 Constructed,
之后呢,就打印输出func, 那么这时候,function函数就结束了,
在这对花括号的作用域里头,那么它包含了两个变量,d2和d3,
但是d2呢,它因为是静态的,
那么静态这个呃,变量的话呢,它的消亡
会是在整个程序结束之时,所以呢在这个作用域下,
消亡的对象只有d3一个,那么id=3 Destructed
这样的一个析构函数呢就会被调用到了。
然后程序呢,就继续cout main ends,
那么整个程序呢,基本上就已经执行完毕,我们看一看说还剩下哪几个
对象没有被消亡掉,还需要在最后的析构函数调用来完成呢?
首先一个,就是d1,全局变量,
第二个,d2,
静态变量,
此外呢,还有d4,
就是在main函数里面对应的定义的一个局部变量,因为它还没有超出main函数的
作用域,那么这时候,我们就看到说析构函数
首先去析构掉id=6 Destructed啊,我们让这个 d4析构掉,
接着呢,就是id=2 Destructed,我们让d2这样的一个
静态的局部对象d2呢
消亡掉,最后呢,是这个d1,
全局对象,也被消亡掉。所以这个呢,就是整个
析构和构造函数分别调用的时机。
我们可以看到说呢,这个先被构造的对象
会最后被析构掉,这也是C++语法的一个设计的
一个思想。 此外的话呢大家要注意一点,
就是构造函数呢和析构函数可能会在不同的编译器中呢有不同的表现。
那么个别时候呢,会出现不一致的情况, 当然这中间呢包含有编译器本身有bug以及代码优化的措施,那么在我们这门课里,
啊,包括作业,考试,那么我们讨论的只是C++标准,啊,也就说标准规定在什么时机上,
这个,构造函数和析构函数呢会被调用到,它不仅牵涉编译器的问题,