重载自增(++)与自减(–)运算符总体上较为简单,仅有一处细微差异:这两种运算符实际各有两个版本——前缀自增/自减(例如 ++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
值得注意的几点:
- 通过在后缀版本中添加一个整型哑元参数来区分前缀与后缀运算符。
- 该哑元参数在函数实现中未被使用,因此未命名,这向编译器表明该变量仅为占位符,不会产生“声明但未使用”的警告。
- 前缀与后缀运算符完成的任务相同,均对对象进行自增或自减,区别在于返回值:
- 前缀版本返回自增/自减后的对象,因此实现简单:直接修改成员变量后返回
*this
。 - 后缀版本需返回操作前的对象状态,于是产生矛盾:若先自增/自减,则无法返回原状态;若先返回原状态,则不会执行自增/自减。
- 前缀版本返回自增/自减后的对象,因此实现简单:直接修改成员变量后返回
常见解决方式是使用临时变量保存操作前的对象值,然后对对象本身进行自增/自减,最后返回该临时变量。如此,调用者得到自增/自减前的副本,而实际对象已完成修改。由于返回的是局部变量,重载运算符的返回类型必须为非引用类型;此外,由于需创建临时变量并以值返回,后缀版本通常比前缀版本效率低。
最后,我们将后缀自增与自减实现为调用前缀版本以完成主要工作,从而减少代码重复并降低未来维护成本。