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

虽然上述示例运行良好,但仍有两个缺点:

  1. 我们需要记住我们创建的函数的名称才能获取枚举器名称。
  2. 调用这样的函数会使我们的输出语句变得冗杂。

理想情况下,如果我们能以某种方式教会operator<<它输出枚举,那么我们就可以做类似这样的事情:std::cout << shirt并让它执行我们期望的操作。

运算符重载简介

在第11.1 课——函数重载简介中,我们介绍了函数重载。函数重载允许我们创建多个同名函数,只要每个函数都有唯一的函数原型即可。利用函数重载,我们可以创建处理不同数据类型的函数变体,而无需为每个变体想出一个唯一的名称。

类似地,C++ 也支持运算符重载,这允许我们定义现有运算符的重载,以便我们可以使这些运算符与我们程序定义的数据类型一起使用。

基本运算符重载相当简单:

  • 使用运算符的名称作为函数名来定义函数。
  • 为每个操作数(从左到右)添加一个适当类型的参数。其中一个参数必须是用户自定义类型(类类型或枚举类型),否则编译器会报错。
  • 将返回类型设置为任何合适的类型。
  • 使用 return 语句返回操作结果。

当编译器在表达式中遇到运算符,且一个或多个操作数是用户自定义类型时,编译器会检查是否存在可用于解析该调用的重载运算符函数。例如,给定一个表达式x + y,编译器会使用函数重载解析来查找是否存在operator+(x, y)可用于计算该运算的函数调用。如果找到明确的operator+函数,则会调用该函数,并将运算结果作为返回值返回。

相关内容:
我们将在第21 章中更详细地介绍运算符重载。

适合高级读者:
运算符也可以作为最左边操作数的成员函数进行重载。我们将在第21.5 课——使用成员函数重载运算符中讨论这一点。

重载operator<<以打印枚举器

在继续之前,让我们快速回顾一下它operator<<在用于输出时的工作方式。

考虑一个简单的表达式,例如std::cout << 5std::cout它具有类型std::ostream(这是标准库中的用户定义类型),并且5是类型int的字面量。

当计算此表达式时,编译器会查找operator<<能够处理 std::ostreamint类型参数的重载函数。它会找到这样一个函数(也定义在标准 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;
}

打印出来的内容:

image

让我们深入解析这个重载运算符函数。首先,函数名为 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;
}

image

这里有几点与输出案例的差异值得注意。首先,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来减少输入和输出运算符的冗余,并避免在添加新的枚举器时修改它们。

posted @ 2025-12-21 22:04  游翔  阅读(10)  评论(0)    收藏  举报