运算符重载介绍

C++ 允许将标准的运算符用于类对象,例如 +、= 等。运算符重载是一种形式的 C++ 多态,能够使对象操作更美观。运算符重载其实是一种特殊的成员函数。
什么是多态:字面意思就是一种事物有多种形态。对于 C++ 来说,就是指调用函数时,会根据参数的不同来执行不同的函数。其中函数重载就是一种常见的多态,另外一种常见的多态会在继承相关章节的知识中介绍。
实际上,很多 C++ 运算符已经被重载了,例如,将 * 运算符,如果它的运算对象为地址,将得到存储在这个地址中的值;但如果它的运算对象为两个数字时,得到的将是它们的乘积。C++ 允许将运算符重载扩展到用户自定义类型,例如,允许使用 + 将两个对象相加。重载运算符可使得代码看起来更自然。例如,将两个数组相加会是一种常见的运算,通常,需要使用 for 循环来实现。

  1. for (int i = 0; i < 20; i++)
  2. c[i] = a[i] + b[i];

但在 C++ 中可以定义一个表示数组的类,并重载 + 运算符,于是就可以用这样的语句:

  1. c = a + b;

要重载运算符,就需要使用被称为运算符函数的特殊函数形式,格式如下:

  1. operatorop(arg_list)

其中,op 为需要重载运算符,例如,operator+() 重载+运算符。op 必须是有效的 C++ 运算符,不能虚构一个新的符号。
例如,假设有一个 Demo 类,并为它定义了一个 operator+() 的成员函数,以重载 + 运算符,以便能够将两个 Demo 对象相加,如果 d1、d2、d 都是 Demo 类的对象,则可以编写这样的等式:

  1. d = d1 + d2;

编译器发现操作数是 Demo 类的对象,因此在使用 + 运算符时会用相应的 operator+() 替换 + 运算符,即上述代码和一下代码等价:

  1. d = d1.operator+(d2);

运算符重载示例

定义一个 Time 类,该类包含 hours 和 minutes 两个数据,假设有一个 Time 对象 t1 表示 2 小时 25 分钟,另一个 Time 对象 t2 表示 2 小时 40 分钟,想要将两个时间相加,但相加的两个对象是 Time 对象,与内置类型不匹配,不能直接使用 + 运算符,接下来的演示将先采用之前学过的成员函数来实现相加操作,之后再演示一下运算符重载的代码,对比感受一下运算符重载的效果。

成员函数示例

  1. #ifndef MYTIME1_H_
  2. #define MYTIME1_H_
  3. class Time {
  4. private:
  5. int hours;
  6. int minutes;
  7. public:
  8. Time();
  9. Time(int hours, int minutes);
  10. ~Time();
  11. void show() const;
  12. void modify(int hours, int minutes);
  13. void addHours(int hours);
  14. void addMinutes(int minutes);
  15. Time sum(const Time & t) const;
  16. };
  17. #endif // MYTIME1_H_
  1. #include "mytime1.h"
  2. #include <iostream>
  3. Time::Time() {
  4. this->hours = this->minutes = 0;
  5. }
  6. Time::Time(int hours, int minutes) {
  7. std::cout << "Time Constructor, hours = " << hours << " minutes = " << minutes << std::endl;
  8. if (hours < 0 || hours > 24) {
  9. std::cout << "Hour out of range. It's set 0" << std::endl;
  10. hours = 0;
  11. }
  12. if (minutes < 0 || minutes > 60) {
  13. std::cout << "Hour out of range." << std::endl;
  14. minutes = 0;
  15. }
  16. std::cout << "Minute out of range." << std::endl;
  17. this->hours = hours;
  18. this->minutes = minutes;
  19. }
  20. Time::~Time() {
  21. std::cout << "Destructor." << std::endl;
  22. }
  23. void Time::addHours(int h) {
  24. this->hours = (this->hours + h) % 24;
  25. }
  26. void Time::addMinutes(int m) {
  27. this->minutes = this->minutes + m;
  28. if (this -> minutes > 60) {
  29. this->hours++;
  30. this->hours %= 24;
  31. this->minutes %= 60;
  32. }
  33. }
  34. void Time::show() const {
  35. std::cout << this->hours << " hours, " << this->minutes << " minutes." << std::endl;
  36. }
  37. void Time::modify(int h, int m) {
  38. if (h < 0 || h > 24) {
  39. std::cout << "Hour out of range. Don't modify." << std::endl;
  40. return;
  41. }
  42. if (m < 0 || m > 60) {
  43. std::cout << "Minute out of range. Don't modify." << std::endl;
  44. return;
  45. }
  46. this->hours = h;
  47. this->minutes = m;
  48. }
  49. Time Time::sum(const Time &t) const {
  50. Time tmp;
  51. tmp.minutes = t.minutes + minutes;
  52. tmp.hours = (t.hours + hours + tmp.minutes / 60) % 24;
  53. tmp.minutes %= 60;
  54. return tmp;
  55. }

来看一下 sum() 函数代码。需要注意,参数中使用了引用,但返回类型却不是引用。
参数中使用引用是为了提高效率,也可以按值传递 Time 对象也可以实现相同的功能,但比起按引用传递,效率低,使用的内存也多。
然而,返回值不能是引用。因为 sum() 函数中将创建一个新的 Time 对象(tmp),来保存另外两个 Time 对象的和。如果返回类型是 Time &,则返回的是 tmp 对象的引用,但 tmp 对象是局部变量,在函数结束时将被删除,因此引用将指向不存在的对象。而返回类型为 Time 意味着程序在删除临时对象 tmp 之前构造它的拷贝,调用函数将返回该拷贝。

警告:不要返回指向局部变量或临时对象的引用。函数执行完毕之后,局部变量和临时对象将消失,引用将指向不存在的数据。

// 对Time类成员函数的测试
#include "mytime1.h"
#include <iostream>
int main(int argc, char ** args) {
    Time t1;
    Time t2(2, 40);
    Time t3 = Time(5, 55);

    std::cout << "t1 = ";
    t1.show();
    std::cout << "t2 = ";
    t2.show();
    std::cout << "t3 = ";
    t3.show();

    t1 = t2.sum(t3);
    std::cout << "t1 + t2 = ";
    t1.show();
    return 0;
}
Default Constructor.
Time Constructor, hours = 2 minutes = 40
Time Constructor, hours = 5 minutes = 55
t1 = 0 hours, 0 minutes.
t2 = 2 hours, 40 minutes.
t3 = 5 hours, 55 minutes.
Default Constructor.
Destructor.
t1 + t2 = 8 hours, 35 minutes.
Destructor.
Destructor.
Destructor.

运算符重载示例

将 Time 类转换成重载的加法运算符很容易,只要将成员函数 sum() 的名称改为 operator+() 即可。

#ifndef MYTIME2_H_
#define MYTIME2_H_

class Time {
private:
    int hours;
    int minutes;
public:
    Time();
    Time(int, int);
    ~Time();
    void addHour(int);
    void addMinute(int);
    void show() const;
    void modify(int, int);
    Time operator+(const Time &) const;
};
#endif // MYTIME2_H_
Time Time::operator+(const Time & t) const {
    Time sum;
    sum.minutes = this->minutes + t.minutes;
    sum.hours = (this->hours + t.hours + sum.minutes / 60) % 24;
    sum.minutes %= 60;
    return sum;
}

和 sum() 一样,operator+() 也是由 Time 对象调用的,它将第二个 Time 对象作为参数,并返回一个 Time 对象。因此,可以像调用 sum() 那样调用 operator+() 方法。

t1 = t2.operator+(t3);

但是在该方法命名为 operator+() 之后,也可以使用运算符表示法:

t1 = t2 + t3;

这两种表示法都将调用 operator+() 函数。在运算符表示法中,运算符左侧的对象是调用对象,运算符右侧的对象是作为参数被传递的对象。

#include "mytime2.h"
#include <iostream>
int main(int argc, char ** args) {
    Time t1 = Time(2, 40);
    Time t2 = Time(5, 55);
    Time t3 = Time(3, 28);

    Time t4 = t1 + t2;
    Time t5 = t1.operator+(t3);
    std::cout << "t4 = ";
    t4.show();
    std::cout << "t5 = ";
    t5.show();
    return 0;
}
Time Constructor, hours = 2 minutes = 40
Time Constructor, hours = 5 minutes = 55
Time Constructor, hours = 3 minutes = 28
Time Default Constructor
Time Default Constructor
t4 = 8 hours, 35 minutes.
t5 = 6 hours, 8 minutes.
Destructor
Destructor
Destructor
Destructor
Destructor

总之,operator+() 函数可以使用函数表示法或者运算符表示法来调用它。

疑问

对于两个对象以上的加法,可以像基本运算符那样连加吗?

t4 = t1 + t2 + t3; // 这是有效的吗?

答:可以的。

我们来看一下上述语句将如何转换为函数调用。
《C++ Primer Plus》第387页:由于 + 运算符是从左向右结合运算符,因此上述语句将首先被转换成下面这样:

t4 = t1.operator+(t2 + t3);

疑问一:从理论上来说,+ 运算符是从左向右的结合性,因此不应该是先算 t1 + t2,然后将得到的表示 t1 + t2 的临时对象与 t3 去相加吗?为什么先计算 t2 + t3?
疑问二:根据代码实际测试,t1 + t2 + t3 是先计算 t1 + t2 的值。不确定是书上写错了,还是 C++11 对其有所修改。

运算符重载的限制

多数 C++ 运算符都可以用这样的方式重载。下面介绍 C++ 对用户自定义的运算符重载的限制:

  1. 重载后的运算符必须至少有一个操作数是用户自定义的类型。这是为了防止用户为标准类型重载运算符,影响标准类型的使用。
  2. 使用运算符时不能违反运算符原来的句法规则。例如,% 运算符的操作数为两个,不能将 % 运算符重载为使用一个操作数的运算符。
  3. 不能创建新的运算符
  4. 不能重载的运算符:sizeof 运算符、成员运算符 .、成员指针运算符 .*、作用域解析符 ::、条件运算符 ?:、const_cast、dynamic_cast、static_cast、reinterpret_cast、typeid。
  5. 大部分可重载的运算符都不必是成员函数,还可以定义为非成员函数,一般来说,非成员函数应是友元函数,这样它才能直接访问类的私有数据。但有四个运算符必须作为成员函数来重载:赋值运算符 =、函数调用运算符 ()、下标运算符 []、指针访问类成员的运算符 ->。

    总结

  6. C++ 允许用户重载运算符,重载后的运算符必须至少有一个操作数是用户自定义的类型,并且不能修改运算符的操作数。

  7. 使用 operatorop 来定义重载运算符,其中 [op] 使用需要被重载的运算符来代替。
  8. 不能被重载的运算符:sizeof . :: ?: .*。
  9. 只能作为成员函数来重载的运算符:= () [] ->。
  10. 重载运算符可以作为成员函数,也可以作为非成员函数。作为成员函数时,参数数目比运算符操作数少一个,因为会将 *this 作为隐式参数;作为非成员函数时,参数数目等于操作数数目,并且第一个参数就是最左侧的操作数。通常作为非成员函数时,应声明为友元函数,这样可以直接访问类的私有成员。关于友元函数下一章讲。