Bootstrap

C++类和对象--构造函数和析构函数

0.前言

在我们写某些需要动态开辟内存空间的函数时候,会经常忘记初始化、销毁,而且有时候程序返回的情况很多,那么销毁函数写起来就会很繁琐,那么有没有什么办法解决这个问题呢?答案是:当然有!在C++中有两个默认构造函数–构造函数和析构函数,可以自动帮助我们完成初始化和销毁工作!🎉🎉🎉
在这里插入图片描述

1.构造函数

1.1构造函数概念

构造函数是一个特殊的成员函数,名字和类相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次 🦀🦀🦀

1.1构造函数特性

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象 🎾🎾🎾

其特征如下:

1.函数名与类名相同
2.无返回值(也不需要写void)
3.对象实例化时编译器自动调用对应的构造函数

一个栗子:

#include<iostream>
typedef int Datatype;
class Stack
{
public:
	Stack()
	{
		printf("Stack()\n");
		_a = (Datatype*)malloc(sizeof(Datatype) * 6);
		if (_a == NULL)
		{
			perror("Stack()::malloc");
			return;
		}
		_capacity = 6;
		_size = 0;
	}
private:
	Datatype* _a;
	int _size;
	int _capacity;
};
int main()
{
	Stack s;
	return 0;
}

代码运行的结果:
在这里插入图片描述
注意:

Stack s();//定义的时候,不能这样调用构造函数,编译无法很好区分s是对象还是函数名

4.构造函数可以重载(可以有多种初始化方式)

#include<iostream>
typedef int Datatype;
class Stack
{
public:
	Stack()
	{
		printf("Stack()\n");
		_a = (Datatype*)malloc(sizeof(Datatype) * 6);
		if (_a == NULL)
		{
			perror("Stack()::malloc");
			return;
		}
		_capacity = 6;
		_size = 0;
	}
	Stack(int capacity = 6)
	{
		printf("Stack(int capacity = 6)\n");
		_a = (Datatype*)malloc(sizeof(Datatype) * capacity);
		if (_a == NULL)
		{
			perror("Stack()::malloc");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}
private:
	Datatype* _a;
	int _size;
	int _capacity;
};
int main()
{
	Stack st1(8);
	return 0;
}

代码运行的结果:
在这里插入图片描述
但是当像这样Stack st2;定义对象时,无参构造函数和缺省构造函数会造成歧义,编译器不知道该调用哪个进而引发错误
在这里插入图片描述

一般情况下我们保留含缺省参数的构造函数,方便我们可以指定申请的内存空间的大小

5.如果类中没有显示定义构造函数,则C++编译器会自动生成无参的默认构造函数,一旦用户自己显示定义编译器将不再生成!

#include<iostream>
using std::cout;
using std::endl;
class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	//将Date类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数
	//将Date类中的构造函数放开,代码编译失败,因为一旦显示定义任何构造函数,编译器将不会再生成
	//无参构造函数放开后报错,类Date没有合适的默认构造函数
	Date d1;
	return 0;
}

代码编译运行的结果为:
在这里插入图片描述
6.内置类型/基本类型,即语言本身定义的基础类型int/char/double/指针类型等等,编译器不做处理,需要自己写构造函数;自定义类型:用struct/class等定义的类型,编译器默认生成构造函数。
eg:

#include<iostream>
using namespace std;
class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	//基本类型(内置类型)
	int _year;
	int _month;
	int _day;
	//自定义类型
	Time _t;
};
int main()
{
	Date d;
	return 0;
}

调试运行之前:
在这里插入图片描述
调试运行之后:
在这里插入图片描述
注意:C++11中针对内置类型成员不初始化的缺陷,打了一个补丁,即:内置类型成员变量在类中声明时可以给默认值

#include<iostream>
using namespace std;
class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
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;
}

调试运行之后:
在这里插入图片描述

结论:1、一般情况下,构造函数都需要我们自己写;2、不需要自己写构造函数的情况a、内置类型成员都有缺省值,且初始化符合我们的要求;b、类成员全是自定义类型的构造,且这些类型都定义默认构造。

7、无参构造函数、全缺省的构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数(不传参就可以调用的就是构造函数)。

2.析构函数

2.1概念

析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中的资源的清理工作。

2.2特性

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

1.析构函数是在类名前加上字符~。
2.无参数无返回值类型。
3.一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数,不能重载。
4.对象的生命周期结束时,C++编译系统自动调用析构函数。
5.一般情况下,有动态内存申请资源,就要写析构函数释放资源;没有动态内存开辟的资源,不需要写析构;需要释放的成员都是自定义类型,不需要写析构。

栗子:

#include<iostream>
using namespace std;
class Stack
{
public:
	Stack(int capacity = 3)
	{
		cout << "Stack(int capacity = 3)" << endl;
		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr)
		{
			perror("Stack(int capacity = 3)::malloc");
			return;
		}
		_capacity = capacity;
		top = 0;
	}
	~Stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_capacity = 0;
		top = 0;
	}
private:
	int* _a = nullptr;
	int _capacity = 0;
	int top = 0;
};
int main()
{
	Stack st1;
	return 0;
}

代码运行的结果为:
在这里插入图片描述

3.拷贝构造函数

3.1概念

拷贝构造函数:只是单个形参,该形参是对本类类型对象的引用(一般用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

3.2特征

拷贝构造函数也是特殊的成员函数,其特征如下:

1.拷贝构造函数是构造函数的一个重载形式。
2.拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。

错误的栗子

#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year = 1970, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(Date d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2023,1,1);
	Date d2(d1);
	return 0;
}

代码编译的结果为:
在这里插入图片描述

在这里插入图片描述
C++规定:在传值传参的时候,内置类型传参直接拷贝;自定义类型必须调用拷贝构造函数完成拷贝
正确的使用:

#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year = 1970, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2023,1,1);
	Date d2(d1);
	return 0;
}

代码调试运行的结果为:
在这里插入图片描述
一般加上const修饰

	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

发生如下情况编译器会进行报错

	Date(const Date& d)
	{
		d._year = _year;
		d._month = _month;
		d._day = _day;
	}

在这里插入图片描述
注意:使用传引用传参,自定义类型不会调用拷贝构造函数
3.若为显示定义,编译器会生成默认拷贝构造函数。默认的拷贝构造函数对象按内存存储(即按字节节序完成拷贝),这种拷贝叫做浅拷贝(值拷贝)。
eg1:

#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year = 1970, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//Date(const Date& d)
	//{
	//	_year = d._year;
	//	_month = d._month;
	//	_day = d._day;
	//}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2023,1,1);
	Date d2(d1);
	return 0;
}

代码调试运行的结果为:
在这里插入图片描述
eg2:

#include<iostream>
using namespace std;
class Stack
{
public:
	Stack(int capacity = 3)
	{
		cout << "Stack(int capacity = 3)" << endl;
		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr)
		{
			perror("Stack(int capacity = 3)::malloc");
			return;
		}
		_capacity = capacity;
		top = 0;
	}

	~Stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_capacity = 0;
		top = 0;
	}
private:
	int* _a;
	int _capacity;
	int top;
};
int main()
{
	Stack st1;
	Stack st2(st1);
	return 0;
}

代码调试运行的结果为:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
Stack类型进行值拷贝时存在的问题:1.会调用两次析构函数,即会对动态开辟的同一块空间释放两次;2.对一个Stack类型的对象修改会影响另一个对象。所以,我们需要自己实现进行深拷贝的构造函数。
深拷贝:

#include<iostream>
using namespace std;
class Stack
{
public:
	Stack(int capacity = 3)
	{
		cout << "Stack(int capacity = 3)" << endl;
		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr)
		{
			perror("Stack(int capacity = 3)::malloc");
			return;
		}
		_capacity = capacity;
		top = 0;
	}
	Stack(const Stack& st)
	{
		_a = (int*)malloc(sizeof(int) * st._capacity);
		if (_a == nullptr)
		{
			perror("Stack(const Stack& st)");
			return;
		}
		memcpy(_a, st._a, sizeof(int) * st.top);
		_capacity = st._capacity;
		top = st.top;
	}
	~Stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_capacity = 0;
		top = 0;
	}
private:
	int* _a;
	int _capacity;
	int top;
};
int main()
{
	Stack st1;
	Stack st2(st1);
	return 0;
}

代码运行的结果为:
在这里插入图片描述

在这里插入图片描述

进行深拷贝,st1\st2指向的动态内存的空间不同,正常结束会调用两次析构函数。

总结: 成员类型为内置类型的Date类型(编译器会进行浅拷贝构造)和成员类型都为自定义类型的类类型(前提为该自定义类型已经自己实现了深拷贝构造)不需要写拷贝构造函数;Stack类型需要自己深拷贝实现。
4.拷贝构造函数的使用场景:

  • 使用已存在的对象创建新对象
  • 函数参数类型为类类型对象
  • 函数返回值类型为类类型对象
#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year, int month, int day)
	{
		cout << "Date(int,int,int):" << this << endl;
	}
	Date(const Date& d)
	{
		cout << "Date(const Date& d):" << this << endl;
	}
	~Date()
	{
		cout << "~Date():" << this << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
Date Test(Date d)
{
	Date tmp(d);
	return tmp;
}
int main()
{
	Date d1(2023, 1, 1);
	Test(d1);
	return 0;
}

代码运行的结果为:
在这里插入图片描述

4.总结

本章我们一起学习了类和对象的构造函数、析构函数、拷贝构造函数等默认成员函数的相关知识,希望对大家认识C++中的类和对象由些许帮助!感谢大家阅读,如有不对,欢迎纠正!!!🎉🎉🎉

;