使用友元函数重载算术运算符

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.

重载加号运算符(+)只需:

  1. 声明名为 operator+ 的函数;
  2. 指定两个形参,类型为欲相加的操作数类型;
  3. 选择合适的返回类型;
  4. 实现函数体。

在本例中:

  • 形参类型:两个 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 + yy + 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 * intFraction * 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,旧编译器可用示例递归函数)。示例输出见题目要求。

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

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

公众号二维码

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