Thinking in C++ ----第12章 运算符重载
运算符重载(operator overloading)是一种语法糖(syntactic sugar),是函数调用的另一种方式,合理地使用它,能使我们的代码更加地易写、易读。
重载运算符时的注意事项
1. 不能重载C/C++中没有的运算符,如使用(**)来代表幂运算符;
2. 不能改变运算符的优先级;
3. 不能改变运算符的操作数个数
4. 不可重载的运算符
1) 成员选择运算符operator.。点在类中对任何成员都有一定的意义。
如果允许它重载,就不能用普通的方法访问成员,
只能用指针和operator->进行访问。
2) 成员指针间接引用operator.*。原因同上
如果没有这些限制,那么我们重载运算符的语义就有可能不清楚。
成员 vs 非成员
操作符可以作为成员函数或非成员函数进行重载。下面是一些建议
1. 所有的一元运算符 建议使用 成员函数
2. = () [] -> ->* 建议使用 必须是成员
3. += -= /= *= ^= &= |= %= >>= <<= 建议使用 成员
4. 所有其他二元运算符 建议使用 非成员
自增和自减运算符的重载
自增(++)和自减(--)有前缀(prefix)版本和后缀(suffix)版本,编译器通过调用不同的函数来区分这两个版本。下面将自增和自减运算符作为成员函数进行重载:
2
3 using namespace std;
4
5 class Integer{
6 private:
7 int m_i;
8 public:
9 Integer(int i = 0):m_i(i){}
10
11 // 前缀版本,返回引用类型
12 Integer &operator++(){
13 ++m_i;
14 return *this;
15 }
16
17 // 后缀版本,返回值类型,这个参数必须是int类型的
18 const Integer operator++(const int){
19 int temp = m_i;
20 m_i++;
21 return Integer(temp);
22 }
23
24 friend ostream &operator<<(ostream &os, const Integer &integer){
25 os << integer.m_i;
26 return os;
27 }
28 };
29
31 int main(){
32 Integer integer; // 默认值为0
33 cout << ++integer << endl; // 输出1
34 cout << integer++ << endl; // 输出1
35 cout << integer <<endl; // 输出2
36 }
如上代码示例,当编译前缀版本的自增运算符时会调用Integer::operator++();当编译器看到后缀版本的自增运算符时会调用Integer::operator++(const int)。
若要将自增和自减重载为非成员函数,如下所示:
2 private:
3 int m_i;
4 public:
5 Integer(int i = 0):m_i(i){}
6
7 // 前缀版本,返回引用类型
8 friend Integer &operator++(Integer &integer){
9 ++integer.m_i;
10 return integer;
11 }
12
13 //后缀版本,返回值类型
14 friend const Integer operator++(Integer &integer, const int){
15 int temp = integer.m_i;
16 integer.m_i++;
17 return Integer(temp);
18 }
19
20 friend ostream &operator<<(ostream &os, const Integer &integer){
21 os << integer.m_i;
22 return os;
23 }
24 };
最后有两点需要注意:
1. 临时对象
后缀版本的最后一行为return Integer(temp);
这是创建一个临时对象的语法,意思是创建一个临时Integer对象并返回它。这样做避免创建不必要的中间对象,对效率有所提升。请分析如下语句:
2 return itmp;
上面两行所完成工作的最后效果同return Integer(temp);是一样的。执行这两行将会发生以下三件事:
1. 创建itmp对象,调用其构造函数;
2. 使用拷贝构造函数将itmp拷贝到外部返回值的存储单元中;
3. 函数返回时,调用itmp的析构函数。
但是,当我们使用return Integer(temp);时,它的行为如下:
当编译器看到我们返回一个临时对象时,直接把这个对象创建在外部返回值的内存单元中,并调用它的构造函数。因此编译器不必调用拷贝构造函数,也不必在函数返回时调用它的析构函数(因为我们没有创建一个局部对象)。
这种方式通常被称为返回值优化(return value optimization)。
2. 后缀版本返回的是一个常量
后缀版本的自增运算符最后返回了一个临时对象,在它上边所作的修改都是无意义的,因此将它的返回值设为const,使编译器来阻止试图改变临时变量的行为。
operator=
在不同的上下文中,赋值运算符的含义也会有所不同,请看如下代码:
2 MyType a = b; // 调用拷贝构造函数
3 a = b; // 调用operator=运算符
也就是说:当编译器看到赋值运算符时,如果对象还没有创建,那么调用的是拷贝构造函数;如果对象已经被创建,则调用operator=。
另外,再一次强调:只能将operator=重载为成员函数
避免自赋值(self-assignment)
自赋值就是说一个对象自己给自己赋值。对于拷贝构造函数或者operator=,应该先判断是否是自赋值,代码可能是下面这样子。
2 if (this == &obj)
3 return *this;
4 // do something else
5 }
《Effective C++》中有这样一个例子,说明了自赋值可能会产生的问题,如下:
2
3 class Widget{
4 …
5 private:
6 Bitmap *bp;
7 };
下面是这个类的operator=操作符:
2 delete pb;
3 pb = new Bitmap(*rhs.pb);
4 return *this;
5 }
在这里,如果rhs和*this是同一个对象的话,那么delete pb;这一行会将自己的bp释放掉,同时也释放掉了rhs.bp,因此pb = new Bitmap(*rhs.pb)这一行就可能产生问题。
自动创建的operator=
因为将一个对象赋给另一个相同类型的对象是大多数人可能做的事情,所以如果没有创建type::operator=(type),编译器将自动创建一个。自动创建的operator=版本的行为模仿自动创建的拷贝构造函数的行为:如果类包含对象,对于这些对象,operator=被递归调用。
以后要看的
1. 自动类型转换
2. 智能指针
3. 自动类型转换
4. 异常安全
5. 引用计数和写时拷贝