C++——类和对象2 构造函数 析构函数 拷贝构造函数 运算符重载 赋值运算符重载 赋值运算符连续赋值_c+

img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

return 0;
}


![](https://img-blog.csdnimg.cn/eb0537bc5e4448239ddcd301569c5397.png)


执行完fun函数后,先进入Stack的析构函数,再进入Date的析构函数 


之前博客里写栈需要写初始化Init函数和释放空间的Destory函数,而在C++中写一个构造函数和析构函数不需要调用即可,析构函数就会代替Destory的作用来释放空间(要自己写)



默认生成析构函数特点

跟构造函数类似,内置类型不处理,自定义类型处理,自定义类型会调用自己的构造 ,指针属于内置类型,指针不处理,因为指针有的是指向动态开辟空间,有的指向一个数组,还有文件指针

当写栈,队列,链表,顺序表,二叉树用自己写的析构函数就比较方便

构造函数和析构函数的顺序问题

 先构造的后析构,后构造的先析构

这是因为这里的内容存在栈中,要满足先进后出

main 先调用f1,f1再调用f2,然后f2销毁返回f1,满足先进后出

先初始化全局的,当进入main函数后按顺序初始化

对于析构,变量销毁后就进行析构,aa2和aa1在栈区,栈帧结束后要清理资源先清理aa2,再清理aa1,全局变量和静态变量在函数结束以后才销毁,之后清理资源,所以先清理aa0再清理aa3

构造顺序:3 0 1 2 4 1 2

析构顺序:2 1 2 1 4 0 3

拷贝构造函数

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

特征

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

  1. 拷贝构造函数是构造函数的一个重载形式。

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

  3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝

对于get1,形参是实参的一份的临时拷贝,get2,形参是实参的别名

参数也是类类型,用d1去初始化d即传参的时候,就需要构造函数(初始化函数),get1的构造是拷贝构造,get2还没传参的时候就已经构造好了,在Date d1这条语句里已经构造好了

验证上面的结论

这条语句之后按F11,直接跳到拷贝构造这里来

先构造d1,再给get1构造

若想把d1拷贝过去,这俩种写法都可以

若拷贝构造函数这样写,则会进入死循环,因为形参必须是类类对象的引用

 解决这种传参问题有俩种办法:1.引用

2.指针(这种方式不是拷贝构造),虽然也能完成,但是不建议这种方法,这种方法比较奇怪,更容易出现错误

有时候拷贝构造函数容易写错,所以要加const

正确形式

未定义拷贝构造

对内置类型按直拷贝进行拷贝的:就是一个字节,一个字节拷贝过去

对自定义类型,调用自定义类型自己的拷贝构造函数完成

注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
拷贝构造还有深浅拷贝问题,后面的博客里会写

浅拷贝举例

这里能正常拷贝,但是在析构的时候会崩溃

这是因为这里执行了俩次析构函数,由于这里是直拷贝,st和st1指向了同一块空间,而这快空间被释放了俩次, 因为这里是内置类型,指针是内置类型,把*array所指向的空间给st1拷贝过去了,然后st1和st指向了同一块空间,析构的时候又free了俩次,直接崩溃

拷贝构造在传值传参和传引用传参的区别

#include<iostream>
using namespace std;
class A
{
public:
	A(int a = 0)
	{
		_a = a;
		cout << "A(int a=0)>>" <<_a<< endl;
	}
	A(const A& aa)
	{
		_a = aa._a;
		cout << "拷贝构造A(int a=0)>>" << _a << endl;
	}
	~A()
	{
		cout << "~A()>>" <<_a<< endl;
	}
private:
	int _a;
};
void func1(A aa)
{

}
void func2(A& aa)
{

}

传值传参有一次拷贝构造,这个拷贝构造是针对aa1的拷贝构造

引用传参没有拷贝构造

拷贝构造在传值返回和传引用返回的区别

#include<iostream>
using namespace std;
class A
{
public:
	A(int a = 0)
	{
		_a = a;
		cout << "A(int a=0)>>" <<_a<< endl;
	}
	A(const A& aa)
	{
		_a = aa._a;
		cout << "拷贝构造A(int a=0)>>" << _a << endl;
	}
	~A()
	{
		cout << "~A()>>" <<_a<< endl;
	}
private:
	int _a;
};
A fun3()
{
	static A aa;
	return aa;
}
A& fun4()
{
	static A aa;
	return aa;
}
int main()
{
	A aa1(1);
	fun3();
	cout << endl;
	fun4();
	return 0;
}

传值返回时,进行了一次拷贝构造

第一个:A(int a=0)>>1是A aa1(1)的默认构造

第二个:A(int a=0)>>0是fun3函数里面对 static A aa的默认构造

拷贝构造A(int a=0)>>0是fun3函数执行return语句时,对aa的拷贝构造

这是因为传值返回在返回时,会拷贝一份aa,然后返回的是拷贝的aa,对于aa本身,在函数结束后就会销毁,对于这份拷贝的aa,就要调用拷贝构造函数

对于拷贝的aa,也是有生命周期的,它的生命周期在main函数fun3();这一行,当这一行调用结束之后,就要拷贝的aa进行析构

稍作修改,使观察更清晰

对于引用返回,不需要拷贝构造函数,直接调用构造函数,调用过程如下图

析构过程如下

这里aa(3)和aa(4)本身在静态区,所以后析构,如果aa出了fun4()的作用域就销毁,那么引用返回就有问题,前面博客里有提到过

运算符重载

这里有俩个日期,d1和d2,我们比较它们的大小关系

C++中规定:内置类型可以直接使用运算符运算,编译器知道如何运算,自定义类型无法直接使用运算符,编译器不知道如何运算

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表),返回值类型由运算符特点决定,如比较大小一般返回true或false,若想知道差值,返回int
注意:
不能通过连接其他符号来创建新的操作符:比如operator@重载操作符必须有一个类类型参数用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this.*   ::   sizeof   ?:   . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。

我们可这样来对日期判断是否相等,但是这里会报错,因为日期这些是私有的

解决方法:

1.写一个函数来获取这些私有数据 ,月和天也同理

将上面的私有数据改为共有,再进行测试

using namespace std;
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
		cout << "构造函数" << endl;
	}
	//Date(const Date& d)//拷贝构造函数
	//{
	//	_year = d._year;
	//	_month = d._month;
	//	_day = d._day;
	//}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	int _year = 1;   // 注意这里不是初始化,给缺省值
	int _month = 1;
	int _day = 1;
};
int main()
{
	Date d1(2022,7,23);
	Date d2(2022, 8, 23);
	cout<<(d1 == d2) << endl;
	return 0;
}

编译器实际会将cout<<(d1d2)<<endl;转换为cout<<operate(d1,d2)<<endl;

本质是函数调用

编译器再工作时,如果发现d1,d2是内置类型,直接转换为相对应的指令,如果不是内置类型就看它是否满足运算符重载的条件,如果满足就执行

为什么不直接写一个调用函数呢?

如果写成这样,就没什么价值了,写成运算符重载,就能像内置类型一样去调用运算符,写成运算符重载可读性更好,如果写成函数,有人会不规范写函数名,导致其他人看不懂

在传参的时候,如果没有引用就会进行拷贝构造,如果是深拷贝,就会很麻烦,所以在传参的时候要引用,为了防止别人写错,最好加上const

但运算符重载,还存在一个问题,就是内置类型,一般是私有,上面为了演示效果我们改为了共有,现在改回来,但是会报错

为了解决这个问题,我们把运算符重载函数写道类里面,还会报错,参数太多

参数太多是因为,还有一个隐藏的参数this指针

我们做以下修改即可

_year==d2._year,这里的_year实际是this->_year,this就是d1

运算符重载应用

对于日期类,我们可以比较大小,也可以去查询N天以后/以前是几几年几月几号

思路:1.直接把天数加到day

2.若day超过了该月的总天数,用day-该月总天数,再给月+1,12月要把1,给年加1

	int GetMonthDay(int year, int month)
	{
		static int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
		//多次调用的时候,每次都要创建数组,现在改为静态,放到静态区

		if (month == 2
			&& ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))//闰年2月
		{
			return 29;
		}
		else
		{
			return days[month];
		}
	}
	Date operator+(int day)
	{
		
		_day += day;
		while (_day > GetMonthDay(_year,_month))
		{
			_day -= GetMonthDay(_year, _month);
			++_month;
			if (_month == 13)//12月特殊处理
			{
				_month = 1;
				_year++;
			}
		}
	}

这样写需要一个返回值,但是我们把日期全部加到了_year,_month,_day,因此要把这些给传回去,我们直接return *this,this是指针,*this是对象

先测试一下

现在写的这个,改变了d1,如果不想改变d1,把结果返回回去,做如下修改,main函数可以这样写,用一个东西来接收,符合拷贝构造

我们拷贝一份d1,就行,这样就不会改变d1了

	int GetMonthDay(int year, int month)
	{
		static int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
		//多次调用的时候,每次都要创建数组,现在改为静态,放到静态区

		if (month == 2
			&& ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))//闰年2月
		{
			return 29;
		}
		else
		{
			return days[month];
		}
	}
	Date operator+(int day)
	{
		Date ret(*this);
		ret._day += day;
		while (ret._day > GetMonthDay(ret._year,ret._month))
		{
			ret._day -= GetMonthDay(ret._year,ret. _month);
			++ret._month;
			if (ret._month == 13)//12月特殊处理
			{
				ret._month = 1;
				ret._year++;
			}
		}
		return ret;
	}
int main()
{
	Date d1(2022,7,23);
	Date ret = (d1 + 50);
	Date ret1(d1 + 50);
	return 0;
}

赋值运算符重载

  1. 赋值运算符重载格式
    参数类型:const T&,传递引用可以提高传参效率
    返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
    检测是否自己给自己赋值
    返回*this :要复合连续赋值的含义
     C语言中把一个数的值,赋值给另一个数,用=就可以,C++中把一个类的值,赋值给另一个类需要用到赋值重载

如这里,要把d3赋值给d1

void TestDate1()
{
	Date d1(2022, 7, 24);
	Date d2(d1);//拷贝构造
	Date d3(2022, 8, 24);
	d1 = d3;//赋值
}
int main()
{
	TestDate1();
	return 0;
}
#include<iostream>
using namespace std;
class Date
{
public:
	//构造函数会频繁调用,所以放在类里面作为inline
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void operator=(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};

main函数里d1=d3;会被转换成d1.operatpr=(&d1,d3);

传d1地址是因为有this指针

赋值重载连续赋值

写成连续赋值就会报错 

对于普通变量这种连续赋值是可以的

连续赋值是这样的:k=j=i,把i赋值给j,然后把j的返回值赋值给k

d2=d1=d3,也遵循上面的道理

d3赋值给d1,d1的返回值赋值给d2

这里应该这样修改,this是d1的指针(地址),*this就是d1

此时不会报错 ,如果不引用传参就要调用好多次拷贝构造

为了防止有人写成d2=d2,我们加上判断条件,d2=d2这条语句编译器是不会对其报错的

赋值运算符只能重载成类的成员函数不能重载成全局函数

写成全局的会报错

原因**:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重只能是类的成员函数。**

用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意**:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算**符重载完成赋值。

屏蔽掉赋值重载函数后,仍然可以拷贝
 

添加一个类进行测试

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重只能是类的成员函数。**

用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意**:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算**符重载完成赋值。

屏蔽掉赋值重载函数后,仍然可以拷贝
 

添加一个类进行测试

[外链图片转存中…(img-1SutqjCb-1715735619694)]
[外链图片转存中…(img-FcW8gTGF-1715735619694)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值