C++ 中最常用的运算符之一便是算术运算符——即加号(+)、减号(−)、乘号(*)与除号(/)。请注意,这四个运算符均为二元运算符,亦即它们均要求两个操作数,分别位于运算符两侧。上述四种运算符的重载方式完全相同。
事实上,运算符重载共有三种实现途径:成员函数方式、友元函数方式以及普通函数方式。本课将讨论友元函数方式(因其对大多数二元运算符而言最为直观)。下一课将介绍普通函数方式,后续课程再讨论成员函数方式,并最终详细总结何时选用何种方式。
利用友元函数重载运算符
考虑以下类:
class Cents
{
private:
    int m_cents{};
public:
    Cents(int cents) : m_cents{ cents } {}
    int getCents() const { return m_cents; }
};
下面的示例演示如何重载加号运算符(+),以将两个 Cents 对象相加:
#include <iostream>
class Cents
{
private:
    int m_cents{};
public:
    Cents(int cents) : m_cents{ cents } {}
    // 使用友元函数实现 Cents + Cents
    friend Cents operator+(const Cents& c1, const Cents& c2);
    int getCents() const { return m_cents; }
};
// 注意:此函数并非成员函数!
Cents operator+(const Cents& c1, const Cents& c2)
{
    // 借助 Cents 构造函数及内建 operator+(int, int)
    // 由于是友元函数,可直接访问 m_cents
    return { c1.m_cents + c2.m_cents };
}
int main()
{
    Cents cents1{ 6 };
    Cents cents2{ 8 };
    Cents centsSum{ cents1 + cents2 };
    std::cout << "I have " << centsSum.getCents() << " cents.\n";
    return 0;
}
运行结果:
I have 14 cents.
重载加号运算符(+)只需:
- 声明名为 operator+的函数;
- 指定两个形参,类型为欲相加的操作数类型;
- 选择合适的返回类型;
- 实现函数体。
在本例中:
- 形参类型:两个 Cents对象;
- 返回类型:Cents,表示结果;
- 实现:将两个对象的 m_cents相加即可。由于友元函数可直接访问m_cents,且m_cents为整型,可直接使用内建加号运算符。
重载减法运算符(-)
同样简单:
#include <iostream>
class Cents
{
private:
    int m_cents{};
public:
    explicit Cents(int cents) : m_cents{ cents } {}
    friend Cents operator+(const Cents& c1, const Cents& c2);
    friend Cents operator-(const Cents& c1, const Cents& c2);
    int getCents() const { return m_cents; }
};
Cents operator+(const Cents& c1, const Cents& c2)
{
    return { c1.m_cents + c2.m_cents };
}
Cents operator-(const Cents& c1, const Cents& c2)
{
    return { c1.m_cents - c2.m_cents };
}
int main()
{
    Cents cents1{ 6 };
    Cents cents2{ 2 };
    Cents centsDiff{ cents1 - cents2 };
    std::cout << "I have " << centsDiff.getCents() << " cents.\n";
    return 0;
}
以此类推,重载乘号(*)与除号(/)只需分别定义 operator* 与 operator/ 即可。
友元函数可在类内定义
尽管友元函数并非类成员,但可将其定义置于类内:
#include <iostream>
class Cents
{
private:
    int m_cents{};
public:
    explicit Cents(int cents) : m_cents{ cents } {}
    friend Cents operator+(const Cents& c1, const Cents& c2)
    {
        return { c1.m_cents + c2.m_cents };
    }
    int getCents() const { return m_cents; }
};
对于实现简单的运算符重载,这样做完全合法。
重载不同操作数类型的运算符
通常,我们希望重载的运算符支持不同类型操作数。例如,Cents(4) + 6 应得到 Cents(10)。
当 C++ 计算表达式 x + y 时,x 为左操作数,y 为右操作数。若二者类型不同,则 x + y 与 y + x 将调用不同的重载函数。
例如:
- Cents(4) + 6调用- operator+(Cents, int);
- 6 + Cents(4)调用- operator+(int, Cents)。
因此,若需支持不同类型,需编写两个版本:
#include <iostream>
class Cents
{
private:
    int m_cents{};
public:
    explicit Cents(int cents) : m_cents{ cents } {}
    friend Cents operator+(const Cents& c1, int value);
    friend Cents operator+(int value, const Cents& c1);
    int getCents() const { return m_cents; }
};
Cents operator+(const Cents& c1, int value)
{
    return { c1.m_cents + value };
}
Cents operator+(int value, const Cents& c1)
{
    return { value + c1.m_cents };  // 或复用上一版本
}
int main()
{
    Cents c1{ Cents{ 4 } + 6 };
    Cents c2{ 6 + Cents{ 4 } };
    std::cout << "I have " << c1.getCents() << " cents.\n";
    std::cout << "I have " << c2.getCents() << " cents.\n";
    return 0;
}
注意两函数实现相同,仅参数顺序不同。
再举一例:MinMax 类
#include <iostream>
class MinMax
{
private:
    int m_min{}; // 当前最小值
    int m_max{}; // 当前最大值
public:
    MinMax(int min, int max) : m_min{ min }, m_max{ max } {}
    int getMin() const { return m_min; }
    int getMax() const { return m_max; }
    friend MinMax operator+(const MinMax& m1, const MinMax& m2);
    friend MinMax operator+(const MinMax& m, int value);
    friend MinMax operator+(int value, const MinMax& m);
};
MinMax operator+(const MinMax& m1, const MinMax& m2)
{
    int min{ std::min(m1.m_min, m2.m_min) };
    int max{ std::max(m1.m_max, m2.m_max) };
    return { min, max };
}
MinMax operator+(const MinMax& m, int value)
{
    int min{ std::min(m.m_min, value) };
    int max{ std::max(m.m_max, value) };
    return { min, max };
}
MinMax operator+(int value, const MinMax& m)
{
    return m + value; // 复用已有重载
}
int main()
{
    MinMax m1{ 10, 15 };
    MinMax m2{ 8, 11 };
    MinMax m3{ 3, 12 };
    MinMax mFinal{ m1 + m2 + 5 + 8 + m3 + 16 };
    std::cout << "Result: (" << mFinal.getMin() << ", " << mFinal.getMax() << ")\n";
    return 0;
}
输出:
Result: (3, 16)
表达式 m1 + m2 + 5 + 8 + m3 + 16 自左向右求值,每一步返回的 MinMax 对象作为下一次调用的左操作数,最终得到最小值 3、最大值 16。
利用已有运算符实现新运算符
上例中,operator+(int, MinMax) 直接调用 operator+(MinMax, int),减少代码冗余并简化维护。只要这样能使代码更简洁,就应复用已有重载;若实现极为简单(仅一行),是否复用可视情况而定。
小测
问题 1
a) 编写名为 Fraction 的类,含整型分子与分母成员;提供 print() 函数输出分数。
示例代码应能通过编译并输出:
1/4
1/2
b) 使用友元函数方式,重载乘法运算符,支持 Fraction * int 与 Fraction * Fraction。
提示:
- 两分数相乘:分子相乘,分母相乘;
- 分数与整数相乘:分子乘整数,分母不变。
示例代码应能通过编译并输出:
2/5
3/8
6/40
4/5
6/8
6/24
c) 若将构造函数改为非 explicit 并删除整数乘法运算符,为何仍能正确工作?
d) 若将 operator*(Fraction, Fraction) 的引用形参改为非 const,则主函数中某语句失效,何故?
e) 附加题:实现 reduce() 成员函数,将分数化为最简形式(C++17 可用 std::gcd,旧编译器可用示例递归函数)。示例输出见题目要求。
