重载自增与自减运算符

重载自增(++)与自减(–)运算符总体上较为简单,仅有一处细微差异:这两种运算符实际各有两个版本——前缀自增/自减(例如 ++x; –y;)与后缀自增/自减(例如 x++; y–;)。

由于自增与自减运算符均为单目运算符,并会修改其操作数,因此最宜将其重载为成员函数。我们先处理前缀版本,因为它们最为直观。

重载前缀自增与自减

前缀自增与自减的重载方式与普通单目运算符完全一致。以下通过示例说明:

#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 的数字。我们重载的自增与自减运算符会在数字越界时回绕。

程序输出

89098

注意我们返回了 *this。重载后的自增与自减运算符返回当前隐式对象,从而使多个运算符可以“链式”调用。

重载后缀自增与自减

通常情况下,函数可因其参数数量或类型不同而进行重载。然而,前缀与后缀自增/自减运算符同名(例如 operator++),均为单目,且参数类型相同。那么重载时如何区分?

C++ 语言规范对此做出特殊规定:编译器检查重载运算符是否带有一个 int 形参。若带 int 形参,则为后缀重载;若无,则为前缀重载。

以下给出同时包含前缀与后缀重载的 Digit 类:

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);
};

// 无参数,故为前缀 operator++
Digit& Digit::operator++()
{
    if (m_digit == 9)
        m_digit = 0;
    else
        ++m_digit;

    return *this;
}

// 无参数,故为前缀 operator--
Digit& Digit::operator--()
{
    if (m_digit == 0)
        m_digit = 9;
    else
        --m_digit;

    return *this;
}

// 带 int 参数,故为后缀 operator++
Digit Digit::operator++(int)
{
    // 创建临时变量保存当前数字
    Digit temp{ *this };

    // 使用前缀运算符递增
    ++(*this); // 应用 operator++

    // 返回临时结果
    return temp; // 返回保存的状态
}

// 带 int 参数,故为后缀 operator--
Digit Digit::operator--(int)
{
    // 创建临时变量保存当前数字
    Digit temp{ *this };

    // 使用前缀运算符递减
    --(*this); // 应用 operator--

    // 返回临时结果
    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;
}

程序输出

5667665

值得注意的几点:

  1. 通过在后缀版本中添加一个整型哑元参数来区分前缀与后缀运算符。
  2. 该哑元参数在函数实现中未被使用,因此未命名,这向编译器表明该变量仅为占位符,不会产生“声明但未使用”的警告。
  3. 前缀与后缀运算符完成的任务相同,均对对象进行自增或自减,区别在于返回值:
    • 前缀版本返回自增/自减后的对象,因此实现简单:直接修改成员变量后返回 *this
    • 后缀版本需返回操作前的对象状态,于是产生矛盾:若先自增/自减,则无法返回原状态;若先返回原状态,则不会执行自增/自减。

常见解决方式是使用临时变量保存操作前的对象值,然后对对象本身进行自增/自减,最后返回该临时变量。如此,调用者得到自增/自减前的副本,而实际对象已完成修改。由于返回的是局部变量,重载运算符的返回类型必须为非引用类型;此外,由于需创建临时变量并以值返回,后缀版本通常比前缀版本效率低。

最后,我们将后缀自增与自减实现为调用前缀版本以完成主要工作,从而减少代码重复并降低未来维护成本。

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"