在之前的课程,我们展示了以下示例,其中我们使用了一个函数将枚举类型转换为等效的字符串:
#include <iostream>
#include <string_view>
enum Color
{
black,
red,
blue,
};
constexpr std::string_view getColorName(Color color)
{
switch (color)
{
case black: return "black";
case red: return "red";
case blue: return "blue";
default: return "???";
}
}
int main()
{
constexpr Color shirt{ blue };
std::cout << "Your shirt is " << getColorName(shirt) << '
';
return 0;
}
虽然上述示例可以正常工作,但存在两个缺点:
- 我们必须记住我们创建的用于获取枚举器名称的函数的名称。
- 调用这样的函数会使输出语句显得杂乱。
理想情况下,如果我们能够教 operator<<
输出枚举类型,这样我们就可以像这样操作:std::cout << shirt
,并让它按我们期望的方式工作,那将是非常方便的。
运算符重载简介
在第 11.1 课 – 函数重载简介中,我们介绍了函数重载,它允许我们创建具有相同名称的多个函数,只要每个函数具有唯一的函数原型即可。通过函数重载,我们可以为不同数据类型创建函数的不同变体,而无需为每个变体想出一个独特的名称。
类似地,C++ 也支持运算符重载,它允许我们定义现有运算符的重载版本,以便使这些运算符能够与我们定义的数据类型一起工作。
基本的运算符重载相当直接:
- 使用运算符的名称作为函数名称来定义一个函数。
- 为每个操作数(按从左到右的顺序)添加适当类型的参数。这些参数中至少有一个必须是用户定义的类型(类类型或枚举类型),否则编译器会报错。
- 将返回类型设置为有意义的类型。
- 使用
return
语句返回运算的结果。
当编译器在表达式中遇到运算符的使用,并且一个或多个操作数是用户定义的类型时,编译器会检查是否存在可以用来解析该调用的重载运算符函数。例如,对于某个表达式 x + y
,编译器将使用函数重载解析来查找是否存在一个 operator+(x, y)
函数调用来评估该运算。如果可以找到一个无歧义的 operator+
函数,它将被调用,并将运算的结果作为返回值返回。
相关内容:
我们将在第 21 章中更详细地介绍运算符重载。
对于高级读者:
运算符也可以作为最左侧操作数的成员函数进行重载。我们在第 21.5 课 – 使用成员函数重载运算符中讨论了这一点。
重载 operator<<
以打印枚举器
在继续之前,让我们快速回顾一下 operator<<
用于输出时的工作方式。
考虑一个简单的表达式,如 std::cout << 5
。std::cout
的类型是 std::ostream
(这是标准库中的一个用户定义类型),而 5
是一个类型为 int
的字面量。
当评估这个表达式时,编译器会查找一个能够处理 std::ostream
和 int
类型参数的重载 operator<<
函数。它会找到这样一个函数(这也是标准输入输出库中定义的一部分),并调用它。在该函数内部,使用 std::cout
将 x
输出到控制台(具体实现方式由实现定义)。最后,operator<<
函数返回其左操作数(在这种情况下是 std::cout
),以便可以将后续的 operator<<
调用进行链式操作。
考虑到上述内容,让我们实现一个重载的 operator<<
来打印 Color
:
#include <iostream>
#include <string_view>
enum Color
{
black,
red,
blue,
};
constexpr std::string_view getColorName(Color color)
{
switch (color)
{
case black: return "black";
case red: return "red";
case blue: return "blue";
default: return "???";
}
}
// 教授 operator<< 如何打印 Color
// std::ostream 是 std::cout、std::cerr 等的类型
// 返回类型和参数类型都是引用(以防止复制操作)
std::ostream& operator<<(std::ostream& out, Color color)
{
out << getColorName(color); // 使用输出流 out 打印颜色名称
return out; // 按约定,operator<< 返回其左操作数
// 上述内容可以简化为以下一行代码:
// return out << getColorName(color);
}
int main()
{
Color shirt{ blue };
std::cout << "Your shirt is " << shirt << '
'; // 它可以工作了!
return 0;
}
该程序的输出为:
Your shirt is blue
让我们来详细分析一下我们重载的运算符函数。首先,函数的名称是 operator<<
,因为我们正在重载这个运算符。operator<<
有两个参数。左参数(将与左操作数匹配)是我们的输出流,类型为 std::ostream
。这里我们使用非 const 引用传递,因为在函数调用时我们不希望复制一个 std::ostream
对象,但 std::ostream
对象需要被修改以便进行输出。右参数(将与右操作数匹配)是我们的 Color
对象。由于 operator<<
按约定返回其左操作数,因此返回类型与左操作数的类型匹配,即 std::ostream&
。
现在我们来看实现部分。std::ostream
对象已经知道如何使用 operator<<
打印 std::string_view
(这是标准库的一部分)。因此,out << getColorName(color)
只是获取我们颜色的名称作为 std::string_view
,然后将其打印到输出流中。
请注意,我们的实现使用了参数 out
而不是 std::cout
,因为我们希望调用者决定他们将输出到哪个输出流(例如,std::cerr << color
应该将输出发送到 std::cerr
,而不是 std::cout
)。
返回左操作数也很简单。左操作数是参数 out
,因此我们只需返回 out
即可。
将所有内容整合在一起:当我们调用 std::cout << shirt
时,编译器会看到我们已经重载了 operator<<
以使其能够处理 Color
类型的对象。然后调用我们重载的 operator<<
函数,其中 std::cout
作为 out
参数,shirt
变量(值为 blue
)作为 color
参数。由于 out
是 std::cout
的引用,color
是枚举器 blue
的副本,因此表达式 out << getColorName(color)
将 “blue” 打印到控制台。最后,out
被返回给调用者,以便我们可以链式地进行更多输出。
重载 operator>>
以输入枚举器
与我们上面能够教 operator<<
输出枚举类型类似,我们也可以教 operator>>
输入枚举类型:
#include <iostream>
#include <limits>
#include <optional>
#include <string>
#include <string_view>
enum Pet
{
cat, // 0
dog, // 1
pig, // 2
whale, // 3
};
constexpr std::string_view getPetName(Pet pet)
{
switch (pet)
{
case cat: return "cat";
case dog: return "dog";
case pig: return "pig";
case whale: return "whale";
default: return "???";
}
}
constexpr std::optional<Pet> getPetFromString(std::string_view sv)
{
if (sv == "cat") return cat;
if (sv == "dog") return dog;
if (sv == "pig") return pig;
if (sv == "whale") return whale;
return {}; // 表示“无值”
}
// pet 是一个输入/输出参数
std::istream& operator>>(std::istream& in, Pet& pet)
{
std::string s{};
in >> s; // 从用户获取输入字符串
std::optional<Pet> match { getPetFromString(s) };
if (match) // 如果找到匹配项
{
pet = *match
```}]}}}