21-5 使用成员函数重载运算符

第21.2节——使用友元函数重载算术运算符中,你学习了如何使用友元函数重载算术运算符。你还了解到可以像重载普通函数那样重载运算符。许多运算符还可以通过另一种方式重载:作为成员函数。

使用成员函数重载运算符与使用友元函数重载运算符非常相似。当使用成员函数重载运算符时:

  • 重载的运算符必须作为左操作数的成员函数添加。
  • 左操作数成为隐式*this对象
  • 所有其他操作数成为函数参数。

作为回顾,以下是使用友元函数重载+运算符的示例:

#include <iostream>

class Cents
{
private:
    int m_cents {};

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

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

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

// note: this function is not a member function!
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;
}

image

将友元重载运算符转换为成员重载运算符很简单:

  • 将重载运算符定义为成员而非友元(Cents::operator+ 替代 friend operator+)
  • 移除左侧参数,因为该参数现已成为隐式 *this 对象。
  • 函数体内所有对左参数的引用均可移除(例如 cents.m_cents 变为 m_cents,该引用隐式指向 *this 对象)。

现在,使用成员函数方式重载相同运算符:

#include <iostream>

class Cents
{
private:
    int m_cents {};

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

    // Overload Cents + int
    Cents operator+(int value) const;

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

// note: this function is a member function!
// the cents parameter in the friend version is now the implicit *this parameter
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;
}

image

请注意运算符的使用方式并未改变(两种情况都是 cents1 + 2),我们只是以不同方式定义了函数。原本的双参数友元函数变成了单参数成员函数,其中友元版本中最左侧的参数(cents)在成员函数版本中变成了隐含的 *this 参数。

让我们深入解析表达式 cents1 + 2 的求值过程。

在友元函数版本中,表达式 cents1 + 2 转化为函数调用 operator+(cents1, 2)。注意这里存在两个函数参数,这很直观。

在成员函数版本中,表达式 cents1 + 2 转化为函数调用 cents1.operator+(2)。此时仅有一个显式函数参数,且 cents1 已成为对象前缀。但在第 15.1 课 隐藏的 this 指针与成员函数链式调用 中我们提到,编译器会将对象前缀隐式转换为名为 *this 的隐藏左侧参数。因此实际中 cents1.operator+(2) 转化为 operator+(&cents1, 2),这与友元版本几乎完全相同。

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

既然既可通过友元方式又可通过成员方式重载运算符,我们该如何选择?要解答这个问题,你还需要了解以下几点。


并非所有运算符都能作为友元函数重载

赋值运算符(=)、下标运算符([])、函数调用运算符(())以及成员选择运算符(->)必须作为成员函数重载,因为语言规范要求如此。


并非所有内容都能作为成员函数进行重载

第21.4节——重载I/O运算符中,我们通过友元函数方法为Point类重载了<<运算符。以下是操作步骤回顾:

#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)
{
    // Since operator<< is a friend of the Point class, we can access Point's members directly.
    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;
}

image

然而,我们无法将<<运算符重载为成员函数。为什么?因为重载的运算符必须作为左操作数的成员添加。在此情况下,左操作数是std::ostream类型的对象。std::ostream作为标准库的一部分是固定的,我们无法修改类声明来将其重载添加为std::ostream的成员函数。

这意味着<<运算符必须作为普通函数(推荐方式)或友元函数进行重载。

同理,虽然我们可以将operator+(Cents, int)作为成员函数重载(如前所述),但无法将operator+(int, Cents)作为成员函数重载,因为int并非可添加成员的类。

通常情况下,当左操作数既非类(如 int),又属于不可修改的类(如 std::ostream)时,成员重载便无法使用。


何时使用普通重载、友元重载或成员重载

在大多数情况下,语言将选择权交由开发者决定是使用普通/友元重载还是成员重载版本。但通常其中一种选择会优于另一种。

对于不修改左操作数的二元运算符(如operator+),通常更推荐使用普通函数或友元函数版本,因为它适用于所有参数类型(即使左操作数不是类对象,或属于不可修改的类)。普通函数或友元函数版本还具有“对称性”优势——所有操作数均成为显式参数(而非左操作数变为*this而右操作数成为显式参数)。

对于会修改左操作数的二元运算符(如 operator+=),通常更推荐使用成员函数版本。此时最左操作数必然是类类型,让被修改对象成为 *this 所指向的对象更为自然。由于最右操作数成为显式参数,可避免混淆修改对象与评估对象的责任归属。

一元运算符通常也作为成员函数重载,因其版本无需参数。

以下经验法则可帮助确定特定场景的最佳形式:

  • 若重载赋值运算符(=)、下标运算符([])、函数调用运算符(())或成员选择运算符(->),请采用成员函数形式。
  • 若重载一元运算符,请采用成员函数形式。
  • 若重载不修改左操作数的二元运算符(如operator+),建议采用普通函数或友元函数形式。
  • 若重载的二元运算符会修改左操作数,但无法在左操作数的类定义中添加成员(如左操作数类型为 ostream 的 operator<<),则应采用普通函数(推荐)或友元函数实现。
  • 若重载的二元运算符会修改其左操作数(如 operator+=),且可修改左操作数的定义,则应将其实现为成员函数。
posted @ 2026-01-22 16:29  游翔  阅读(0)  评论(0)    收藏  举报