章节目录

重载递增和递减运算符

本节阅读量:

重载increment(++)和decrement(–)运算符非常简单,只有一个小例外。实际上,递增和递减运算符各有两个版本:前缀递增和递减(例如++x;–y;),以及后缀递增和递减(例如x++; y–;)。

由于递增和递减运算符都是一元运算符,并且会修改其操作数,因此最好将它们重载为成员函数。我们先处理前缀版本,因为它们最简单。


重载前缀递增和递减

重载前缀递增和递减运算符,与重载普通一元运算符完全相同。下面通过示例进行演示:

 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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <iostream>

class Digit
{
private:
    int m_digit{};
public:
    Digit(int digit=0)
        : m_digit{digit}
    {
    }

    Digit& operator++();
    Digit& operator--();

    friend std::ostream& operator<< (std::ostream& out, const Digit& d);
};

Digit& Digit::operator++()
{
    // 如果数字是 9, 回绕到 0
    if (m_digit == 9)
        m_digit = 0;
    // 否则增加到下一个
    else
        ++m_digit;

    return *this;
}

Digit& Digit::operator--()
{
    // 如果数字是 0, 回绕到 9
    if (m_digit == 0)
        m_digit = 9;
    // 否则减少到下一个
    else
        --m_digit;

    return *this;
}

std::ostream& operator<< (std::ostream& out, const Digit& d)
{
	out << d.m_digit;
	return out;
}

int main()
{
    Digit digit { 8 };

    std::cout << digit;
    std::cout << ++digit;
    std::cout << ++digit;
    std::cout << --digit;
    std::cout << --digit;

    return 0;
}

我们的Digit类处理0到9之间的数字。我们重载了递增和递减运算符,让它们对数字进行递增或递减;如果结果超出范围,就进行环绕。

此示例打印:

1
89098

请注意,我们返回了*this。重载的递增和递减运算符返回当前隐式对象,因此可以将多个运算符“链接”在一起。


重载后缀递增和递减

通常,当函数名称相同但参数数量和/或参数类型不同时,可以对它们进行重载。然而,前缀和后缀递增/递减运算符比较特殊。两者名称相同(例如operator++),都是一元运算符,并且作用于同一类型的操作数。那么,重载时如何区分它们呢?

C++语言规范提供了一个特例来处理这种情况:编译器会查看重载运算符是否具有int参数。如果重载运算符具有int参数,则该运算符是后缀重载;如果没有参数,则是前缀重载。

下面是上面的Digit类,其中包含前缀和后缀重载:

 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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
class Digit
{
private:
    int m_digit{};
public:
    Digit(int digit=0)
        : m_digit{digit}
    {
    }

    Digit& operator++(); // 无参数,前缀
    Digit& operator--(); // 无参数,前缀

    Digit operator++(int); // 有int参数,后缀
    Digit operator--(int); // 有int参数,后缀

    friend std::ostream& operator<< (std::ostream& out, const Digit& d);
};

// 无参数,前缀
Digit& Digit::operator++()
{
    // 如果数字是 9, 回绕到 0
    if (m_digit == 9)
        m_digit = 0;
    // 否则增加到下一个
    else
        ++m_digit;

    return *this;
}

// 无参数,前缀
Digit& Digit::operator--()
{
    // 如果数字是 0, 回绕到 9
    if (m_digit == 0)
        m_digit = 9;
    // 否则减少到下一个
    else
        --m_digit;

    return *this;
}

// 有int参数,后缀
Digit Digit::operator++(int)
{
    // 使用当前值创建一个临时变量
    Digit temp{*this};

    // 使用前缀版本去递增一下
    ++(*this); // 调用对应的运算符

    // 返回临时对象
    return temp; // 返回保存的状态
}

// 有int参数,后缀
Digit Digit::operator--(int)
{
    // 使用当前值创建一个临时变量
    Digit temp{*this};

    // 使用前缀版本去递减一下
    --(*this); // 调用对应的运算符

    // 返回临时对象
    return temp; // 返回保存的状态
}

std::ostream& operator<< (std::ostream& out, const Digit& d)
{
	out << d.m_digit;
	return out;
}

int main()
{
    Digit digit { 5 };

    std::cout << digit;
    std::cout << ++digit; // 调用 Digit::operator++();
    std::cout << digit++; // 调用 Digit::operator++(int);
    std::cout << digit;
    std::cout << --digit; // 调用 Digit::operator--();
    std::cout << digit--; // 调用 Digit::operator--(int);
    std::cout << digit;

    return 0;
}

结果打印:

1
5667665

这里发生了一些有趣的事情。首先,请注意,前缀和后缀运算符是通过后缀版本中的整数参数来区分的。其次,因为函数实现中没有使用这个参数,所以我们甚至没有给它命名。这告诉编译器把该参数视为占位符,因此它不会警告我们声明了一个变量却从未使用。

第三,注意前缀和后缀运算符执行相同的工作:它们都会增加或减少对象。两者的区别在于返回值。重载的前缀运算符会在对象递增或递减后返回该对象。这相当简单,只需增加或减少成员变量,然后返回*this。

另一方面,后缀运算符需要返回对象递增或递减之前的状态。这带来了一个难题:如果先增加或减少对象,就无法返回修改前的状态;如果在递增或递减之前直接返回对象状态,则递增或递减永远不会发生。

解决这个问题的典型方法是使用临时变量,在对象的值递增或递减之前保存其当前状态。然后,对象本身执行递增或递减。最后,将临时变量返回给调用者。这样,调用者会收到对象修改前的副本,而对象本身已经被修改。注意,这意味着重载运算符的返回值必须是非引用,因为我们不能返回局部变量的引用,局部变量会在函数退出时被销毁。还要注意,后缀运算符通常不如前缀运算符性能好,因为创建临时变量并按值返回会带来额外开销。

最后,请注意,我们让后缀版本调用前缀版本来完成大部分工作。这样可以减少重复代码,并让类将来更容易修改。


21.6 重载比较运算符

上一节

21.8 重载下标运算符

下一节