13-5 I/O 运算符重载介绍
在上节课(13.4——枚举与字符串的转换)中,我们展示了这个示例:通过函数将枚举转换为等效字符串:
#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) << '\n';
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的字面量。
当计算此表达式时,编译器会查找operator<<能够处理 std::ostream和 int类型参数的重载函数。它会找到这样一个函数(也定义在标准 I/O 库中)并调用它。在该函数内部, std::cout 用于向控制台输出(具体实现方式由实现定义)。最后,该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 "???";
}
}
// Teach operator<< how to print a Color
// std::ostream is the type of std::cout, std::cerr, etc...
// The return type and parameter type are references (to prevent copies from being made)
std::ostream& operator<<(std::ostream& out, Color color)
{
out << getColorName(color); // print our color's name to whatever output stream out
return out; // operator<< conventionally returns its left operand
// The above can be condensed to the following single line:
// return out << getColorName(color)
}
int main()
{
Color shirt{ blue };
std::cout << "Your shirt is " << shirt << '\n'; // it works!
return 0;
}
打印出来的内容:

让我们深入解析这个重载运算符函数。首先,函数名为 operator<<,因为我们要重载的就是这个运算符。operator<< 有两个参数。左侧参数(将与左操作数匹配)是我们的输出流,其类型为 std::ostream。这里采用非const引用传递,因为我们不希望函数调用时复制std::ostream对象,但输出操作需要修改该对象。右参数(将与右操作数匹配)是我们的Color对象。由于<<运算符惯例上返回其左操作数,因此返回类型与左操作数的类型(即std::ostream&)一致。
现在我们来看具体实现。std::ostream对象本身就懂得如何通过<<运算符打印std::string_view(这是标准库提供的功能)。因此out << getColorName(color)只是获取color的名称作为std::string_view,然后将其打印到输出流中。
请注意,我们的实现使用参数 out 而不是 std::cout,因为我们希望允许调用方决定输出到哪个输出流(例如,std::cerr << color 应输出到 std::cerr,而非 std::cout)。
返回左操作数也很简单。左操作数是参数 out,因此我们只需返回 out 即可。
综合来看:当我们调用 std::cout << shirt 时,编译器会发现我们已重载了 operator<< 以处理 Color 类型的对象。随后,重载的<<运算符函数被调用,其中std::cout作为输出参数out,而shirt变量(其值为blue)作为参数color传入。由于out是std::cout的引用,而color是枚举值blue的副本,表达式out << getColorName(color)将向控制台输出“blue”。最后out被返回给调用方,以便后续进行链式输出操作。
重载>>运算符以输入枚举器
正如我们之前能够让运算符<<输出枚举值那样,我们也可以让运算符>>学会如何输入枚举值:
#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 is an in/out parameter
std::istream& operator>>(std::istream& in, Pet& pet)
{
std::string s{};
in >> s; // get input string from user
std::optional<Pet> match { getPetFromString(s) };
if (match) // if we found a match
{
pet = *match; // dereference std::optional to get matching enumerator
return in;
}
// We didn't find a match, so input must have been invalid
// so we will set input stream to fail state
in.setstate(std::ios_base::failbit);
// On an extraction failure, operator>> zero-initializes fundamental types
// Uncomment the following line to make this operator do the same thing
// pet = {};
return in;
}
int main()
{
std::cout << "Enter a pet: cat, dog, pig, or whale: ";
Pet pet{};
std::cin >> pet;
if (std::cin) // if we found a match
std::cout << "You chose: " << getPetName(pet) << '\n';
else
{
std::cin.clear(); // reset the input stream to good
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
std::cout << "Your pet was not valid\n";
}
return 0;
}

这里有几点与输出案例的差异值得注意。首先,std::cin 的类型是 std::istream,因此我们使用 std::istream& 作为左参数和返回值的类型,而非 std::ostream&。其次,pet 参数是非 const 引用。这使得我们的 operator>> 能在匹配成功时修改传入的右操作数值。
关键见解:
我们的右操作数(pet)是输出参数。我们将在第12.13 课——输入和输出参数中介绍输出参数。
如果pet是一个值参数而不是引用参数,那么我们的operator>>函数最终会将新值赋给右操作数的副本,而不是实际的右操作数。而我们希望在这种情况下修改的是右操作数本身。
在函数内部,我们使用operator>>运算符输入std::string(这是它已掌握的功能)。若用户输入的值与我们的宠物之一匹配,则可将对应枚举值赋给pet,并返回左操作数(in)。
如果用户未输入有效的宠物名称,则通过将 std::cin 置于“失败模式”来处理此情况。这是 std::cin 在提取失败时通常进入的状态。调用方随后可检查 std::cin 以判断提取操作是否成功。
相关内容:
在第17.6 课——std::array 和枚举中,我们展示了如何使用它们std::array来减少输入和输出运算符的冗余,并避免在添加新的枚举器时修改它们。

浙公网安备 33010602011771号