21-8 重载增量和减量运算符

重载递增(++)和递减(--)运算符相当简单,只有一个小例外。递增和递减运算符实际上存在两种形式:前缀递增/递减(例如 ++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++()
{
    // If our number is already at 9, wrap around to 0
    if (m_digit == 9)
        m_digit = 0;
    // otherwise just increment to next number
    else
        ++m_digit;

    return *this;
}

Digit& Digit::operator--()
{
    // If our number is already at 0, wrap around to 9
    if (m_digit == 0)
        m_digit = 9;
    // otherwise just decrement to next number
    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之间的数字。我们重载了递增和递减操作,使其能递增/递减该数字,当递增/递减超出范围时会进行循环。

此示例输出:

image

请注意我们返回的是*this。重载的递增和递减运算符返回当前隐式对象,因此多个运算符可以“链式”组合使用。

后缀增量与减量运算符的重载

通常,函数在名称相同但参数数量和/或类型不同时可进行重载。但前缀与后缀增减运算符的情况特殊:它们名称相同(如 operator++)、均为一元运算符,且接受相同类型的单个参数。那么重载时如何区分二者?

C++语言规范通过特殊规则给出了解答:编译器会检查重载运算符是否包含int参数。若包含int参数,则该运算符为后缀重载;若无参数,则为前缀重载。

以下是包含前后缀重载的前缀和后缀运算符的Digit类示例:

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

    Digit& operator++(); // prefix has no parameter
    Digit& operator--(); // prefix has no parameter

    Digit operator++(int); // postfix has an int parameter
    Digit operator--(int); // postfix has an int parameter

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

// No parameter means this is prefix operator++
Digit& Digit::operator++()
{
    // If our number is already at 9, wrap around to 0
    if (m_digit == 9)
        m_digit = 0;
    // otherwise just increment to next number
    else
        ++m_digit;

    return *this;
}

// No parameter means this is prefix operator--
Digit& Digit::operator--()
{
    // If our number is already at 0, wrap around to 9
    if (m_digit == 0)
        m_digit = 9;
    // otherwise just decrement to next number
    else
        --m_digit;

    return *this;
}

// int parameter means this is postfix operator++
Digit Digit::operator++(int)
{
    // Create a temporary variable with our current digit
    Digit temp{*this};

    // Use prefix operator to increment this digit
    ++(*this); // apply operator

    // return temporary result
    return temp; // return saved state
}

// int parameter means this is postfix operator--
Digit Digit::operator--(int)
{
    // Create a temporary variable with our current digit
    Digit temp{*this};

    // Use prefix operator to decrement this digit
    --(*this); // apply operator

    // return temporary result
    return temp; // return saved state
}

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; // calls Digit::operator++();
    std::cout << digit++; // calls Digit::operator++(int);
    std::cout << digit;
    std::cout << --digit; // calls Digit::operator--();
    std::cout << digit--; // calls Digit::operator--(int);
    std::cout << digit;

    return 0;
}

这将打印

image

这里有几个值得注意的细节。首先,我们通过在后缀运算符版本中添加整型占位参数来区分前后缀运算符。其次,由于该占位参数在函数实现中并未被使用,我们甚至未给它命名。这告知编译器将该变量视为占位符,因此不会因变量声明后未使用而发出警告。

第三,请注意前缀和后缀运算符功能相同——均用于递增或递减对象值。二者的区别在于返回值:重载的前缀运算符在递增/递减后直接返回对象本身。因此重载逻辑相当简单:只需递增/递减成员变量,再返回*this即可。

后缀运算符则需要返回增减操作前的对象状态。这便形成了一个悖论:若执行增减操作,便无法返回操作前的对象状态。反之,若先返回状态再执行增减操作,增减操作将永远不会被调用。

解决此问题的典型方案是使用临时变量保存增减前的对象值,随后对对象本体执行增减操作。最后将临时变量返回给调用方。如此既使调用方获得增减前的对象副本,又确保对象本身完成增减操作。需注意:这意味着重载运算符的返回值必须是非引用类型,因为无法返回局部变量引用——该变量在函数退出时将被销毁。另需注意,后置运算符通常比前置运算符效率更低,因为它增加了实例化临时变量及按值返回(而非按引用返回)的额外开销。

最后需说明,我们通过调用前置增减运算符来完成后置增减运算的大部分工作,这种设计既减少了代码重复,也使类在未来更易于修改。

posted @ 2026-01-24 11:29  游翔  阅读(0)  评论(0)    收藏  举报