• 详解c++----类和对象(二)


    前言

    啊哈亲爱的小伙伴们大家好啊,经过上面的学习想必大家对类和对象有了一点点的了解,那么接下来我们的这篇文章将继续沿着上篇文章的节奏接着来跟大家讲讲类和对象的一些类容,那么这里我们就主要来讲讲六大默认构造函数的前两大,那么我们废话不多说直接开始。

    为什么会有默认成员函数

    我们在学习这个知识之前我们先来聊聊为什么会有这个东西?他存在的意义又是什么?那么首先我们可以根据默认成员函数来联想一个我们之前学的一个知识点就是成员函数,我们说在类中创建的一些函数都可以将其称为成员函数,比如说下面的代码我们就创建了一个类,这个类的功能就是实现一个栈:

    #include
    typedef int DataType;
    class Stack
    {
    public:
    	void Init()
    	{
    		_array = (DataType*)malloc(sizeof(DataType) * 3);
    		if (NULL == _array)
    		{
    			perror("malloc申请空间失败!!!");
    			return;
    		}
    		_capacity = 3;
    		_size = 0;
    	}
    	void Push(DataType data)
    	{
    		CheckCapacity();
    		_array[_size] = data;
    		_size++;
    	}
    	void Pop()
    	{
    		if (Empty())
    			return;
    		_size--;
    	}
    	void Destroy()
    	{
    		if (_array)
    		{
    			free(_array);
    			_array = NULL;
    			_capacity = 0;
    			_size = 0;
    		}
    	}
    	int Empty() 
    	{
    		return 0 == _size; 
    	}
    private:
    		void CheckCapacity()
    		{
    			if (_size == _capacity)
    			{
    				int newcapacity = _capacity * 2;
    				DataType* temp = (DataType*)realloc(_array, newcapacity *
    					sizeof(DataType));
    				if (temp == NULL)
    				{
    					perror("realloc申请空间失败!!!");
    					return;
    				}
    				_array = temp;
    				_capacity = newcapacity;
    			}
    		}
    private:
    	DataType* _array;
    	int _capacity;
    	int _size;
    };
    
    • 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
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64

    那么这里面的Init,Push,Pop,Destroy,Empty等我们自己写的函数就可以称为成员函数,而且我们之前是学过数据结构所以我们知道这些函数分别对应的功能是什么?init函数是用来初始化我们这里创建的栈的相关数据,push函数是往栈里面填入数据,pop函数是删除栈中的数据,最后的destroy函数是当我们的栈不再使用时用来销毁栈并释放空间的。如果大家写的类比较多的话,数据结构比较熟悉的话,大家会发现一件事就是这里的初始化函数(init),和销毁函数(destroy)是不是会经常的调用和创建啊,几乎每实现一种数据结构的时候就得创建这样的两个函数,每用类来实例化出一个对象的时候就得调用init函数,每次用完这个对象的时候就得调用destroy函数来进行销毁,那么这里就会出现两个问题就是,第一个就是:写这些函数太麻烦了而且他们的功能还类似那我的编译器能不能自动的来实现这个函数呢?第二个就是:这些函数实现了但是有时候在某些场景下我们忘记调用了他们,比如说创建了一个栈但是在没有调用init函数之前我们就插入了数据,这样是不是就会报错啊,还有就是我们的栈不用了,但是却忘记使用Destroy函数来释放这个栈申请的空间,那这样的话是不是就会照成空间泄露啊,那我们的编译器能不能自动的在某些场合下使用这些函数来解决我们人类因为马虎大意而出现的问题呢?那么为了解决上述的问题我们的c++就有了默认成员函数这个东西,他就可以来解决我们上述的问题,那这里的默认成员函数是我们的编译器自动生成的,那根据默认两个字我们可以知道这里有个性质就是我们不写编译器会自己生成并且调用,但是如果自己写了的话,编译器则不会自动生成,但是他还是会自动地调用,而且不同地类中含有不同地数据类型,那么这就会导致有时候编译器生成地默认成员函数够用我们不用自己写,而有时候却不够用我们得自己写,那究竟有哪些默认成员函数呢?他们地功能又是什么呢?这些函数什么时候得自己写?什么时候又不用呢?那么接下来我们就来详细地为大家解决上述地问题。

    有哪些默认成员函数

    那么我们知道了为什么会有默认成员函数之后我们再来看看有哪些默认成员函数,那我们这里就可以通过下面的图片来了解了解:
    在这里插入图片描述
    我们可以看到这里有6个默认成员函数,但是这这六个中当中只有前四个是我们经常用的到得认真学习的,那么接下来我们就来对这四个进行深入的讲解。

    构造函数和析构函数

    为什么会有这两个函数

    通过我们上面的引入,我们大致的了解了这两个函数,我们说在对一个类进行实例化并且对其进行使用时,我们有时候会忘记在使用前调用初始化函数来将这个对象里面的数据进行初始化,这样的话就会报出一些错误出来,比如说下面的代码:

    int main()
    {
    	Stack s1;
    	s1.Push(1);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    那这个代码的意思就是用上面的类实例化出一个对象s1出来,然后就直接调用s1里面的push函数来往栈里面插入数据,如果是这样的话运行起来会出现什么样的情况呢?我们来调试一下看看:
    在这里插入图片描述
    我们发现编译器报出了 错误,而这里报错的原因就是因为我们一开始没有调用初始化函数来对这个类里面的数据进行初始化,类里面的_array _capacity _size的值都是随机值,

    在这里插入图片描述
    而这时我们再调用push函数的时候,他里面是用根据这些数据的值来做出一些操作的,所以我们这里报错的地方就是在push函数里面,那我们这里对上面的代码进行修改添加一个init函数上去,

    int main()
    {
    	Stack s1;
    	s1.Init();
    	s1.Push(1);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    再来调试一下我们就可以发现这里就没有报出错误,能够正常的往栈里面插入数据:
    在这里插入图片描述
    在这里插入图片描述
    那上面演示的过程就是我们在写代码的时候忘记调用初始化函数所带来的问题,而且有时候我们在使用完栈的时候忘记了调用Destroy函数来对栈进行销毁,来对申请的空间进行释放的话也会造成我们内存越来越少,编译器运行速度越来越慢,所以为了解决人们总是忘记调用函数的问题我们的科学家就想着把这两种函数改成自动调用,当我们用这个类进行实例化的时候我们就自动调用初始化函数也就是我们上面说的构造函数,当这个实例化的对象生命周期结束时我们就会自动调用销毁函数也就是我们上面说的析构函数,那这两个函数具体如何来使用我们接下来就来一个一个的由浅到深的学习。首先来看构造函数。

    构造函数的简介

    首先我们要知道的一点就是:构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。然后我们的构造函数有如下几个特征:

    1. 函数名与类名相同。
    2. 无返回值。
    3. 对象实例化时编译器自动调用对应的构造函数。
    4. 构造函数可以重载。

    好知道这些点之后我们就可以根据这些特性来一步一步的写一个构造函数出来,首先我们来个简单一点的例子就是一个关于日期方面的类,首先这个类里面什么都没有就一些与日期相关的数据在里面:

    class Date 
    {
    public:
    
    private:
    
    	int _year;
    	int _month;
    	int _day;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    那我们写的第一个函数就是构造函数,根据第一个特性我们知道这个构造函数的函数名得跟类名相似所以我们这里就叫Date,然后根据第二个性质我们知道这个函数没有返回值,所以函数名的前面我们什么都不写,那这里大家要记住的一点就是:这里的无返回值指的是什么都不用写,而不是写一个void上去,好!既然是函数的话那肯定得进行传参,所以我们在函数名的后面加一个括号,括号里面填入你想要初始化的数据和该数据对应的类型,那我们的代码就如下:

    Date(int year, int month, int day)
    
    • 1

    既然是函数的话我们下面就还得有函数体在函数体里面完成一些初始化的操作,所以我们在下面还得加一个大括号,在大括号里给这个类的数据进行我们想要的赋值,那下面的代码就是我们完整的初始化函数:

    class Date 
    {
    public:
    	Date(int year, int month, int day)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    
    private:
    
    	int _year;
    	int _month;
    	int _day;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    既然我们的代码写完了,那我们接下来的步骤就是如何来使用这个构造函数,如何体现他的自动调用的特性。

    构造函数的调用

    首先我们这里的构造函数,他也是个函数我们在调用他的时候也是得进行传参的,但是我们这里传参的方式并不是通过函数名来进行传参的,而是在将类进行实例化的时候进行传参,那么这里为了方便大家观察类中的值,我们这里就往类中再写入一个打印函数这个函数的作用就是打印这个类中数据的值,那我们的代码就如下:

    	void print()
    	{
    		printf("year->%d\n", _year);
    		printf("month->%d\n", _month);
    		printf("day->%d\n", _day);
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    然后再在main函数里面用该类创建出一个对象出来,因为我们这里要调用默认构造函数所以我们得在创建的时候得在对象名的后面加个括号,在括号里面输入你想要初始化的值,那我们的代码就是这样的:

    int main()
    {
    	Date d1(2022, 11, 11);
    	d1.print();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    然后我们将这个代码运行一下就可以看看我们这里的初始化结果:
    在这里插入图片描述
    那么我们这里进行观察就可以看出我们这里确实调用了构造函数,并且对类中的数据进行了初始化,那这就是我们调用构造函数的形式:
    在这里插入图片描述

    构造函数的重载

    我们上面的构造函数是输入三个值来进行初始化的,但是有时候我们只想初始化两个数据,比如说创建对象时只输入年和月的值而将天的值都默认为1呢?那面对这种情况我们就可以用到构造函数的重载这个知识点,我们再往类中写一个构造函数,但是将该构造函数的参数改成两个:一个是年一个是月,因为这样就可以构成函数的重载,然后在函数体里面将天的值初始化为1,这样就实现了我们上面的要求,那么这里我们的代码就如下:

    class Date
    {
    public:
    	Date(int year, int month, int day)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    	Date(int year, int month)
    	{
    		_year = year;
    		_month = month;
    		_day = 1;
    	}
    	void print()
    	{
    		printf("year->%d\n", _year);
    		printf("month->%d\n", _month);
    		printf("day->%d\n", _day);
    	}
    
    private:
    
    	int _year;
    	int _month;
    	int _day;
    };
    
    • 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

    那么我们这里就可以再创建一个对象并且使用第二个构造函数来进行调用,其代码如下:

    int main()
    {
    	Date d1(2022, 11, 11);//调用第一个构造函数
    	d1.print();
    	Date d2(2022, 11);//调用第二个构造函数
    	d2.print();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    我们来看看这段代码的运行结果:
    在这里插入图片描述
    那么这段代码运行成功了就说明我们这里的构造函数确实是可以进行重载,而且我们还可以根据函数重载的条件来出许多不同的构造函数出来,比如说写一个构造函数但是他一个参数都没有,就好比这样:

    	class Date
    {
    public:
    	Date(int year, int month, int day)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    	Date(int year, int month)
    	{
    		_year = year;
    		_month = month;
    		_day = 1;
    	}
    	Date()
    	{
    		_year = 1;
    		_month = 1;
    		_day = 1;
    	}
    	void print()
    	{
    		printf("year->%d\n", _year);
    		printf("month->%d\n", _month);
    		printf("day->%d\n", _day);
    	}
    
    private:
    
    	int _year;
    	int _month;
    	int _day;
    };
    
    • 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
    • 32
    • 33
    • 34

    这个构造函数是不需要我们传任何参数的,但是他在函数体的内部将数据全部都初始化为了1,但是这里大家要注意的一点就是,如果我们调用的构造函数不需要传参的话,那么在创建对象的时候也不要在对象名的后面加括号,就好比这样:

    int main()
    {
    	Date d1(2022, 11, 11);//调用第一个构造函数
    	d1.print();
    	Date d2(2022, 11);//调用第二个构造函数
    	d2.print();
    	Date d3;//调用第三个构造函数
    	//注意我们这里调用的是第三个构造函数,
    	//第三个构造函数没有参数所以我们这里在调用的时候不用进行传参也不要加括号
    	d3.print();//调用第三个构造函数
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    我们将这个代码运行一下看看:
    在这里插入图片描述
    我们这里运行成功了就说明我们这里的函数调用是真确的,我们来看看如果调用第三个构造函数的时候加了括号会如何:

    int main()
    {
    	Date d1(2022, 11, 11);//调用第一个构造函数
    	d1.print();
    	Date d2(2022, 11);//调用第二个构造函数
    	d2.print();
    	Date d3();//调用第三个构造函数
    	d3.print();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    我们将这段代码运行一下就发现这里的代码就报错了:
    在这里插入图片描述
    那以上就是构造函数重载的内容。我们接着往下看。

    构造函数的缺省参数

    以免有些小伙伴忘记了缺省参数是什么?那么我们这里就首先来看看缺省参数的概念:缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。而构造函数也是可以使用缺省参数的,比如说我们下面的代码:

    class Date
    {
    public:
    	Date(int year = 2022, int month = 11, int day = 11)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    	void print()
    	{
    		printf("year->%d\n", _year);
    		printf("month->%d\n", _month);
    		printf("day->%d\n", _day);
    	}
    private:
    
    	int _year;
    	int _month;
    	int _day;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    我们创建了一个构造函数,但是我们将他的三个参数都赋予了缺省值,那么这样的话我们在调用该函数的时候就可以有三种不同的传参方式可以传三个参数不用缺省值,可以传两个参数使用一个缺省值,也可以传一个参数使用两个缺省值,还可以不传参数使用0个缺省值,比如说我们下面的代码:

    int main()
    {
    	Date d1(2021, 1, 1);
    	Date d2(2020, 2);
    	Date d3(2021);
    	Date d4;
    	d1.print();
    	d2.print();
    	d3.print();
    	d4.print();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    我们这里创建了一个构造函数但是却可以使用4种不同的传参的方式,但是这里大家还是要注意的一点就是当你不传参的时候不要加括号,那么我们将这里的代码运行一下来看看我们这里会不会报错:
    在这里插入图片描述
    我们发现这里是没有出现问题的,而且他还确实将这些类中的数据进行初始化了,那么这就说明我们这里的缺省参数是确实可以用在构造里面的,但是这里大家还是要注意的一点就是二义性的问题,我们往上面的类中再加入一个构造函数:

    class Date
    {
    public:
    	Date(int year = 2022, int month = 11, int day = 11)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    	Date()
    	{
    		_year = 1;
    		_month = 1;
    		_day = 1;
    	}
    	void print()
    	{
    		printf("year->%d\n", _year);
    		printf("month->%d\n", _month);
    		printf("day->%d\n", _day);
    	}
    private:
    
    	int _year;
    	int _month;
    	int _day;
    };
    
    • 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

    我们可以看到这个类里面有了两个构造函数,但是这两个构造函数的参数个数不同,所以这里是可以构成重载的,我们直接运行一下也发现编译器没有报出什么问题出来:
    在这里插入图片描述
    说明我们这里的重载是没有什么问题的,但是如果我们这里创建了一个对象并使用第二种构造函数的话,那这里就会出现问题我们来看看代码:

    int main()
    {
    	Date d1;
    	d1.print();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    我们将其运行一下就可以看到这里报出一些问题
    在这里插入图片描述
    而这里出现问题的原因就是因为这里的函数的调用出现了歧义性,因为我们这里采用的是不传参的调用方式,而这两种构造函数都可以不需要参数,所以我们这里在调用的时候就会出现歧义性,编译器不知道要调用谁了所以就报错了,那么大家在构造函数中使用缺省参数的时候得注意这个问题,避免出现二义性。

    析构函数的简介

    通过前面构造函数的学习,我们知道一个对象是怎么来的,又是如何对类中的数据进行初始化的,那这里就有一个问题:一个对象又是怎么没呢的?那这里我们就得引入一个新的知识点析构函数。
    析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由
    编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作,比如说在堆区中申请了一个大小为50字节的空间,使用fopen函数打开了一个文件夹等等,这些对象中申请的资源都得由我们的析构函数来进行释放,那析构函数长什么样呢?其特性又是什么呢?带着这些问题我们继续往下看。

    析构函数的特性

    析构函数是特殊的成员函数,其特征如下:

    1. 析构函数名是在类名前加上字符 ~。
    2. 无参数无返回值类型。
    3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构
      函数不能重载
    4. 对象生命周期结束时,C++编译系统自动调用析构函数。

    析构函数的例子

    那么这里为了凸显析构函数的特性我们这里就来实现一个栈的类,在这个类里面有三个参数分别为:int* _a int _top int _capacity,那么我们类的雏形就是这样:

    class Stack
    {
    public:
    
    private:
    	int* _a;
    	int _top;
    	int _capacity;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    我们要完成的第一步就是写一个构造函数,这个构造函数我们就给一个参数capacity,并且我们还给这个参数赋予一个缺省值上去,这样就可以方便我们在创建对象的时候不用总是进行传参,并且还可以在我们需要的时候修改这里的初始值,接着在构造函数的函数体里面我们的任务就是用malloc来开辟一个动态的空间来装以后要用的数据,然后将指针a指向这个新开辟的动态空间,接着将_top的值初始化为0将capacity的值赋值给_capacity,那这就是我们的构造函数里面的内容,我们的代码就如下:

    Stack(int capacity = 4)
    	{
    		_a = (int*)malloc(sizeof(int)*capacity);
    		if (_a == nullptr)
    		{
    			perror("malloc fail");
    			exit(-1);
    		}
    		_top = 0;
    		_capacity = capacity;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    构造函数完成之后我们就来看看析构函数如何来写,首先根据第一个特性我们知道析构函数名是在类名前加上字符 ~,然后根据第二个性质我们知道这个析构函数跟构造函数一样没有返回值类型,所以我们在函数名的前面也就什么都不用加连void也不用加,因为析构函数也是一个函数,所以我们也得在函数名的后面加一个括号,但是按照c++的语法规定析构函数是不能有参数的,所以我们这个括号里面就什么都不要写,又因为函数得有函数体,所以我们得在函数名的下面加一个大括号在括号里面填入具体的操作,那么我们这里析构函数的雏形就如下:

    	~Stack()
    	{
    
    	}
    
    • 1
    • 2
    • 3
    • 4

    那我们接下来的步骤就是要完成函数体里面的操作,我们知道析构函数的作用就是对类中申请的资源
    进行释放,那这里我们有个问题就是对什么资源进行释放呢?比如说我们上面的写的有关的日期的类你觉得有必要写析构函数吗?答案是不需要的,因为这个日期的类中所创建的变量都是在栈上,他是一个局部变量当这个对象的生命周期结束之后,这里面创建的变量也会跟着进行销毁,所以我们不用单独的对其写一个析构函数来对其进行销毁,但是我们这里的Stack呢?他在调用构造函数的时候会向堆中申请一个块空间,而这块空间是不会随着我们对象的销毁而跟着销毁的,只有当我们的手动的使用free函数来进行回收的时候他才会跟着进行销毁,所以我们这里就得写一个析构函数来对这里malloc申请的空间来进行销毁,同样的道理,如果我们在初始化的时候使用fopen函数来打开一个文件呢?那这个文件会随着我们的对象的生命周期的结束而跟着关闭吗?答案是不会的,得我们手动的关闭,所以如果我们在类中打开一个文件的话是得写析构函数来对这个文件进行关闭的,那么看到这个想必大家应该知道了什么时候该写析构函数什么时候不用写了,那这里我们回到这个例子,我们这里Stack类的析构函数该写啥呢?因为我们在写构造函数的时候使用了malloc来申请空间,而这块空间不会随着对象的声明周期而销毁,所以我们这里的析构函数的内容就是使用free函数来释放这块堆上的空间,所以我们的代码就如下:

    	~Stack()
    	{
    		free(_a);
    		_a = nullptr;
    		_top = _capacity = 0;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    好!看到这里想必大家因该知道了如何来写一个析构函数,那么这里我们再来看看第四个特性:对象生命周期结束时,C++编译系统自动调用析构函数。那这里就有个问题对象的生命周期什么时候结束呢?那这里我们就分为三种情况:
    第一种
    就是当这个类所在的函数结束的时候,这个类的生命周期也就跟着结束了,比如说我们下面的代码:

    	~Stack()
    	{
    		cout << "~Stack()" << endl;
    		free(_a);
    		_a = nullptr;
    		_top = _capacity = 0;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    我们在析构函数里面加一段打印代码,这个代码的作用就是打印一句话表示我们调用过这个函数,然后我们再创建一个func函数,在这个函数里面就创建一个该类的对象,然后我们再在调用的函数下面用一个sleep函数这个让其休眠个几秒,再来打印一句话用来表示此时我们的程序以及结束,这样我们就可以来证明这种情况,那么我们的代码就是这样:

    #include
    void func()
    {
    	Stack s1;
    }
    int main()
    {
    	func();
    	Sleep(100000);
    	cout << "end" << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    那么我们这里运行起来就可以看到这里先打印了析构函数运行的标志
    在这里插入图片描述
    然后再过了几秒才出现main函数结束的标志,而这几秒却十分的关键因为这代表着此时我们的main函数还没有结束,但是我们的析构函数却以及调用了
    在这里插入图片描述
    那么这就是我们析构函数调用的第一种情况。
    第二种:
    当我们以全局变量的形式创建的一个类的时候,那么这个类调用析构函数的时机就是当main函数结束的时候开始进行调用。比如说我们下面的代码:

    Stack s1;
    int main()
    {
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    第三种:
    当我们在堆中创建一个对象的时候,那这个对象调用析构函数的时机就取决于我们什么时候用free将这个对象释放掉。比如说我们下面的代码:

    #include
    int main()
    {
    	Stack* a=(Stack*)malloc(sizeof(Stack));
    	free(a);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    那么以上就是我们析构函数的部分内容,通过以上的内容大家应该能够大致的了解析构函数和构造函数的作用,那么我们这里就来完成了30%的学习任务,我们接着往下看析构函数和构造函数的其他的性质。

    重回构造函数

    大家看了上面的内容有没有发现一个问题就是我们这个构造函数好像和初始化函数差不多啊,都是得自己写个构造函数,在初始化的时候还得来手动的来写个括号来调用这里的构造函数,好像没有体现出来我们上面说的能够自动的生成,自动的调用的特点吧,那么带着这些缺陷我们来看看构造函数的一个其他的特性就是:**如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。**那这句话的意思就是我们不写的话编译器能够帮我们写一个构造函数,那有些小伙伴们看到这个特性那估计得笑死了,哦原来我们不用写啊编译器能够帮我们写啊,那我们之前花费那么大的功夫学如何写构造函数干嘛啊,直接让我们的编译器自动写一个自动调用一下不就够了嘛,多么省事多么开心连cv工程师都不用当了,那么我们这里就用下面的代码来试一下让编译器自己写一个默认构造函数会发生什么样的情况,那这里我们就先拿一个简单的类来开开刀:

    class Date
    {
    public:
    	void print()
    	{
    		printf("year->%d\n", _year);
    		printf("month->%d\n", _month);
    		printf("day->%d\n", _day);
    	}
    
    private:
    
    	int _year;
    	int _month;
    	int _day;
    };
    int main()
    {
    	Date d1;
    	d1.print();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    我们这个类里面没有写任何的构造函数,那么按照这个规则来看的话我们的编译器就会自动的生成一个默认构造函数,然后我们在创建对象的时候就会自动的调用这个默认构造函数,那我们来看看这个默认构造函数的初始化结果如何:
    在这里插入图片描述
    wc我们发现这个就非常的难受了啊,这里打印的值为随机值,那也就是说这个编译器自动生成的默认构造函数好像没啥用啊,他还是随机值,那这是为什么呢?那这里大家先不要急着去质疑我们编译器自动生成的构造函数,毕竟存在即合理嘛,不然我们要他干嘛呢?对吧,那么我们这里就来换一个例子,我们再创建一个类,这个类实现的功能就是队列,通过之前的学习我们知道队列是可以通过两个栈来实现的,所以这里队列的类就有两个元素,他们都是栈的实例化对象,那我们的代码就是如下的:

    class MyQueue {
    public:
    
    private:
    	Stack _pushST;
    	Stack _popST;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    然后我们这个类依然是没有自己写构造函数的,所以编译器会自动地生成并调用默认构造函数,那我们这里就运行一下看看这里生成的默认构造函数执行结果如何:
    在这里插入图片描述
    我们仔细地观察一下就发现事情变得有意思起来了,他生成的默认构造函数好像起到了一点作用,他将这两个对象中的数据都进行了初始化,将_capacity的值都初始化为了4,将_top的值都初始化为了0,而且指针指向的地址也不是一个随机值,那么看到这里大家有没有发现这里好像就是我们栈的类中执行的构造函数的结果啊,我们再来来看看这个代码:

    	Stack(int capacity = 4)
    	{
    		cout << "Stack(int capacity = 4)" << endl;
    
    		_a = (int*)malloc(sizeof(int)*capacity);
    		if (_a == nullptr)
    		{
    			perror("malloc fail");
    			exit(-1);
    		}
    
    		_top = 0;
    		_capacity = capacity;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    而且我们这里还添加了一个打印语句来标识一下,当执行Stack的构造函数的时候,我们就会打印一句话出来作为标记,那这时我们再运行一下就会发现,我们的屏幕上面确实会出现这两句话:
    在这里插入图片描述
    那这就说明编译器自己生成的默认构造函数会调用其成员变量内部所对应的默认构造函数,比如说这里的MyQueue生成的默认构造函数,他就会调用其类中的stack类型里的默认构造函数,来对其上述例子的_pushST _popST进行初始化,所以我们才能看到这样的结果,那我们反过来看之前写的Date的类中生成的默认构造函数他为什么没有对其类中的三个int类型的数据进行初始化呢?那这是因为:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char/double等类型,而自定义类型就是我们使用class/struct/union等自己定义的类型,通过上面的例子就会发现编译器生成默认构造函数会对自定类型成员调用的它的默认构造函数来进行初始化,而对内置类型一般不做处理。那么这就是我们默认构造函数的一个性质。那么我们通过这个性质再来

    第一点

    看了上面的代码有些小伙伴肯定会去自己的编译器上面做一些尝试比如:将我们上面的代码做出了一些修改,在全是自定义类型的成员变量中加了一个内置类型,就好比这样:

    class MyQueue {
    public:
    
    private:
    	Stack _pushST;
    	Stack _popST;
    	size_t _size ; 
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    然后再来运行一下就发现编译器自动生成并且调用的默认构造函数的结果是这样的:
    在这里插入图片描述
    他对_size的值进行了初始化将其变成了0,那这时有些小伙伴们就感觉不对劲了,不是不对内置内省做处理吗?那这里的_size为什么会变成0呢?那这里大家就得注意的一个问题就是我们的c++语法并没有规定自动生成的默认构造函数是否要对内置类型进行处理,所以这就导致了有些编译器在执行上述代码的时候会将_size初始化为0,而有些编译器在执行上述代码的时候_size的值依然是个随机值,而且同样的编译器在小版本不同的情况下初始化的结果依然可能是不同的,并且同一个编译器在面对不同的代码的时候他是否初始化都是不确定的,所以这里大家就要注意一下,以后在写代码的时候发现编译器对内置类型进行初始化了的话,也不要感到奇怪毕竟语法没有规定嘛。那么带着这个发现大家心里肯定有这么个想法,如果我这里像让_size的值初始化为10该怎做呢?因为我们这里有两个自定义类型的存在,所以我们在初始化的时候可以通过默认构造函数来省去很多的麻烦,但是他们对不对内置类型进行初始化以及初始化的结果是什么我们是不知道的,那如果我们自己再写一个构造函数呢?那确实,我们是可以对内置类型进行我们想要的初始化,可是你又如何来对自定义类型进行初始化呢?对吧!我们甚至连类都没有实例化出来又何谈函数的调用,更何况这里是一个特殊的成员函数呢?那么为了解决上述的问题:C++11 中就针对内置类型成员不初始化的缺陷,又打了补丁即:内置类型成员变量在类中声明时可以给默认值。比如说上述的MyQueue的类我们就可以给_size一个默认值将其变成10,其形式就是这样:

    class MyQueue {
    public:
    
    
    private:
    	Stack _pushST;
    	Stack _popST;
    	size_t _size=10 ; // 这里不是初始化,给的缺省值
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    那这里大家要注意的一点就是我们这里并不是初始化而是给他了一个缺省值,我们再调试一下看看:
    在这里插入图片描述
    就会发现我们这里_size的值确实变成了10,那我们再回到那个日期的类,我们之前说这个生成的默认构造函数没有做任何的事情,他里面的三个内置类型的成员变量都是随机值,那我们这里是不是就可以通过给缺省值的方式来对其进行初始化让他成为我们想要的值啊,那么我们这里的代码就是这样:

    class Date
    {
    public:
    	void print()
    	{
    		printf("year->%d\n", _year);
    		printf("month->%d\n", _month);
    		printf("day->%d\n", _day);
    	}
    private:
    	int _year=10;
    	int _month=10;
    	int _day=10;
    };
    int main()
    {
    	Date d1;
    	d1.print();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    我们来看看这个代码的运行的结果:
    在这里插入图片描述
    我们发现这里的值不再是随机值而是变成了我们代码里给的缺省值.

    第二点

    我们的MyQueue生成的默认构造函数他是会调用其自定义类型里的默认构造函数,也就是下面的函数:

    Stack(int capacity = 4)
    	{
    		cout << "Stack(int capacity = 4)" << endl;
    
    		_a = (int*)malloc(sizeof(int)*capacity);
    		if (_a == nullptr)
    		{
    			perror("malloc fail");
    			exit(-1);
    		}
    
    		_top = 0;
    		_capacity = capacity;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    他是被调用的默认构造函数,我们MyQueue里的_pushST和_popST都会被初始化该形式,比如说_capacity 都被初始化为4,_top都初始化为0,但是这里就会产生一个问题就是如果我们这里想将_capacity值初始化为8呢?那这该如何去做?传值吗?怎么传?他是编译器自动生成的默认构造函数编译器自己调用的默认构造函数,以我们目前学的你传不了啊,那么为了解决这个问题我们就得用到一个全新的知识叫做初始化列表,他就可以实现将这里的_capacity初始化为8的功能,其形式如下:

    class MyQueue {
    public:
    	// 初始化列表 -- 类和对象下再讲
    	MyQueue(size_t capacity = 8)
    		:_popST(8)
    		, _pushST(8)
    		, _size(0)
    	{
    		//_size = 0;
    	}
    
    
    private:
    	Stack _pushST;
    	Stack _popST;
    	size_t _size=10 ; // 这里不是初始化,给的缺省值
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    那我们这里将这个代码运行一下就可以发现这里的栈里面的_capacity确实是变成了8
    在这里插入图片描述

    那这里我们就先不多描述这个初始化列表,因为这个是类和对象(下)所要讲的内容,这里先给大家看一下以免大家有这方面的疑问。

    究竟什么是默认构造函数

    看到了这里大家有没有发现一个问题就是什么是默认构造函数?我们说编译器自动生成的默认构造函数会调用其成员数据中的默认构造函数,但是这个默认构造函数好像长的是这样的吧:

    	Stack(int capacity = 4)
    	{
    		cout << "Stack(int capacity = 4)" << endl;
    
    		_a = (int*)malloc(sizeof(int)*capacity);
    		if (_a == nullptr)
    		{
    			perror("malloc fail");
    			exit(-1);
    		}
    
    		_top = 0;
    		_capacity = capacity;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    是不是就非常的奇怪,按照大家理解应该是编译器自动生成的构造函数就是默认构造函数,但我们的c++却不是这么规定的,我们的c++规定说:无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。那这里我们就来以代码形式来看看默认构造函数的分类:
    第一种:无参构造函数
    那这种默认构造函数就没有参数,比如我们之前的写的那个日期的构造函数,我们没有给这个函数设计参数,而是在函数的内部将所有的变量全部都初始化为了1,其代码如下:

    	Date()
    	{
    		_year = 1;
    		_month = 1;
    		_day = 1;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    那像这种没有参数的构造函数我们就可以将其称为默认构造函数。
    第二种:全缺省构造函数
    那么这种默认构造函数他是有参数的,但是每个参数都得给他缺省值,那么这里我们还是通过日期的类来看看:

    	Date(int year = 2022, int month = 11, int day = 11)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这个默认构造函数他有三个参数,但是每个参数我们都得给其缺省值,如果有一个没有给缺省值的话,那就不能称为默认构造函数,比如说下面的形式:

    	Date(int year, int month = 11, int day = 11)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    我们这里没有给year赋予缺省值,所以这里就不能称该形式为默认构造函数。
    第三种:编译器自动生成的构造函数
    那这种构造函数就很好理解,我们上面用的就是这种,但是这种构造函数的形式我们不大好展示只能根据我们上面讲的来理解理解。

    默认构造函数所带来的问题

    第一个问题

    那么我们这里知道了什么是默认构造函数,但是大家有没有注意到一点就是我们上面说了这么一句话就是:默认构造函数只能有一个,那如果有多个默认构造函数那会怎么样呢?我们来看看下面的代码:

    class Date
    {
    public:
    	Date(int year = 2022, int month = 11, int day = 11)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    	Date()
    	{
    		_year = 1;
    		_month = 1;
    		_day = 1;
    	}
    		void print()
    	{
    		printf("year->%d\n", _year);
    		printf("month->%d\n", _month);
    		printf("day->%d\n", _day);
    	}
    
    private:
    
    	int _year=10;
    	int _month=10;
    	int _day=10;
    };
    int main()
    {
    	Date d1;
    	d1.print();
    	return 0;
    }
    
    • 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
    • 32
    • 33
    • 34

    我们这个类中接写了第一种和第二种默认构造函数,然后我们在main函数的里面创建了一个对象不进行传参的话我们来看看编译器会报出什么错误:
    在这里插入图片描述
    首先可以看到这里出现的一句话就是重载函数的调用不明确,也就是说这里不知道要调用哪个重载函数,也就是所谓的二义性,然后我们再往下观察就可以发现编译器说了这么一句话:叫做该类中包含了多个默认构造函数,那出现这个错误的原因就是因为我们的语法不支持存在多个默认构造函数,如果我们将第一个默认构造函数的缺省值去掉一个将其变成普通的构造函数的话这里就不会报出这样的错误,并且正常的执行了第二个默认构造函数:
    在这里插入图片描述
    那么这里大家在写构造函数的时候就要注意这个问题。

    第二个问题

    我们说编译器自动生成的默认构造函数的条件是该类中没有任何的构造函数,他就会自动生成,比如说下面的类我们在实例化对象的时候编译器会自动创建默认构造函数吗?

    class Date
    {
    public:
    	void print()
    	{
    		printf("year->%d\n", _year);
    		printf("month->%d\n", _month);
    		printf("day->%d\n", _day);
    	}
    private:
    
    	int _year=10;
    	int _month=10;
    	int _day=10;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    答案是会的,因为这里面一个构造函数都没有,所以编译器会自动地生成,那我们再来看看下面地这个情况:

    class Date
    {
    public:
    	Date(int year , int month = 11, int day = 11)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    	
    	void print()
    	{
    		printf("year->%d\n", _year);
    		printf("month->%d\n", _month);
    		printf("day->%d\n", _day);
    	}
    
    private:
    
    	int _year=10;
    	int _month=10;
    	int _day=10;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    编译器会自动地生成吗?但是不会的,因为我们这个类中含有一个默认构造函数我们自己写了默认构造函数,所以编译器不会自动生成,那我们再来看一种情况:

    class Date
    {
    public:
    Date(int year, int month, int day)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    
    	void print()
    	{
    		printf("year->%d\n", _year);
    		printf("month->%d\n", _month);
    		printf("day->%d\n", _day);
    	}
    
    private:
    
    	int _year=10;
    	int _month=10;
    	int _day=10;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    那这里我们自己写了一个构造函数,虽然他不是默认构造函数,但是因为该函数地存在我们的编译器依然不会自动地生成默认构造函数。好我们知道了什么时候编译器会自动生成默认构造函数,那我们再来看一个问题就是,我们的编译器是如何调用默认构造函数的?那这就非常的简答就是在创建对象的时候不加括号就会自动的调用默认构造函数,比如说这样:

    int main()
    {
    	Date d1;
    }
    
    • 1
    • 2
    • 3
    • 4

    我们创建了一个对象d1,但是我们没有在函数名的后面加括号,所以他在创建对象的同时就会调用该类里面的默认构造函数来初始化该对象里面的数据,那这里有个点需要大家注意的就是:以这种方式来创建对象调用的是默认构造4函数,那如果我们的类中没有构造函数呢?他会出现什么样的结果,我们来看看下面的代码:

    class Date
    {
    public:
    
    	Date(int year, int month, int day)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    
    	void print()
    	{
    		printf("year->%d\n", _year);
    		printf("month->%d\n", _month);
    		printf("day->%d\n", _day);
    	}
    
    private:
    
    	int _year;
    	int _month;
    	int _day;
    };
    int main()
    {
    	Date d1;
    	return 0;
    }
    
    • 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

    我们将这个代码运行一下就可以发现这里报出了这样的错误:
    在这里插入图片描述
    那这里报错的原因就是因为我们这里初始化的形式调用默认构造函数,但是这个类里面没有前两种默认构造函数,又因为我们这里写了其他的构造函数,编译器自己就不会再生成默认构造函数,所以编译器下面又说该类中不存在默认构造函数,那这就是报错的原因,通过这个错误想必大家能够更好的理解默认构造函数,那我们再来看一个例子,我们将Stack的构造函数进行一下修改,将我们自己写的第二种默认构造函数改成普通的构造函数就像这样:

    Stack(int capacity )
    	{
    		cout << "Stack(int capacity )" << endl;
    
    		_a = (int*)malloc(sizeof(int)*capacity);
    		if (_a == nullptr)
    		{
    			perror("malloc fail");
    			exit(-1);
    		}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    然后我们再来创建一个队列的对象:

    int main()
    {
    	MyQueue q1;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    简单的运行一下看看这里的结果如何:
    在这里插入图片描述
    我们发现这里报出了许多的错误其报错的愿意就是因为stack里面没有了默认构造函数,而编译器给MyQueue生成的默认构造函数会调用其内部成员数据Stack里面的默认构造函数,而我们这么一修改导致了Stack里面没有默认构造函数,而且编译器还不会给他自动生成,但是这里的调用形式是调用默认构造函数来进行初始化的啊,那么这里产生了矛盾所以就会报错,希望大家能够理解一下这里出错的原因,那这里解决的方法还是初始化列表,那这个方法怎么用我们往后再来唠嗑,暂时给大家留一个悬念。

    第三个问题

    那么看到这里想必大家都觉得写构造函数是一个非常麻烦的事情,那我们这里能不能即不写默认构造函数也能让编译器自己生成的默认构造函数有用呢?答案是可以的,我们这里可以完全的通过给缺省值的形式来实现该想法比如说我们之前写的栈我们就可以把构造函数都去掉,然后给每个成员函数都赋予一个缺省值就好比这样:

    class Stack
    {
    public:
    	~Stack()
    	{
    		/*cout << "~Stack()" << endl;*/
    		free(_a);
    		_a = nullptr;
    		_top = _capacity = 0;
    	}
    private:
    	int* _a = nullptr;
    	int _top = 0;
    	int _capacity = 0;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    我们编译器生成的构造函数就能对所有的内置类型都进行初始化,而且通过其他类的构造函数来调用他时也可以进行初始化比如说我们上面的代码:

    int main()
    {
    	MyQueue q1;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    我们调试一下就可以看到:
    在这里插入图片描述
    这里确实没有报错而且全部都按照我们的缺省值进行了初始化,而且这里的缺省值的功能还十分的强大,我们不仅仅可以给他赋值一个空指针上去,而且还可以给他现场创建一个动态空间出来,把这个动态空间的地址作为缺省值给这里的指针比如说我们下面的代码:

    class Stack
    {
    public:
    	~Stack()
    	{
    		/*cout << "~Stack()" << endl;*/
    
    		free(_a);
    		_a = nullptr;
    		_top = _capacity = 0;
    	}
    private:
    	int* _a = (int*)malloc(sizeof(int)*10);
    	int _top = 0;
    	int _capacity = 10;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    我们再来调试一下就可以看到这里确实能够编译成功:
    在这里插入图片描述

    重回析构函数

    我们上面说析构函数也是默认成员函数,那既然构造函数可以自动的生成并且调用的话,那我们的析构函数会不会也有自动生成的这个特性呢?那我们观察一下下面的代码就可以知道这个问题的答案:

    class MyQueue 
    {
    public:
    
    private:
    	Stack _pushST;
    	Stack _popST;
    	size_t _size;
    };
    
    class Stack
    {
    public:
    Stack(int capacity =4 )
    	{
    		cout << "Stack(int capacity )" << endl;
    		_a = (int*)malloc(sizeof(int)*capacity);
    		if (_a == nullptr)
    		{
    			perror("malloc fail");
    			exit(-1);
    		}
    
    		_top = 0;
    		_capacity = capacity;
    	}
    	~Stack()
    	{
    		cout << "~Stack()" << endl;
    		free(_a);
    		_a = nullptr;
    		_top = _capacity = 0;
    	}
    private:
    	int* _a ;
    	int _top;
    	int _capacity;
    };
    void func()
    {
    	MyQueue q1;
    }
    int main()
    {
    	func();
    	return 0;
    }
    
    • 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
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    我们这里创建了两个类,在Stack这个类里面我们自己主动写了析构函数并且还加了打印语句来作为标识,然后另一个类MyQueue里面就没有写析构函数,但是这个类里面却有两个Stack类型的对象在里面,然后我们就在main函数外创建了一个函数,在该函数里面实例化了一个MyQueue类型的对象,那我们这里就来通过调试看看这里究竟会发生什么?首先我们按f11进入到func这个函数里面然后按f10实例化了这个对象:
    在这里插入图片描述

    然后我们就可以通过监视来看到这里的_pushST和_popST都通过了stack里面的默认构造函数对其进行了初始化,屏幕上也对此做出了两个标记:
    在这里插入图片描述
    然后我们再往下执行让其退出这个函数,当这个函数执行完之后我们的对象q1的生命周期也就跟着结束了,那这时我们再来看看这个对象中的数据:
    在这里插入图片描述
    发现这里的数据都成灰色看不了了,那这就说明这个对象的生命周期已经结束了,但是我们再看屏幕的话就会发现这里又多出来两个stack的析构函数的标志:
    在这里插入图片描述
    那这就说明了当q1的生命周期结束的时候,他会调用其成员数据中stack的析构函数来回收stack的对象所申请的空间,那这不就间接性的验证了析构函数也有自动生成并调用的特性了吗?并且这里自动生成的默认析构函数他也是对内置类型不做任何的处理,对于那些我们自己写的自定义类型他就会调用其该类型的析构函数来回收申请的空间,比如说这里MyQueue的生命周期结束的时候,因为我们这里没写析构函数,所以编译器就会给他自动的生成一个析构函数,而该析构函数在运行的时候就又会调用Stack的析构函数来进行该类型(Stack)的数据的进行回收,对于那些内置类型我们的析构函数就不会做任何的处理,因为当对象的生命周期结束的时候,那些内置类型的生命周期也会跟着结束,那这里就是我们对析构函数的一个补充希望大家能够类比的理解,因为析构函数无法实现重载所以他就没有构造函数那样的特殊情况。

    析构函数和构造函数的总结

    那我们这里的总结就来看一个问题:什么时候该自己写构造函数?什么时候该自己写析构函数?那面对这个问题我们最好回答就是根据自己的需求来判断,如果编译器自己生成的默认构造函数能够达到我们的要求,那我们就可以不写比如说我们上面写的MyQueue,他就可以不用写(这里指的是没有写size的时候),因为根据该默认构造函数的特性他能对_pushST和 _popST进行我们想要的初始化,所以我们就不用自己写够着函数,那反观上面的Date和Stack的类呢?他们能不写吗?答案是不行的,因为自动的生成的构造函数不对内置类型进行处理,所以他达不到我们想要的需求所以我们就必须得在Date里面写构造函数来给三个整型进行赋值,在Stack里面就得自己写构造函数来开辟一个固定大小的空间并把这个空间的地址赋值给该类中的指针,那这里就是我们面对需求的一个例子,我们再来看看析构函数的需求,大家觉得date需要些析构函数吗?答案肯定是不需要的,因为该类中的成员数据会随着对象的生命周期结束而随之销毁,达到了我们想要的需求,所以他不用写析构函数,那MyQueue需要自己写析构函数吗?答案是不需要的,因为自动生成的析构函数会调用其内置类型里的析构函数对我们申请的空间进行释放,能够达到我们的需求所以我们不用写,那stack需要我们写析构函数吗?答案是需要的,因为我们的stack会向堆中申请一些空间,而随着我们对象的生命周期结束这些空间不会随之销毁,所以我们得写析构函数来达到我们的需求,那看到这里还是那句话写不写都看编译器自动生成的能否达到我们的需求。

  • 相关阅读:
    Semantic Kernel 入门系列:📅 Planner 规划器
    模式也能开盲盒,”盲返“模式带动电商平台共享经济
    java基于Springboot+vue的药品销售商城 药品进销存系统 element
    2016-2023年国赛题型及算法模型总结
    FANUC机器人零点复归的报警原因分析和零点标定相关步骤
    基于QUBO模型的多体分子对接
    服务器64GB内存、8核CPU的MySQL 8配置参数
    Flink 读写 Ceph S3入门学习总结
    分类算法——支持向量机 详解
    pytorch 神经网络特征可视化
  • 原文地址:https://blog.csdn.net/qq_68695298/article/details/127925738