一. 输入和输出运算符
1 | class A { |
1. 输出运算符
第一个形参使用非常量的ostream
对象的引用,第二个形参使用常量的引用。
ostream
非常量是因为向流写入内容会改变其状态;第二个形参使用常量是为了避免修改对象内容,引用是为了避免调用复制构造函数。
通常, 输出运算符应该主要负责打印对象的内容而非控制格式,输出运算符不应该打印换行符。
2. 输入运算符
第一个形参使用非常量的istream
对象的引用,第二个形参使用非常量的引用。
istream
非常量是因为向流写入内容会改变其状态;第二个形参使用非常量引用是因为输入运算符本来就是要把数据读入到这个对象中。
为什么输入和输出运算符的重载是友元函数?
首先不能是类的成员函数,否则调用格式就变成了A a; a << ...
,另外我们也不能给iostream
标准库的类中添加成员,最后为了访问类中的私有成员,所以重载为友元。
所以实际上的调用过程可以理解为<< (cout, a)
。
二. 算数和关系运算符
定义为成员函数还是友元函数?
最好定义为友元函数。
首先,定义为成员函数是可行的,比如如下定义:
1 | class A { |
上述代码的返回值类型是对象而不是引用,原因是如果返回的是引用,则sum
对象在函数返回之后会被销毁,从而导致引用失效。
另一个问题是如果没有新建一个对象,而直接使用val += a.val
,则调用函数的对象的值也被修改了。
那么为什么最好定义为友元函数?
1 | string s = "world"; |
如果
operator +
是string 类的成员,则上面的第一个加法等价于s.operator+( " ! " )
。
同样地,"hi “ +s 等价于"hi ". operator+(s)
。显然”hi “的类型是const char* ,这是一种内置类型,根本就没有成员函数。因为string 将
+
定义成了普通的非成员函数,所以"hi " + s
等价于operator+("hi ", s)
。和任何其他函数调用一样,每个实参都能被转换成形参类型。唯一的要求是至少有一个运算对象是类类型,并且两个运算对象都能准确无误地转换成string 。
类型转换函数可以被显式定义,如operator int() const { return val; }
。
编译器隐式调用类型转换函数或者构造函数进行类型转换可能出现问题,因此为了避免编译器隐式进行类型转换,可以使用explicit
字段修饰类型转换函数和构造函数,编译器通常不会使用显式的类型转换运算符用作隐式类型转换。
该规定存在一个例外,即如果表达式被用作条件,则编译器会将显式的类型转换自动应用于它。换句话说, 当表达式出现在下列位置时, 显式的类型转换将被隐式地执行:
- if , while 及 do 语句的条件部分
- for 语句头的条件表达式
- 逻辑非运算符(!)、逻辑或运算符(||)、逻辑与运算符(&&)的运算对象
- 条件运算符(? : )的条件表达式。
1. 算术运算符
1 | class A { |
返回值定义为const
类型可以避免对函数进行操作,即a + b = c
;同时也不会对c = a + b
造成影响,c
并不要求一定是const
类型。
2. 关系运算符
1 | class A { |
三. 赋值运算符
1 | class A { |
a = b = c
首先执行b = c
,然后执行a = b
。
返回引用的目的就是为了在连续赋值时,完成b = c
之后返回一个b
的引用,让a
能够被b
赋值。
如果是(a = b) = c
,则是a
首先被赋予b
的值,其次a
又被赋予了c
的值。而如果返回的是const
引用,则不能这样复制,因为无法给const
引用赋值。
四. 递增和递减运算符
1. 前置递增/递减运算符
1 | class A { |
2. 后置递增/递减运算符
为了区分前置和后置运算符,后置版本接受一个额外的不被使用的int类型的形参,作为函数重载的区分。
1 | class A { |
与前置递增/递减运算符不同的是,后置的运算符返回的是对象而不是引用,因为后置运算符返回的是在函数内新定义的对象,离开函数时会被析构,所以返回值类型是对象,从而调用复制构造函数生成一个新的对象。
如何显式调用前置/后置递增/递减运算符?
1 | A a; |
部分内容摘自《C++ Primer (第5版)》