C++ 赋值运算符重载

赋值运算符重载

 运算符重载:

C++为了增强代码的可读性,可以对 运算符 进行重载,运算符重载  就是具有特殊函数名的函数,这个函数也具有返回值类型,函数名字和参数列表,它的返回值和参数列表的形式和普通函数类似。

比如,我们在比较内置类型大小的时候,可以使用  <   ,   >   ,  ==   等等这些运算符来实现,那么在C++当中也希望,我们的自定义类型也能  和 内置类型一样使用这些运算符,所以他就搞了一个运算符重载, 其实就是实现一个函数来 实现原本 运算符实现的 效果,然后我们在使用 这些运算符的时候,如果是对应的自定义类型,就会调用这个函数,来直接实现结果。

这样的话,假设我们要比较两个自定义类型的大小,就不用写一个函数,然后再主函数中进行调用,这样就算是 写了注释,而且 函数名命名 好 的函数也需要花费时间来阅读,来思考这个函数到底实现了什么功能,如下所示:

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//private:
	int _year;
	int _month;
	int _day;
};

bool Dateequal(const Date& date1, const Date& date2)
{
	return date1._year == date2._year
		&& date1._month == date2._month
		&& date1._day == date2._day;
}

int main()
{
	Date d1(2020,1,1);
	Date d2(2020,1,1);
	bool Mybool = Dateequal(d1, d2);
	cout << Mybool << endl;  //1

	return 0;
}

我们发现这个代码,就是实现了一个函数,然后调用这个函数,这样子,就算我们命名给了提示,他还是需要看代码和注释来了解这个函数实现了什么,那么如果我们比较内置类型,直接使用 == 这个运算符就可以实现了,他返回的也是 bool  类型的值:

int a = 10;
int b = 10;

cout << a == b << endl;

这样是不是非常的直观,我们一看就知道这个 a == b 是什么意思,那么我们也想 自定义类型也这样用,那么此时就可以使用 运算符 的重载了,运算符的重载使用了 operator 这个关键词。

语法:

返回值类型 operator操作符(参数列表)

那么上述例子,我们就可以这样来实现这个 运算符的重载:

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

然后我们在主函数中就可以这样使用:

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

int main()
{	
	Date d1(2020,1,1);
	Date d2(2020,1,1);

	cout << (d1 == d2) << endl;//1
	return 0;
}

如上述,要实现了之前的效果,但是可读性大大提高了。

需要注意的是:因为我们写的是运算符重载,这个运算符也是有优先级的,就像上述, " << " 流运算符的优先级是很高的,如果我们想要  得到  d1 == d2  这样的结果,输出的话,那么我们最好是加上括号。

如上述,如果我们不加括号 就会报错:

 他会先运算  cout << d1 这一个表达式,那么就会出错。

 当然,我们在选择重载运算符的时候,也要看看这个重载运算符实现的结果对这个类是否有意义,比如上述,日期 -  日期    计算天数,这个是有意义;但是 日期 + 日期 这个的结果就没有什么意义。

 现在我们来实现,日期的比较大小  (  < ) :

bool operator< (const Date& d1, const Date& d2)
{
	if (d1._year < d2._year)
	{
		return true;
	}
	else if (d1._year == d2._year && d1._month < d2._month)
	{
		return true;
	}
	else if (d1._year == d2._year && d1._month == d2._month && d1._day < d2._day)
	{
		return true;
	}

	return false;
}

int main()
{	
	Date d1(2020,1,1);
	Date d2(2020,1,1);

	//cout << (d1 == d2) << endl; //1
	cout << (d1 < d2) << endl; //0
	return 0;
}

 我们只能重载 自定义类型的 运算符重载,如果全是是内置类型重载,这样是不行的:

 运算符的重载,参数列表当中必须有一个是 自定义类型:

 如上图,此时编译通过。

而且上述的参数个数也是有限定的,运算符有多少个操作数,我们重载的函数就应该有几个参数

 我们上述都是直接访问Date类当中的 成员,成员的访问权限是 public的,其实成员应该是 protected 的,如果需要访问 protected 的成员,需要其他方法,比如友元解决,但是有元是突破访问限制,这样是不到万不得已不使用的,所以我们就干脆直接使用 类当中的成员函数:

也就是在类当中来定义这个函数:

	bool operator< (const Date& d)
	{
		if (_year < d._year)
		{
			return true;
		}
		else if (_year == d._year && _month < d._month)
		{
			return true;
		}
		else if (_year == d._year && _month == d._month && _day < d._day)
		{
			return true;
		}

		return false;
	}

 需要注意是:我们上述 函数 只使用了 一个参数,因为在类当中的 成员函数,在调用的时候会给一个this 指针,这个this 指针指向的就是当前对象指针,而我们上述也说过,重载的运算符函数的参数个数,是和对应的运算符的操作数是相等的,所以如果我们 此处像之前一样给了 两个参数,就会报错:

而上述我们在调用这个  重载运算符函数的时候,我们是直接  d1 < d2  ,这样来实现,但是其实,因为是在类当中实现的,那么我们应该像访问类当中的成员函数一样来调用这个函数,但是上述我们并没有报错,其实是 编译器会对这个进行转换,例如这个 d1 < d2 ,就被转换为  d1.operator<(d1,d2);  这样的表达式,这也符合我们对类当中成员函数的访问方式。

当然我们也可以这样来使用这个  重载的运算符函数:

d1.operator<(d2)

 这样看似只传入一个参数,其实是两个参数,因为这个函数还需要传入这个 d1 对象的this 指针。

 我们把上述两种方式都写出来,看看汇编代码是如何写的:

 两个代码反汇编是一样的。

 以下五个操作符是不能进行重载的:

  • .*  
  •  ::        域访问操作符
  •  sizeof      计算大小
  •  ?:      相当于 if
  • .      成员访

总结  :

在 运算符重载当中,我们需要注意的是:
注意:

  • 不能通过连接其他符号来创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型参数
  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
  • .  *   ::   sizeof   ?:    . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
  • 是否要重载运算符,要看这个运算符实现效果对这个类是否有意义

 赋值运算符重载

 赋值运算符就是  " = " 这个运算符,他的定义方式和其他函数的定义方式是一样的,都是使用 operator  这个关键词来定义,如下例子:

void operator=(const Date& d1, const Date& d2)
{

}

需要注意的是我们需要把 赋值运算符 和 拷贝构造函数 这两个 区别开来;拷贝构造函数的本质是构造函数,他是在主函数中有一个 对象的时候,调用拷贝构造函数,来创建一个新的对象,把其中的值拷贝过去;而赋值运算符 重载函数本质是 运算符重载,他是在主函数中本来就有两个对象,然后把 =  前面对象当中的值,赋值给第二个对象当中。

 现在我们在类当中定义这个 赋值运算符重载函数:

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

Date类当中只需要 浅拷贝就能实现 赋值,向上述一样,就是最简单的赋值操作符的重载。

d1 = d2  执行之前:

 d1  =  d2 执行之后:

 我们发现,成功赋值了。

但是这样写有一些问题:

我们在内置类型当中使用赋值运算符的时候,可以一次多次赋值:

int a = 10;
int b = 10;
int c = 10;

a = b = c = 0;

 他是从右到左进行一次赋值的:

 如果我们把 上述实现的 重载赋值运算符 运用在 我们实现的自定义类型当中,进行如上述的多元赋值,就会有些问题:

	d4 = d3 = d1;

像这个二元赋值的表达式,在运算的时候就报错了:

二元  " = " 这个指的就是 赋值给 d4 的这个 " = " ,意思就是这个 " = "  的右值是 void 类型的,那就不能再进行赋值了,如下图:

 

 这时因为:我们在写 赋值运算符函数的时候,给的返回值就是 void 的,那么这样就不太好,向上述的多元赋值就不行。

 我们之前说过,像 i = j = 0; 对于 i 来说,他的接收的值是 j ,也就是说 j = 0 这个表达式返回值是 j,所以我们在 函数中的返回值也应该是对应的对象,我们可以使用this 指针来返回 当前对象:

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

        return *this;
	}

上述代码能实现,但是其实上述代码还是有些问题,因为上述代码是传值返回,是解引用this 指针,来返回这个 对象,我们知道,函数返回是需要创建一个临时变量来拷贝到主函数中接收的变量的,如果这个对象很大,那么对内存和效率的消耗也很大。

所以,因为上述中的对象是在主函数中的,他的生命周期是在主函数中的,这个函数销毁之后,对象不会被销毁,那么我们就用使用 引用返回,这样就不用 在创建临时变量了。

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

        return *this;
	}

还有一种情况: d1 = d1 ,这种情况,在一般情况下是不会报错的,但是在一些极特殊的情况下就会报错,所以我们可以在函数中加一个断言,不给这样使用。

 比如这样判断:

if( this != &d)
{
     //执行代码
}

这样写也行:

if(*this != d)

但是上述是要写了 != 重载运算符函数才行,而且这样写代价有点大,每一次都需要调用函数去判断。

 对于赋值 运算符的重载函数,如果用户没有显示定义,那么编译器会自动创建 赋值运算符重载。

这个默认的重载和 拷贝构造函数 的行为是一样的:内置类型/成员---值拷贝/浅拷贝;自定义类型拷贝----会去调用它的赋值重载函数。

像这里的Date 类是不用我们去写 赋值运算符重载函数的,因为这里只是浅拷贝,浅拷贝默认的赋值运算符重载就能帮我们实现。(浅拷贝就是值的方式逐字节拷贝)

像类似Stack 这样的,对应类的对象当中里面有 开辟空间的 ,这时候需要深拷贝,这时候就需要我们来 写了。

 注意:我们之前实现的 普通运算符重载函数,既可以写在 全局中,又可以写在类当中,构成成员函数。但是这里的 赋值 运算符重载函数,就不能写成 全局的。

 因为这里的 赋值运算符重载函数是一种特殊的 运算符重载函数,他是默认成员函数,而普通的运算符重载函数不是默认成员函数。赋值运算符重载函数,只能再类当中进行定义和声明,当然,如果定义和声明都是在类当中的,声明和定义是可以分离的。

如这个例子:

我们在全局定义了这个 赋值运算符重载函数,发现直接报错了。 

理解 << 流插入 操作符 和 >> 流提取 操作符

  << 流插入

 现在我们就理解了,为什么在C++当中的 << 流插入操作符 可以自动识别 数据类型,其实就是实现了很多的 << 运算符重载函数:

 

 如上图所示,实现了很多 << 的运算符重载函数。

但是 << 流插入操作自动识别是在库里实现的,内置类型的重载,但是自定义类型没有重载,所以,当我们要想使用 << 来打印 自定义类型的 数据的时候,需要重写 << 操作符重载函数:

例子:

实现日期类的流插入函数:

void Date::operator << (istream& out)
{
	out << _year << "年" << _month << "月" << _day << "日" << endl;
}

int main()
{
	Date d1(1949, 10, 1);
	cout << d1;

    return 0;
}

上述实现是错误的,会报如下的错误:

 这是因为我们在 main  函数中调用方式有问题,我们以往在调用重载运算符函数的时候,比如        d1 + d2,在编译的时候会转换成  d1.operator+(d2) 这样的形式来调用;那么上述的              cout << d1 ,我们想要转化的是这样的---  d1.operator<<(cout) ;这样的形式,但是注意的是:上述转化应该是这样的: cout.operator(d1) ; 这样的话,编译器就不能识别了,我们这样使用就可以打印了: d1 << cout  。 如下所示:

 d1 << cout 才会转化为 我们想要的  d1.operator<<(cout) ;的形式。

但是 d1 << cout  这样使用很别扭,一般都是  cout << 对象,这样来实现的,因为我们在说 << 流插入操作符的时候,说的是,右边的对象流入左边 cout (终端控制台)中,如果我们想要实现像之前 cout << 内置类型 ;这样的操作的时候,我们需要先在库当中实现Date类的定义,修改库当中(ostream类)定义的 << 重载函数,因为只有在 ostream类当中,第二个参数才能是 Date。

但是,库当中的代码我们是不能修改的,所以,我们在类当中实现 << 流插入操作的重载是不可行的,因为 如上述 在 Date类当中,我们实现成员函数,默认第一个参数就是 做的是 左操作数,这就注定了,我们在使用这个 << 重载函数的时候,就非常别扭,不符合使用习惯。

所以我们把这个函数定义为全局的函数,这样我们就可以定义参数的位置了:

int Date::Getyear()
{
	return _year;
}

int Date::Getmonth()
{
	return _month;
}

int Date::Getday()
{
	return _day;
}

void operator <<(ostream& out,const Date& d)
{ 
	out << d.Getyear() << "年" << d.Getmonth() << "月" << d.Getday() << "日" << endl;;
}

 我们在类当中实现三个函数,分别帮我们拿到 三个成员的数据。

类似这样的一种定义,这样我们在调用这个函数的时候就,可以像 cout << d1 这样的方式来调用了。

除了像上述一样用 对应 的 get 和 set 函数可以拿到 和 修改 对象当中 成员的值,在C++当中我们还可以用 友元函数来实现,我们可以在类当中的任意位置来写入下面这个代码:

 

 这样就表示,这个函数被当做为这个类的朋友,引入这个函数做为朋友,那么就可以突破这个类当中的访问权限的限制,直接访问到类当中的 成员和 成员函数。

像上述的操作,就是友元函数的声明。

那么关于 << 流插入函数的实现还是有一些问题,我们在使用 << 流插入的时候,还会这样来使用:

cout << d1 << d2 << d3 << endl;

那么向上述 的过程是 d1 先流入 cout 然后再是 d2 ,最后是 d3,所以我们应该这个函数应该返回一个 ostream& 的值:

ostream& operator <<(ostream& out,const Date& d)
{ 
	out << d.Getyear() << "年" << d.Getmonth() << "月" << d.Getday() << "日" << endl;;

	return out;
}

>> 流提取

 同样有了输入,就有输入, >> 提取流,就能帮助 对 自定义类型当中的成员进行输入:
 

istream& operator >> (istream& in, Date& d)
{
	in >> d._year >> d._month >> d._day;

	return in;
}

总结

 C++为什么要引入流这些操作符,不仅仅是因为更方便理解,我们在C当中的 printf()和scanf()等等这些输入输出的函数,只能对内置类型进行输入输出,对自定义类型不能像内置类型一样的输出,那么在C++当中充值了这个 << 和 >>  两个操作符之后,就可以对自定义的类型进行输入输出。

除了自定义类型的输入和输出,我们自定义类型当中的成员的值,有时候也是有输入和输出格式的,比如上述的日期,月就之后 1- 12 ,没有类型 13 这样的月份,那么我们在用 >> 提取流输入的时候就可以进行判断:
我们可以在  >> 和 <<  重载函数中去 判断,也可以在 构造函数的中去判断,如下就是在构造函数中去判断:

Date::Date(int year, int month, int day)
{
	if ((_month < 13 && _month > 0) && _day < GetMonthDate(_year, _month))
	{
		_year = year;
		_month = month;
		_day = day;
	}
	else
	{
		cout << "非法输入" << endl;
	}
}

  • 6
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

chihiro1122

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值