使用成员函数重载运算符

在《使用友元函数重载算术运算符》中,你已学习了如何使用友元函数重载算术运算符,也了解到运算符可以按普通函数的方式重载。事实上,许多运算符还有第三种重载方式:以成员函数的形式实现。

一、成员函数重载的基本思路

使用成员函数重载运算符与使用友元函数非常相似,但需满足以下要求:

  • 被重载的运算符必须作为“左操作数”所属类的成员函数;
  • 左操作数成为隐含的 *this 对象;
  • 其余操作数均作为函数的显式形参。

二、友元版本回顾

回忆一下我们曾用友元函数重载 operator+ 的写法:

#include <iostream>

class Cents
{
private:
    int m_cents {};

public:
    Cents(int cents)
        : m_cents { cents } { }

    // 重载 Cents + int
    friend Cents operator+(const Cents& cents, int value);

    int getCents() const { return m_cents; }
};

// 注意:此函数并非成员函数!
Cents operator+(const Cents& cents, int value)
{
    return Cents(cents.m_cents + value);
}

int main()
{
    const Cents cents1 { 6 };
    const Cents cents2 { cents1 + 2 };
    std::cout << "I have " << cents2.getCents() << " cents.\n";

    return 0;
}

三、改写为成员函数

将友元版本转换为成员版本十分直接:

  1. 将运算符定义为类的成员(Cents::operator+),而非友元;
  2. 去掉左侧形参,因为它现在由隐含的 *this 提供;
  3. 函数体内凡引用左侧形参之处,均改为直接访问数据成员(例如 cents.m_cents 改为 m_cents,隐含地通过 *this)。
#include <iostream>

class Cents
{
private:
    int m_cents {};

public:
    Cents(int cents)
        : m_cents { cents } { }

    // 重载 Cents + int
    Cents operator+(int value) const;

    int getCents() const { return m_cents; }
};

// 注意:此函数是成员函数!
// 友元版本中的 cents 形参现在由隐含的 *this 提供
Cents Cents::operator+(int value) const
{
    return Cents { m_cents + value };
}

int main()
{
    const Cents cents1 { 6 };
    const Cents cents2 { cents1 + 2 };
    std::cout << "I have " << cents2.getCents() << " cents.\n";

    return 0;
}

四、表达式求值方式对比

表达式 cents1 + 2 在不同版本中的求值过程如下:

  • 友元版本:表达式转换为函数调用 operator+(cents1, 2),有两个显式实参,直观明了。
  • 成员版本:表达式转换为 cents1.operator+(2),仅有一个显式实参,cents1 作为对象前缀。根据 ,编译器会将对象前缀隐式转换为隐藏的最左形参 *this,因此实际调用为 operator+(&cents1, 2),与友元版本几乎一致。

两种实现最终产生相同结果,只是路径略有差异。

五、友元与成员函数的选择

若运算符既可声明为友元又可声明为成员,究竟该如何取舍?需先了解以下限制:

1. 某些运算符必须以成员函数形式重载

赋值(=)、下标([])、函数调用(())、成员访问(->)运算符,语言强制要求它们必须是成员函数。

2. 某些运算符无法以成员函数形式重载

中,我们以友元方式为 Point 类重载了 operator<<。回顾如下:

#include <iostream>

class Point
{
private:
    double m_x {};
    double m_y {};
    double m_z {};

public:
    Point(double x = 0.0, double y = 0.0, double z = 0.0)
        : m_x { x }, m_y { y }, m_z { z }
    {
    }

    friend std::ostream& operator<< (std::ostream& out, const Point& point);
};

std::ostream& operator<< (std::ostream& out, const Point& point)
{
    // 因 operator<< 是 Point 的友元,可直接访问其成员
    out << "Point(" << point.m_x << ", " << point.m_y << ", " << point.m_z << ")";

    return out;
}

int main()
{
    Point point1 { 2.0, 3.0, 4.0 };

    std::cout << point1;

    return 0;
}

我们无法将 operator<< 实现为成员函数,原因在于:

  • 重载运算符必须作为“左操作数”所属类的成员;
  • 此处左操作数为 std::ostream 类型,而 std::ostream 属于标准库,无法修改其定义以添加成员。

因此,operator<< 只能以普通函数(首选)或友元函数形式实现。

同理,虽然 operator+(Cents, int) 可以写成成员函数,但 operator+(int, Cents) 却无法写成成员函数,因为 int 并非可添加成员的类。

六、何时使用普通函数、友元函数或成员函数

语言在大多数情况下允许自由选择,但通常其中一种形式更为合适。经验法则如下:

  1. 若重载赋值(=)、下标([])、函数调用(())或成员访问(->)运算符,务必使用成员函数。
  2. 若重载一元运算符,通常使用成员函数(成员版本无形参)。
  3. 若重载不修改左操作数的二元运算符(如 operator+),优先使用普通函数或友元函数,因其对所有参数类型均适用(即使左操作数不是类对象或无法修改),且所有操作数均为显式形参,具有“对称性”。
  4. 若重载会修改左操作数的二元运算符,但无法修改左操作数所属类的定义(例如 operator<<,其左操作数为 ostream),则使用普通函数(首选)或友元函数。
  5. 若重载会修改左操作数的二元运算符(如 operator+=),且能够修改左操作数所属类的定义,则使用成员函数。

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

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

公众号二维码

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