书接上文,下面介绍赋值构造函数和析构函数。
4 赋值构造函数
虽然网上经常叫赋值构造函数,但深究的话这种叫法是错误的。因为它并没有发生构造的操作。它触发的条件是两个已经存在的对象之间的赋值操作,没有新对象产生就不会触发构造操作,准确来说它应该叫赋值函数,或者可以说叫等号运算符重载。是不是还记得我上一篇提到过,等号运算符重载并不是构造函数。是不是有点凌乱,我因为这个还特地问了我的老师跟查了一些资料,差点自己都没有转过来。如果我这地方说错了,请一定一定要提出来,要不然我自己就会一直错误的使用下去。
由于上一篇已经对等号运算符重载(第三节拷贝构造函数的补充里面:重载赋值运算符)进行了较详细说明。这里就直接跳过了(偷点懒)。
调用时机总结
总结为三句话:
(1)如果对象不存在,且没用别的对象来初始化,就是调用了(默认/有参)构造函数;
(2)如果对象不存在,且新创建时用了别的对象(已经存在的)来初始化,就是拷贝构造函数;
(3)如果对象存在,用别的对象(已存在的)来给它赋值,就是赋值函数。
5 析构函数
析构函数是成员函数的一种(特殊的成员函数),它的名字与类名相同,但前面要加~,既没有参数也没有返回值。
一个类有且仅有一个析构函数。如果定义类时没写析构函数,则编译器生成默认析构函数。如果定义了析构函数,则编译器不生成默认析构函数。
析构函数的作用
对象销毁前,做释放内存的清理工作,避免造成了内存泄漏。
1、析构函数(destructor) 执行与构造函数相反的操作,当对象结束其生命周期时(例如对象所在的函数已调用完毕),系统将会自动执行析构函数。析构函数往往用来做“清理善后” 的工作(例如在建立对象时用new开辟了一片内存空间。)简而言之就是释放对象使用的资源,并销毁非static成员。
2、析构函数如果我们不写的话,C++ 会帮我们自动的合成一个,就是说:C++ 会自动的帮我们写一个析构函数。很多时候,自动生成的析构函数就可以很好的工作,但是一些重要的事迹,就必须我们自己去写析构函数。
简而言之就是:如果函数在初始化时没有申请资源(一般是内存资源),那么很少使用析构函数的。
3、按照 C++ 的要求,只要有 new 就要有相应的 delete 。这个 new 是在构造函数里 new 的,就是出生的时候。所以在死掉的时候,就是调用析构函数时,我们必须对指针进行 delete 操作。
函数模型:
~类型()
使用方法:
(1)不能主动调用,对象销毁前,系统会自动调用
(2)如果没有显式的定义,编译器会自动生成一个析构函数(什么也不做)
(3)析构函数不能重载。一般每有一次构造函数的调用就会对应有一次析构函数的调用。
示例代码:
#include <iostream>
#include <windows.h>
#include <string>
using std::string;
class Human
{
public:
Human();
~Human(); //析构函数
private:
string name = "zhang";
int age = 28;
int salary;
};
Human::Human()
{
name = "无名氏";
age = 18;
salary = 30000;
}
Human::~Human()
{
std::cout << "~Date()" << this << std::endl;
}
void test(){
Human d1;
}
int main(){
test();
system("pause");
return 0;
}
在test()函数中构造了对象d1(构造时先调用了构造函数),那么在出test()作用域前d1应该被销毁,此时将自动调用析构函数。
下面是程序的输出:
我们知道,在构造函数中,成员的在初始化是在函数体执行前完成的,并按照成员在类中出现的顺序进行初始化,而在析构函数中,首先执行函数体,然后再销毁成员,并且成员按照初始化的逆序进行销毁。下面提到的类类对象的销毁步骤就验证了析构函数是逆序进行销毁的。
什么是销毁?
我们一直在说析构函数的作用是在你的类对象离开作用域后释放对象使用的资源,并销毁成员。那么到底这里所说的销毁到底是什么?那么继续看下面这几行示例代码:
void example()
{
int a = 1;
int b = 2;
}
通过上面的代码,再回想我们平时在一个函数体内定义一个变量的情况,在上面的example函数中定义了a和b两个变量,那么在出这个函数之后,a和b就会被销毁(因为是局部变量,所以是栈上的操作)。那么如果是在一个指向动态开辟的一块空间的指针,我们都知道需要自己进行delete,否则就会造成内存的泄漏。
上面说到的这些,其实在类里面的情况和所说的是一样的,这就是系统合成析构函数体都为空的原因,系统自动析构函数并不需要做什么,当类对象出作用域时系统会释放你的内置类型的那些成员。但是像上面说的一样,如果,我的成员里有一个指针变量并且指向了一块你动态开辟的内存,那么像以前那样也需要自己来释放,此时就需要我们自定义一个析构函数,并在实现部分写释放内存的代码,这样在调用析构函数的时候就可以把你所有申请的资源进行释放。(这才是析构函数有用的地方)
不知道你是否注意过下面这些:
当我们在一个类中又新建一个类(即类类型对象的成员还有一个类类型对象),那么在析构函数里也会调用这个对象的析构函数,而且它的调用顺序是先执行内部类的析构函数,再执行上一层析构函数的。因为一般都是先进行外部类的构造函数才会执行类类的构造函数的,而析构函数的调用顺序跟构造函数的调用顺序是相反的。
后面如果学到了父类、子类(继承父类)这些的时候,就能很好的验证析构函数释放的顺序是跟构造函数的调用顺序是相反的。
补充:
(1)记得上一节内容中在说道阻止拷贝的时候用到了新标准中的delete这样词。
其实在析构函数中也可以使用。
...
~Human() = delete;
...
但如果我这么写了,又在底下创建Human类型的对象,那么这个对象将是无法被销毁的,其实编译器并不允许这么做,直接会给我们报错。(是不是很尴尬)
但其实是允许我们动态创建这个类类型对象的,像这样:Human* p = new Human;虽然这样是可行的,但当你delete p的时候依然会出错。
所以既然这样做的话既不能定义一个对象也不能释放动态分配的对象(出力不讨好的事),所以还是不要这么用为好喽。
(2)
一般在你显式的定义了析构函数的情况下,应该也把拷贝构造函数和赋值操作显式的定义。
看下面示例代码5.2:
class Human
{
public:
Human(); { p = new int; }
~Human() { delete p; }
private:
int *p;
};
int main()
{
Human human1;
Human human2(human1);
return 0;
}
成员中有动态开辟的指针成员,在析构函数中对它进行了delete,如果不显式的定义拷贝构造函数,当如上面5.2这样:Human human2(human1)来创建Human2,我们都知道默认的拷贝构造函数是浅拷贝,那么这么做的结果就会是human2的成员p和human1的p是指向同一块空间的,那么当调用析构函数的时候就会导致同一块空间被释放两次,程序会崩溃的!
好了,关于C++构造和析构函数的讨论就到此为止啦!
连续写两篇打字打的我手都酸了…