(六)C++学习 | 运算符重载(2)


1. 简介

继上一文介绍了运算符重载的基本概念、赋值运算符这一特殊运算符的重载以及利用运算符重载实现一个可变长数组类后,本文将介绍 C {\rm C++} 中的其他几类比较重要的运算符的重载,包括流插入流提取运算符、类型转换运算符和自增自减运算符等。本文直接使用的复数类的具体内容可参考上文。


2. 流插入运算符和流提取运算符的重载

C {\rm C++} 中流插入运算符和流提取运算符其实分别是对左移运算符<<和右移运算符>>的重载。例如,我们经常使用的一个形式是cout << 5 << "this";,它能成立的前提是什么?即通过何种重载方式可以实现以上功能。在使用 c o u t {\rm cout} 前我们都要包含头文件 i o s t r e a m {\rm iostream} c o u t {\rm cout} 其实是该头文件中 o s t r e a m {\rm ostream} 的对象。所以,cout能够作用在<<上的前提就是在类 o s t r e a m {\rm ostream} 中对该运算符进行了重载。我们现在来考虑,如何重载左移运算符使得cout << 5;cout << "this";均成立,根据上一文的知识,我们首先可以分别写出以下代码:

// 执行cout<<5;
void ostream::operator<<(int n){
	...	// 输出n的语句
	return;
}
// 执行cout<<"this";
void ostream::operator<<(char* s){
	...//  输出s的语句
	return; 
}

这里的返回值类型暂定为空。我们现在考虑cout << 5 << "this";这个整体语句,我们可以将其展开写为(cout.operator<<(5)).operator<<("this);,即cout << 5;的返回值再去使用重载后的运算符。所以不能将运算符重载的返回值置空,因为只有 o s t r e a m {\rm ostream} 的对象才能使用重载后的运算符,显然我们应该将上述程序的返回值类型设置为ostream &,返回*this(这里返回值类型使用引用的目的是实现深拷贝)。

更进一步,当我们要使得 c o u t {\rm cout} 作用于类的对象时,即cout << s << 5;,此时 s {\rm s} 和类 S a m p l e {\rm Sample} 的对象。则我们要考虑继续对左移运算符进行重载。前面我们已经提到,左移运算符是定义在头文件 i o s t r e a m {\rm iostream} o s t r e a m {\rm ostream} 类已经重载好的成员函数(双目运算符只有一个形参),我们不可能再给它加入其他成员函数,所以这里我们考虑将其重载为全局函数(双目运算符有两个形参)。得到如下形式:

ostream& operator<<(ostream& o, const Sample& s){
	o << s.value;
	return o;
}

我们注意上面重载函数的第二个形参,一方面我们将其设定为常量是程序内部不会再修改它;二来将其定义为类的引用可以避免生成临时对象额外调用复制构造函数而产生额外的开销。对于重载后返回值的类型为引用的原因同上。

进一步,为了直观地实现复数的输入与输出,假设我们需要完成如下功能:cin >> c;接受键盘的输入形式是a + bi,而cout << c;得输出形式也为a + bi。即:

// 由键盘输入1+2i 3
cin >> c >> n;
// 输出1+2i 3
cout << c << n;

则我们需要对左右运算符和右移运算符作进一步重载。由上面得介绍,我们只能将重载函数定义为复数类的全局函数(将其声明为类的友元函数)。根据上面对cout << s << 5;重载的例子,易得:

ostream& operator<<(ostream& o, const Complex& c){
	o << c.real << "+" << c.imag << "i";
	return o;
}

右移运算符的重复比上面左移运算符的重载更复杂。由于键盘接受的是a + bi整体,所以我们首先考虑使用字符串保存该输入,然后通过加号分离出输入复数的实部和虚部。程序如下:

istream& operator>>(istream& i, Complex& c) {
	// 使用s保存键盘的输入内容,格式需严格遵守a+bi的形式
	string s;
	i >> s;
	// 从位置0处开始查找字符“+”并返回其位置
	int pos = s.find("+", 0);
	// 分离出实部和虚部
	string rTmp = s.substr(0, pos);
	string iTmp = s.substr(pos + 1, s.length() - pos - 2);
	// 使用实部和虚部
	c.real = atof(rTmp.c_str());
	c.imag = atof(iTmp.c_str());
	return i;
}

3. 类型转换运算符的重载

C {\rm C++} 存在强制类型转换,如(double) a;就将 a {\rm a} 的类型强制转换为浮点型。我们现在来介绍类型转换运算符的重载方式。如果我们希望将类型转换运算符作用于复数类对象上,并返回复数的实部。我们这里将其重载为类的成员函数,又它本身是一个单目运算符,所以就不带形参,形式如下:

operator double() { 
	return real;	// 返回该对象的实部
}

重载后的类型转换运算符有两种调用方式,一种是显式调用;一种是隐式调用。

Complex c(1.1, 2.3);
cout << double(c) << endl;	// 显示转换,输出复数的实部1.1
double n = 2 + c;			// 隐式转换,相当于double n = 2 + c.operator double() {}
cout << n;					// 输出3.1


4. 自增和自减运算符的重载

首先我们知道,在 C {\rm C++} 中自增和自减运算符有前后之分。如++a;a++;在单独使用的时候,二者产生的结果相同;但当要使用运算后的结果时则不能将二者混为一谈。而在 C {\rm C++} 中对自增和自减运算符进行重载时,为了区分是实现前置功能还是后置功能,有如下规定:前置运算符作为一元运算符重载(成员函数时没有参数,非成员函数时有一个参数),后置运算符作为二元运算符重载(成员函数时有一个参数,非成员函数时有两个参数)。 而我们知道自增自减运算符本身是一元运算符,所以当后置运算符重载为非成员函数时,额外的参数只是当作标志使用,不会参与实质性运算。即自增自减运算符重载后所完成的功能为:

// 定义CDemo类对象c,并调用构造函数将c的值赋值为5
CDemo c(5);
cout << (c++);	// 等价于c.operator++(0),后置重载时为二元运算符,0不参与运算
cout << c;		// 输出(c+1)的值
cout << (++c);	// 等价于c.operator++(),重载为成员函数
cout << c;		// 输出(c+1)的值
cout << (c--);	// 等价于operator--(c, 0),后置重载时为二元运算符,0不参与运算
cout << c;		// 输出(c-1)的值
cout << (--c);	// 等价于operator--(c),重载为非成员函数
cout << c;		// 输出(c-1)的值

在具体实现自增和自减的重载时,我们需要注意几点:

  • 前置运算符返回的是变量的引用,如++a/--a;返回的是对a的引用;而后置运算符返回的是一个临时变量,其值为a参与运算前的值。所以,对前置运算符的重载,我们的返回值类型应该为引用形式;
  • 我们在打印相关值时可以发现,++a;a改变后的值;a++;是改变前的值;
  • 对于上述的输出语句,我们可以根据第二节的内容重载左移运算符,也可以根据第三节内容重载类型转换运算符(这里是隐式转换)。具体实现如下:
// 类型转换运算符的重载,将对象转换为整型
operator int() {
	return n;
}
// 前置加加重载为成员函数,++i相当于i.operator++()
CDemo& CDemo::operator++()
{
	++n;
	return *this;	// 返回对象的引用
}
// 后置加加重载为成员函数,i++相当于i.operator++(int)
CDemo CDemo::operator++(int)
{
	CDemo tmp(*this);	// 使用*this初始化,记录对象修改前的值
	n++;
	return tmp;			// 返回修改前的值
}
// 前置减减重载为非成员函数,形参类型为引用可以改变实参本体
CDemo& operator--(CDemo& c)
{
	c.n--;
	return c;
}
// 后置减减重载为非成员函数
CDemo operator--(CDemo& c, int)
{
	CDemo tmp(c);	// 使用c初始化tmp调用默认赋值构造函数
	c.n--;
	return tmp;		// 返回修改前的值
}

由前置运算符和后置运算符的重载的实现形式可知,在后置运算符的重载时会首先生成一个临时值,然后再返回临时内容。所以,在不影响最终结果的情况下我们应尽量使用前置运算符,这可以在一定程度上加快程序运行。如在循环变量里使用++i而不是i++,虽然二者实现的功能一致。


5. 总结

我们这里对运算符重载作一个总结:

  1. C {\rm C++} 中,重载的运算符仅限于已经存在的运算符(上文介绍 C {\rm C++} 中运算符的优先级已经提到),不能定义新的运算符;
  2. 一般来说,为了增加程序的可读性,重载后的运算符不应改变原运算符的使用习惯和基本功能
  3. C {\rm C++} 中,运算符重载不改变原有运算符的优先级
  4. 以下运算符不能重载..*::?:sizeof

运算符重载可以增加原运算符作用范围,丰富运算符的功能,但我们同时要注意保留原运算符的基本特性。 在类中定义运算符重载,可以将运算符的计算能力引入类的对象中,使得整个程序的可读性更强。合理地利用运算符重载可以使得我们的代码更加整洁、高效,且更加便于维护和修改。

运算符重载的另一个广泛应用可以在 P y t h o n {\rm Python} 语言中体现,由于大量使用重载后的运算符, P y t h o n {\rm Python} 语言的书写编写变得很简单。但如果想要写出整洁高效的代码,我们依然需要熟悉重载后运算符的使用。例如其中常用到的数组切片操作就是对:进行了重载、字符串的相加可以使用重载后的+完成、字符串的连续赋值可以使用*完成等等。我们也可以合理地利用运算符重载开发出一套属于自己的代码!


参考

  1. 北京大学公开课:程序设计与算法(三)C++面向对象程序设计.


猜你喜欢

转载自blog.csdn.net/Skies_/article/details/106326061