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;
}

将友元重载运算符转换为成员重载运算符很简单:
- 将重载运算符定义为成员而非友元(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;
}

请注意运算符的使用方式并未改变(两种情况都是 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+(¢s1, 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;
}

然而,我们无法将<<运算符重载为成员函数。为什么?因为重载的运算符必须作为左操作数的成员添加。在此情况下,左操作数是std::ostream类型的对象。std::ostream作为标准库的一部分是固定的,我们无法修改类声明来将其重载添加为std::ostream的成员函数。
这意味着<<运算符必须作为普通函数(推荐方式)或友元函数进行重载。
同理,虽然我们可以将operator+(Cents, int)作为成员函数重载(如前所述),但无法将operator+(int, Cents)作为成员函数重载,因为int并非可添加成员的类。
通常情况下,当左操作数既非类(如 int),又属于不可修改的类(如 std::ostream)时,成员重载便无法使用。
何时使用普通重载、友元重载或成员重载
在大多数情况下,语言将选择权交由开发者决定是使用普通/友元重载还是成员重载版本。但通常其中一种选择会优于另一种。
对于不修改左操作数的二元运算符(如operator+),通常更推荐使用普通函数或友元函数版本,因为它适用于所有参数类型(即使左操作数不是类对象,或属于不可修改的类)。普通函数或友元函数版本还具有“对称性”优势——所有操作数均成为显式参数(而非左操作数变为*this而右操作数成为显式参数)。
对于会修改左操作数的二元运算符(如 operator+=),通常更推荐使用成员函数版本。此时最左操作数必然是类类型,让被修改对象成为 *this 所指向的对象更为自然。由于最右操作数成为显式参数,可避免混淆修改对象与评估对象的责任归属。
一元运算符通常也作为成员函数重载,因其版本无需参数。
以下经验法则可帮助确定特定场景的最佳形式:
- 若重载赋值运算符(=)、下标运算符([])、函数调用运算符(())或成员选择运算符(->),请采用成员函数形式。
- 若重载一元运算符,请采用成员函数形式。
- 若重载不修改左操作数的二元运算符(如operator+),建议采用普通函数或友元函数形式。
- 若重载的二元运算符会修改左操作数,但无法在左操作数的类定义中添加成员(如左操作数类型为 ostream 的 operator<<),则应采用普通函数(推荐)或友元函数实现。
- 若重载的二元运算符会修改其左操作数(如 operator+=),且可修改左操作数的定义,则应将其实现为成员函数。

浙公网安备 33010602011771号