章节目录

使用成员函数重载运算符

本节阅读量:

前面我们学习了如何使用友元函数和普通函数重载运算符。下面来学习如何使用成员函数重载运算符。

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

  1. 重载运算符是左操作数的成员函数。
  2. 左操作数成为隐式*this对象。
  3. 所有其他操作数都成为函数参数。

下面是使用友元函数重载operator+的方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#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+,而不是友元operator+)。
  2. 移除左侧参数,因为它现在变成了隐式的*this对象。
  3. 在函数体内,移除对左侧参数的显式使用(例如,cents.m_cents变为m_cents,因为它已经通过*this访问)。

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#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),我们只是用不同方式定义了函数。双参数友元函数变成了单参数成员函数,友元版本中最左侧的参数(cents)成为成员函数版本中的隐式*this。

让我们仔细看看表达式cents1+2的计算方式。

在友元函数版本中,表达式cents1+2会变成函数调用operator+(cents1,2)。注意,这里有两个函数参数。这很简单。

在成员函数版本中,表达式cents1+2会变成函数调用cents1.operator+(2)。请注意,现在只有一个显式函数参数,并且cents1成为了对象前缀。编译器会隐式地将对象前缀转换为名为*this的最左侧隐藏参数。因此,实际效果上cents1.operator+(2)会变成operator+(&cents1,2),这几乎与友元版本相同。

这两种情况产生相同的结果,只是方式略有不同。

因此,如果某个运算符既可以重载为友元函数,也可以重载为成员函数,我们应该选择哪一种?要回答这个问题,还需要了解一些内容。


并非所有内容都可以作为友元函数重载

赋值(=)、下标([])、函数调用(())和成员选择(->)运算符必须重载为成员函数,因为语言要求它们是成员函数。


并非所有内容都可以作为成员函数重载

在前面重载I/O运算符时,我们使用友元函数方式为Point类重载了operator«。下面是对应示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#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 的友元, 因此可以直接访问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;
}

然而,我们不能将运算符«作为成员函数重载。为什么呢?因为重载运算符必须作为左操作数的成员。在这种情况下,左操作数是std::ostream类型的对象,而std::ostream属于标准库。我们不能修改它的类声明,把重载添加为std::ostream的成员函数。

因此,需要将运算符«重载为普通函数(首选)或友元函数。

类似地,尽管我们可以将operator+(Cents,int)作为成员函数重载(正如上面所做的),但不能将operator+(int,Cents)作为成员函数重载,因为int不是可以添加成员的类。

通常,如果左操作数不是类(例如int),或者是无法修改的类(例如std::ostream),就无法使用成员函数重载。


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

在大多数情况下,语言允许您自行决定使用普通/友元函数版本,还是成员函数版本。然而,其中一种通常会比另一种更合适。

当处理不修改左操作数的二元运算符(例如operator+)时,通常首选普通函数或友元函数版本,因为它适用于所有参数类型(即使左操作数不是类对象,或者是不可修改的类)。普通函数或友元函数版本还具有“对称性”的优势,因为所有操作数都会成为显式参数(而不是左操作数变成*this,右操作数才是显式参数)。

当处理会修改左操作数的二元运算符(例如operator+=)时,通常首选成员函数版本。在这些情况下,最左侧的操作数通常是类类型,直接在该对象上操作也更自然。因为右侧操作数会成为显式参数,所以不会混淆谁被修改、谁只是被读取。

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

以下经验法则可以帮助您确定哪种形式最适合给定的情况:

  1. 如果重载赋值(=)、下标([])、函数调用(())或成员选择(->),请作为成员函数进行重载。
  2. 如果要重载一元运算符,请作为成员函数进行重载。
  3. 如果重载不修改其左操作数的二元运算符(例如,运算符+),请作为普通函数(首选)或友元函数进行重载。
  4. 如果重载修改其左操作数的二元运算符,但左侧操作数不允许添加成员函数(例如,运算符«,它具有ostream类型的左操作数)的类定义中,请作为普通函数(首选)或友元函数进行重载。
  5. 如果重载修改其左操作数的二元运算符(例如,运算符+=),并且可以修改左操作数的定义,请作为成员函数进行重载。

21.3 重载I/O运算符

上一节

21.5 重载一元运算符+、-和!

下一节