0%

C++运算符重载

一、运算符的定义

运算符重载就是运算符的“一符多用”。重载运算符是具有特殊名称的函数:保留字 operator 后接需定义的操作符符号。像任意其他函数一样,重载操作符具有返回类型和形参表,每个操作符用于内置类型都有关联的定义,当内置操作符和类型上的操作存在逻辑对应关系时,操作符重载最有用,最直观,使用重载操作符并不是创造命名操作。

二、在哪种情况下使用哪种重载运算符的方式合适?

C++ 提供了两种重载运算符的方式,在大多数情况下:

  • 只有一个操作数的运算符(一目)使用类运算符重载方式为好;
  • 一般地说,如果运算符要修改操作数(类对象)的状态(值),则应使用类运算符(成员形式)。(在计算中可能改变操作数的值得运算符被称为有副作用的运算符,诸如:=、+=、-=、*=、/=、%=、++、– 等);
  • C++规定,运算符=、()、[ ]、-> 只能采用类运算符形式重载;
  • 有两个操作数的运算符(二目)使用友元运算符重载方式为好;
  • 友元运算符重载方式在操作数的数据类型自动转换方面更为宽容,尤其是第一个操作数希望能够隐式类型转换时,则应采用友元形式;
  • 不允许重载的运算符有:&&、||、. 、:: 、 * 、?: 。

三、运算符重载具体讨论(返回值和参数,这里讨论几个常用的运算符)

默认地,重载运算符必须与内置操作符保持一致,也就是说重载后的运算符必须与本来内置操作符保持特性一致。函数最主要的两个就是返回值和形参。

3.1、前缀++类运算符重载函数(前缀–类似)

自增(自减)操作符的前置式定义:累加(递减)而后取出;后置式定义:取出而后累加(递减)。

我们知道,在C语言里整型变量是允许连续前缀++两次的,也叫链式操作。这样为了保证重载运算符与内置操作符++类型一致,就要求前缀++类运算符重载函数也支持连续操作(链式操作),所以前缀++类运算符重载函数的返回值必须是类名的引用。上面第二点也说了,++作为单目运算符,并且会修改操作数的值,则应定义为类运算符,这样重载函数无形参。我们就可申明该前缀++类运算符重载函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Zoo



{



public:



Zoo(int lion_n = 0, int tiger_n = 0){



lion = lion_n; tiger = tiger_n; }



~Zoo(){}







Zoo& operator++();//无参,返回值为类名的引用



private:



int lion;



int tiger;



};

下面就是前缀++类运算符重载函数的实现了

内置类型前缀++操作符是直接修改了操作数,然后返回修改后的操作数本身(唯一地址),不存在复制的情况,所以重载函数也应遵循这一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Zoo& Zoo::operator++()



{



++lion;



++tiger;



return *this;



}

由于函数的返回值类型被定义为引用,所以不会发生复制,返回的是操作数本身,完全符合内置前缀++的语法定义。

我们再来考虑错误情况:如果前缀++类运算符重载函数的返回值是类型,也就是返回一个对象,其对应实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Zoo Zoo::operator++()



{



++lion;



++tiger;



return *this;



}

咋一看上面的也实现了前缀++的功能,但是返回值是对象,在函数返回时会发生复制,虽然该函数成功将操作类对象的成员修改了,但是函数返回的是一个复制品,然后再执行++链式操作时,修改的会是这个复制品的值(相当于这个复制品调用前缀++类运算符重载函数),本尊并没有修改,也就是不能成功实现链式操作,不符合内置++的语法定义( C++中,前缀++是可以连续前缀两次以上的,但后缀++则不可以)。

1
2
3
4
5
Zoo zoo;



++(++zoo);

上面执行后,zoo.lion = 1,zoo.tiger = 1 。并不是期望的2。

至于返回其余类型那就更加错误了。

3.2、后缀++类运算符重载函数(后缀–类似)

与前缀++操作符一样,后缀++也是单目操作符,也会修改操作书本身,所以二者的形参数目和类型相同,为了区别函数,后缀++操作符接受一个额外的(即,无用的)int 型形参。使用后缀++操作符时,编译器提供0作为这个形参的实参。

与前缀++类运算符截然相反的是,后缀++返回值的类型恰恰不能是类的引用,其目的是在返回值时引起复制,即让一个并未自增的替身对象去参加表达式的后续运算,另外C/C++在语法上不允许后缀++连续运算两次以上,也就不要求返回引用,并且必须返回 const 对象。我们看看内置后缀++操作符:

1
2
3
4
5
6
7
8
9
int i = 0;



int j = i++;



i++++; //违法

内置后缀++操作符,操作数 i 本身已经完成了自增,但是后续的赋值操作并不是将自增后的 i 赋值给j,而是将并未自增的替身参与赋值运算。所以在重载后缀++类运算符的时候,我们应该考虑这点,另外必须返回一个 const 对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const Zoo Zoo::operator++(int)



{



Zoo ret(*this);//拷贝构造函数,构造复制品



++lion;//本尊自增



++tiger;



return ret;//返回复制品



}

在已经定义了前缀++类运算符重载函数的情况下,后缀++类运算符重载函数一般这样实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const Zoo Zoo::operator++(int)



{



Zoo ret(*this);//拷贝构造函数,构造复制品



++(*this);//本尊自增



return ret;//返回复制品



}

3.3、二目运算符重载(+=,-=,+,-)

先说复合赋值操作符,上面“+=”,“-=”也可认为是赋值操作符。内置+=、-=、%= 是允许进行链式操作的(如果不确定是否允许,可以写一个测试程序判断),所以为了与内置类型的操作一致,重载函数毫无疑问是返回一个引用,也避免了创建和撤销结果的临时副本。

但是“+”“-” 等是返回一个新的结果,这就要求算术运算符的重载不能返回一个引用,另外+的表达式也不能作为左值。

1
2
3
4
5
6
7
8
9
10
11
12
13
int i = 1;



int j = 2;



i = i + j + j;//可以连续+,但是右边的i,j还是原值,(i+j) = i + i;错误!



i += (i += j);//复合了赋值操作符,这样是允许的

有了前面分析,不难写出上面的重载函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Zoo& Zoo::operator+=(Zoo &rhs)



{



lion += rhs.lion;



tiger += rhs.tiger;



return *this;



}

继而过来讨论“+”“-”:

返回值是一个右值

前面说到了,“+”“-”是返回一个新的结果,算术运算符通常产生一个新值,该值是两个操作数的计算结果,它不同于任一操作数且在一个局部变量中计算,返回对那个变量的引用是一个运行时错误。通俗一点,假如算术运算符重载函数返回一个对象的引用,这个引用是两个操作的计算结果,它的本体就会是一个局部变量(对象),返回一个局部变量的引用,是错误的。所以对于类算术运算符的重载,只能返回一个右值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Zoo operator+(Zoo &first, Zoo &second)



{



Zoo ret(first);//拷贝构造函数,构造一个局部变量,用于返回值



ret += second;//运算操作







return ret;//返回一个值



}

二目算术运算符重载通常使用友元运算符重载方式。

从上面也可以看出,类运算符的重载最好与内置运算符保持一致,虽然没硬性规定,但这俨然成了一个默认规定。

另外 !(逻辑反)、~(按位与)、-(负号)等与二目算术运算符有类似之处,那就他们都不会修改原对象数据成员,而是将运算结果交给一个新值,所以在重载时,需要构造一个临时对象作为返回值,返回值也就同样不能是引用。

3.4、输入输出操作符重载

支持I/O操作的类所提供的I/O操作接口,一般应该与标准库iostream为内置类型定义的接口相同。

1、输出操作符 << 的重载

为了与I/O标准库一致,操作符应接受 ostream& 作为第一个形参,对类类型 const 对象的引用作为第二个形参,并返回对 ostream 形参的引用。

重载输出操作符可能相对于比较难理解,这里简单的说下,我们只能以自定义类的友元函数的形式重载这两个运算符,这是因为如果我们用成员函数的形式来重载的话,就要改动系统的流类 istream 和 ostream 定义,这是C++不允许的,如果不定义为友元函数的话,将无法调用类对象成员数据输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
ostream& operator<<(ostream& stream, const Zoo &object)



{



//对object所引用的对象的数据进行的输出操作



stream << object.lion;



stream << object.tiger;







return stream;



}

我们来看看上面这个输出操作符重载函数,第一个参数是 ostream 类的引用,而函数的返回值也是 ostream 类的引用。毫无疑问,我们调用这个运算符重载函数时。实参肯定是 cout,这样就造成了这样一种情况:实参 stream 引用 cout,而函数的返回值又引用 stream,等于函数返回值引用的实体还是 cout。这样做的目的是实现了连续的输出操作。当执行下面语句:

1
2
3
4
5
6
7
8
9
cout << zoo_a << zoo_b;



//上面 cout << zoo_a 实质就是调用 operator<<(cout, zoo_a),然后返回 cout



//下一个 << 就相当于执行 cout << zoo_b, 同上

我们不能将该操作符重载函数定义为类的成员函数,否则,左操作数将只能是该类类型的对象。IO操作符通常要对非公用数据成员进行读写,因此,类通常将IO操作符(输入输出)设为友元。

2、输入操作符 >> 的重载

为了与IO标准库一致,操作符应接受 istream& 作为第一个形参,指向它要读的流,并且返回的也是对同一个流的引用(链式操作)。它的第二个形参是对要读入的对象的非 const 引用,该形参必须为非 const,因为输入操作符的目的是将数据读到这个对象中。

更重要但通常重视不够的是,输入和输出操作符有如下区别:输入操作符必须处理错误和文件结束的可能性。

输入期间的错误:任何读操作都可能因为提供的值不正确而失败;任何读入都可能碰到输入流中的文件结束或其他一些错误。也就需要对输入进行附加检查,发现有这些错误就需要我们进行处理。

3.5、不能重载的运算符 &&、|| 和 , 操作符

和 C 一样,C++ 对于真假值表达式采用所谓骤死式评估方式。意思是一旦该表达式的真假值去顶,纵使表达式中还有部分尚未检验,整个评估工作仍告结束。比如下面这种情况:

1
2
3
4
5
6
7
8
9
char *p;



……



if ((p != NULL) && (strlen(p) > 10) ……

你无需担心调用 strlen 时 p 是否为 NULL 指针,因为如果 p 是否为NULL 的测试结果是否定的,strlen 就绝不会被调用。事实上,对一个 NULL 指针调用 strlen,结果未可预期。

回到重载,C++ 允许我们为用户定制型别量身定做各类操作符,包括 && 和 ||,操作符重载语义上是允许的,但是我们要考虑重载会不会改变对应内置操作符的规则。拿 && 和 || 来说,重载则是对 operator && 和 operator || 两函数进行重载工作,值得注意的是,函数调用语义将会取代骤死式语义,也就是说,如果你将operator && 重载,下面这个虱子:

1
if (expression1 && expression2) ……

会被编译器视为以下两者之一:

1
2
3
4
5
6
7
8
9
10
11
12
13
if (expression1.operator&&(expression2)) ……



//假设 operator&& 是个 member function



if (operator&&(expression1, expression2)) ……



//假设 operator&& 是个全局函数

上面函数调用语义和所谓骤死式语义有两个重大的不同。第一,当函数调用动作被执行起来,所有参数值都必须评估完成,所以当我们调用 operator&& 和 operator|| 时,两个参数都已评估完成,没有什么骤死式语义。第二,C++ 语言规格并未明定函数调用动作中各参数的评估次序,所以没办法知道 expression1 和 expression2 哪个会先被评估,而内置的真假值表达式,则总是由左向右评估其自变量。

C++ 中,运算符重载的一个重要参考就是:不能修改运算符的内置语义。