【C++】类和对象(中)--上篇

avatar
作者
筋斗云
阅读量:0

在这里插入图片描述
个人主页~
类和对象上


类和对象

一、类的六个默认成员函数

如果有个类中什么成员都没有,那么被称为空类

由编译器自动生成的成员函数称为默认成员函数

空类中会自动生成六个默认成员函数,这六个默认成员函数在每个类中都会自动生成

①初始化功能的构造函数
②清理功能的析构函数
③使用同类对象初始化创建对象的拷贝构造
④把一个对象赋值给另一个对象的赋值重载
⑤对普通对象取地址重载
⑥对const对象取地址重载

这六个默认成员函数主要将操作对象分为内置类型自定义类型,对二者有不同的操作

二、构造函数

1、构造函数基本概念

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

2、构造函数的特性

构造函数是特殊的成员函数,主要任务就是初始化对象
(1)函数名与类名相同
(2)无返回值
(3)对象实例化时编译器自动调用
(4)可以重载

class Date { public: 	//无参构造函数 	Date() 	{} 	//带参构造函数 	Date(int year, int month, int day) 	{ 		_year = year; 		_month = month; 		_day = day; 	} private: 	int _year; 	int _month; 	int _day; }; 

无参调用不用加括号:

//无参调用 Date d1; //有参调用 Date d2(2024,6,22); 

(5)如果类中没有显式定义构造函数,则编译器会自动生成一个无参的默认构造函数,如果有显式定义,编译器将不再生成

class Date { public: 	//无参构造函数 	Date() 	{} private: 	int _year; 	int _month; 	int _day; }; 

在这里插入图片描述
由编译器自己生成:
在这里插入图片描述
编译器会自动生成一个无参的默认构造函数,初始化给的是随机值
(6)行文至此有人会觉得编译器给的无参的默认构造函数很垃圾,初始化给的是随机值,没有什么意义,他对于自定义类型来说是有很大的意义的,它可以调用自定义类型的默认构造函数

class Time { public: 	Time() 	{ 		cout << "Time()" << endl; 	} private: 	int _hour; 	int _minute; 	int _second; }; class Date { private: 	int _year; 	int _month; 	int _day;  	Time _t; }; int main() { 	Date d; 	return 0; } 

在这里插入图片描述

内置成员变量在类中声明时可以给默认值

class Date { private: 	int _year = 1970; 	int _month = 1; 	int _day = 1; }; int main() { 	Date d; 	return 0; } 

在这里插入图片描述
(7)无参的构造函数、全缺省的构造函数、不写而编译器自动生成的构造函数都叫默认构造函数

全缺省:

class Date { public: 	Date(int year = 1970, int month = 1, int day = 1) 	{ 		_year = year; 		_month = month; 		_day = day; 	} private: 	int _year; 	int _month; 	int _day; }; int main() { 	Date d; 	return 0; } 

在这里插入图片描述

三、析构函数

1、析构函数的概念

析构函数是与构造函数功能相反的一个函数,对象在销毁时会自动调用析构函数,完成资源清理

2、特性

(1)析构函数名是在类名前加上字符~
(2)无参数无返回类型
(3)一个类只能有一个析构函数,未显式定义则自动生成
(4)生命周期结束时自动调用

构造函数+析构函数改造栈:

class Stack { public://公共访问,但在类中可以访问private的内容,只是在类外不能直接访问 	Stack(size_t newcapacity = 4)//缺省 	{ 		int* tmp = (int*)realloc(a, sizeof(int) * newcapacity); 		if (tmp == nullptr) 		{ 			perror("realloc fail"); 			exit(-1); 		} 		a = tmp; 		capacity = newcapacity; 		top = 0; 	} 	void Push(int x) 	{ 		a[top++] = x; 	} 	void Pop() 	{ 		top--; 	} 	int Top() 	{ 		return a[top - 1]; 	} 	bool Empty() 	{ 		return top == 0; 	} 	~Stack() 	{ 		free(a); 		a = nullptr; 		capacity = top = 0; 	} 	//自己写的析构函数,在程序执行的最后执行 private://隐私访问 	int* a; 	int top; 	int capacity; }; 

编译器自动生成的析构函数不能对内置类型进行资源回收,但可以调用自定义类型的析构函数

class Time { public: 	~Time() 	{ 		cout << "~Time()" << endl; 	}//Time的析构函数 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; } 

在这里插入图片描述

因为d中包含着四个成员变量,除了_year _month _day 外还有一个Time类,内置类型成员在销毁时不被资源清理,但自定义类型需要调用析构函数回收,但是main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构函数,编译器默认生成析构函数的可以对自定义成员Time调用它的析构函数,即当Date销毁时,Time也会销毁

如果类中没有申请资源时,也就是没有在堆上申请空间时,析构函数可以不写,直接使用编译器生成的默认析构函数,有申请资源的话一定要写,防止资源泄露

四、拷贝构造函数

1、拷贝构造函数的概念

只有单个形参,该形参是对本类类型对象的引用(一般用const修饰),在用已存在的类类型对象创建对象时,由编译器自动调用

2、特征

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

传引用调用:

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; 	Date d2(d1); 	return 0; } 

在这里插入图片描述
传值调用:
在这里插入图片描述
我们知道传值调用形参会开辟一块空间,成为实参的临时拷贝,这样会创建一个Date,因为类会自动调用里面的六个默认成员函数,拷贝构造函数也是其中之一,这样一来,又会创建一个Date,以此类推,无限循环
(3)若未显式定义,编译器会生成默认的拷贝构造函数,这个默认的拷贝构造函数是值拷贝

在编译器生成的默认拷贝函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的

class Time { public: 	Time() 	{ 		_hour = 1; 		_minute = 1; 		_second = 1; 	} 	Time(const Time& t) 	{ 		_hour = t._hour; 		_minute = t._minute; 		_second = t._second; 		cout << "Time(const Time & t)" << endl; 	} private: 	int _hour; 	int _minute; 	int _second; };  class Date { public: 	Date(int year = 1970, int month = 1, int day = 1) 	{ 		_year = year; 		_month = month; 		_day = day; 	} private: 	int _year; 	int _month; 	int _day; 	Time t; };  int main() { 	Date d1; 	Date d2(d1);//这里是用已有的d1拷贝构造d2 	return 0; } 

在这里插入图片描述
在这里插入图片描述
(4)编译器默认生成的拷贝构造函数可以拷贝像Date类这样的类,但对于某些类来说我们要显式定义

class Stack { public: 	Stack(size_t newcapacity = 4) 	{ 		int* tmp = (int*)realloc(a, sizeof(int) * newcapacity); 		if (tmp == nullptr) 		{ 			perror("realloc fail"); 			exit(-1); 		} 		a = tmp; 		capacity = newcapacity; 		top = 0; 	} 	void Push(int x) 	{ 		a[top++] = x; 	} 	void Pop() 	{ 		top--; 	} 	int Top() 	{ 		return a[top - 1]; 	} 	bool Empty() 	{ 		return top == 0; 	} 	~Stack() 	{ 		free(a); 		a = nullptr; 		capacity = top = 0; 	} private: 	int* a; 	int top; 	int capacity; };  int main() { 	Stack s1; 	s1.Push(1); 	s1.Push(2); 	Stack s2(s1); 	return 0; } 

该程序中没有显式定义拷贝构造函数吗,是调用的编译器自动生成的拷贝构造函数
在这里插入图片描述
在执行析构函数的时候出现了错误,这里的原因是数组a已经被释放了,再次释放产生错误

因为编译器自动生成的拷贝构造函数是值拷贝,所以在生成s2时,s2中的指针a指向的数组与s1中的指针指向的数组相同,在程序结束时,调用析构函数释放了s2,对应的这块数组空间也被释放,然后调用析构函数释放s1,已经被释放的空间不能被再次释放,所以出现了这样的错误,所以我们需要自己显式定义一个拷贝构造函数

深拷贝:

Stack(const Stack& s) { 	cout << "Stack(const Stack& s)" << endl; 	int* a = (int*)malloc(sizeof(int) * capacity); 	if (a == nullptr) 	{ 		perror("malloc fail"); 		exit(-1); 	} 	memcpy(a, s.a, sizeof(int) * s.top); 	top = s.top; 	capacity = s.capacity; } 

在这里插入图片描述
(5)拷贝构造函数的使用场景:

已存在的对象建立新对象

函数参数为类类型对象

函数返回值为类类型对象

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 temp(d); 	return temp; }  int main() { 	Date d1(2022, 1, 13); 	Test(d1); 	return 0; } 

因为我所使用的编译器为VS2022,是一种新的编译器,会对程序有一定的优化
我们来分析一下,如果编译器不优化,打印出的结果是什么
①创建d1类,调用构造函数,打印"Date(int,int,int):"和d1的地址

②以d1拷贝构造形参d,调用拷贝构造函数,打印"Date(const Date& d):"和d的地址

③以d拷贝构造形参temp,调用拷贝构造函数,打印"Date(const Date& d):"和temp的地址(这一步在编译器中被优化

④返回temp时,会拷贝一份构造临时对象返回,调用拷贝构造函数,打印"Date(const Date& d):"和临时对象的地址

⑤依次调用递归函数销毁temp(这一步在编译器中被优化),d,临时对象,d1

在这里插入图片描述

传参时,能够进行引用传参的尽量使用引用传参,因为引用传参不需要再拷贝占用空间,提高程序运行效率


今日分享结束~

在这里插入图片描述

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!