拷贝构造函数与运算符重载(c++详解)

拷贝构造函数引出

先给出日期类的定义:

class Date
{
    
    
public:
	

	Date(int year=1900,int month=1, int day=1)
	{
    
    
		_year = year;
		_month = month;
		_day = day;
	}
    void  print()
	{
    
    
		cout << _year << " " << _month << " " << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;

};

有一种场景,我们需要另一个对象和之前对象所初始化的值一模一样。

第一种方法:每次创建对象写成一样的值,但是如果d1的值改变了呢,其他在跟着修改,数据量很大时很不方便


Date d1(2020,2,1);
Date d2(2020,2,1);

第二种方法:采用拷贝构造函数

拷贝构造函数

拷贝构造函数:只有单个形参(不算那个隐含的this指针),该形参是对本类类型的一个引用(一般用const修饰),再用已存在的类类型对象创建新对象时由编译器自动调用。
特点:

  1. 拷贝构造函数是构造函数的一个重载。
  2. 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值会引发无穷的递归调用。
    在这里插入图片描述
    传值的话编译不通过。
    分析一下不通过的原因:
    假如我们要给d2对象拷贝d1的值 Date d2(d1);,根据以前的知识,传值传参需要发生拷贝把d1传给Date(const Date d)中的d,就是传值传参,所以,d1拷贝给d就又要调用拷贝构造函数Date d(d1)`,在调用。引发无穷递归。
    所以需要传引用
Date(const Date& d)
{
    
    
_year = d.year;
_month = d.month;
_day = d.day;
}

在和传值对比一下:
传引用时不需要拷贝,调用函数Date d2(d1); 然后看函数Date(const Date& d)的形参d,d是d1的一个别名。就完成了对 对象d1内成员的全部拷贝,但默认的拷贝构造函数只拷贝值,所以也被我们成为值拷贝,或者浅拷贝。
只拷贝值的含义是什么呢,在这个例子可能理解的不是很清楚,我们看到这两个的值虽然是完全一样的,但是另一种场景呢?

class stack
{
    
    
public:
	stack(int capacity=4)
	{
    
    
		_a = (int*)malloc(sizeof(int)*capacity);
		_capacity = capacity;
		_size = 0;

	}
	~stack
	{
    
    
	free(-a);
	_capacity=0;
	_size=0;
	}
	
private:
	int* _a;
	int _size;
	int _capacity;

};
int main()
{
    
    
	stack s1;
	stack s2(s1);
	return 0;
}

这里我们想干的是在建一个对象s2,这个对象也想s1一样有一样的一个一样大小的数组,但是我们发现
在这里插入图片描述
其他一样是一样了,但是s2的指针,和s1的指针竟然指向了同一块空间,这就是浅拷贝。比如说,我看见了舍友有一个很好用的手机,我想要一个跟他内存一样大的手机,而不是和他共用一个手机。

而且这个程序最后会崩溃,创建s2,s2后进系统上的栈,先出所以s2对象生命周期先结束,会调用我们写的析构函数(自己生成的析构函数不释放)释放对上的空间,而s1此时是浅拷贝,和他共用一个空间,生命周期结束,再次调用析构函数,又free了一次,完蛋。

由于日期类里面没有指针什么的,所以默认生成的就可以用,因为它只需要浅拷贝所有字节, 而栈里,则是必须我们要自己写一个拷贝构造实现深拷贝(要一块跟你一样大的空间,让指针指向自己的),因为不写,就出现了上述举例的情况

运算符重载引出

C++为了增加代码的可读性引入了运算符重载。自定义类型就可以像内置类型一样去用运算符。需要我们自己定义运算符重载

Date d; 
d+1;//日期加一天

运算符重载

运算符重载是具有特殊函数名的函数,也具有返回值类型,函数名字,及参数列表,其返回值类型与参数列表与普通函数类似。
函数名字:关键字operator后面接需要重载的运算符符号。
函数原型: 返回值类型 operator 后面接需要重载的运算符符号
注意:

  • 不能通过连接其他符号创造新的操作符
  • 冲在操作符必须有一个类类型或者枚举类型的操作数
  • 用于内置类型的操作符其含义不能改变,例如内置的整形+,不能改变其含义
  • 作为类成员函数,其形参看起来比操作数数目少一,因为成员函数有个默认的形参this指向调用对象,限定为第一个形参。
  • .* :: sizeof ?: . 以上五个运算符不能重载(面试选择题经常考)
  • 想用什么操作符去自己重载

第一次写了这样,忘记了作为成员函数的时候,第一个参数已经有了就是隐含的this指针,指向调用对象。
在这里插入图片描述
不需要改变所以加上const,传值的话外面会调用拷贝构造函数,传引用就不调用了。

bool operator==(const Date& d2)
	{
    
    
		return _year == d2._year && _month == d2._month && _day == d2._day;
	}

开始的时候运算符函数形参写的是这样 const Date d2,虽然也可以,但是传递过去会调用拷贝构造函数。
但是脑子一混,把operator==函数当成了拷贝构造函数,还在想拷贝构造函数的形参是const Date d2的话不就无穷调用了吗。(zzz,你不重新写拷贝构造函数,人家默认生成的的参数肯定是引用类型的,不用你操心)

赋值运算符重载

把d2的值赋值给d1,需要注意的是它与拷贝构造是不一样的。赋值说明的是,两个对象已经定义出来了。

Date d1;
Date d2(2021,2,6);
d1=d2;

而拷贝构造则是将d1的值直接初始化给d2。两者是不一样的

Date d1;
Date d2(d1);
//赋值重载
	void operator=(const Date& d)
	{
    
    
	//自己赋值自己没有意义
	//开始写成这样,让对象比较。。还要写一个运算符重载函数比较 if(*this!=d)
	if(this!=&d)
	{
    
    
		_year = d._year;
		_month = d._month;
		_day = d._day;
	          }
	}

但是在我们的内置类型中是支持连续赋值的
比如说:

int i,j,k;
i=j=k=10;

也就是说k=10,有一个返回值k,让j=k,返回j,又让i=j。
所以给我们的函数加上返回值,类型为类名,返回this所指向的对象

//赋值重载
	Date operator=(const Date& d)
	{
    
    
	//自己赋值自己没有意义
	if(this!=&d)
	{
    
    
		_year = d._year;
		_month = d._month;
		_day = d._day;
	          }
	return *this;
	}	

当返回值为Date类型,*this传递给外面需要两次拷贝构造(先传给临时变量(Date类型),临时变量再给外面用于接收的变量)

当返回值为Date&,临时变量是返回值的别名,减少一次拷贝构造。形参做引用(总共一次)也减少一次拷贝构造。
所以下面这样写一共减少了两次拷贝构造。

   //最终版赋值重载
	Date& operator=(const Date& d)
	{
    
    
		//自己赋值自己没有意义
	if(this!=&d)
	{
    
    
		_year = d._year;
		_month = d._month;
		_day = d._day;
	          }
	   return *this;
	}
	

注意:返回值做引用有一个条件就是看你返回值的生命周期,假如this所指向的对象生命周期在本函数体内,那么this传给中间变量,此时this销毁,中间变量是个引用也跟着销毁。外面的变量接收不到它。但是在这里*this是d1对象,生命周期在main函数里,所以不用担心

当我们不写,他也会自动生成,不过默认生成的他也是只进行浅拷贝,只拷贝值,像之前写拷贝构造函数里的Static,里面的指针变量。当d1=d2时,两个对象里面的两个指针指向同一块空间,析构的时候释放两次,程序崩溃。

猜你喜欢

转载自blog.csdn.net/qq_45928272/article/details/113713270