输入输出运算符的重载入门

在之前的课程,我们展示了以下示例,其中我们使用了一个函数将枚举类型转换为等效的字符串:

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

虽然上述示例可以正常工作,但存在两个缺点:

  1. 我们必须记住我们创建的用于获取枚举器名称的函数的名称。
  2. 调用这样的函数会使输出语句显得杂乱。

理想情况下,如果我们能够教 operator<< 输出枚举类型,这样我们就可以像这样操作:std::cout << shirt,并让它按我们期望的方式工作,那将是非常方便的。

运算符重载简介

在第 11.1 课 – 函数重载简介中,我们介绍了函数重载,它允许我们创建具有相同名称的多个函数,只要每个函数具有唯一的函数原型即可。通过函数重载,我们可以为不同数据类型创建函数的不同变体,而无需为每个变体想出一个独特的名称。

类似地,C++ 也支持运算符重载,它允许我们定义现有运算符的重载版本,以便使这些运算符能够与我们定义的数据类型一起工作。

基本的运算符重载相当直接:

  1. 使用运算符的名称作为函数名称来定义一个函数。
  2. 为每个操作数(按从左到右的顺序)添加适当类型的参数。这些参数中至少有一个必须是用户定义的类型(类类型或枚举类型),否则编译器会报错。
  3. 将返回类型设置为有意义的类型。
  4. 使用 return 语句返回运算的结果。

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

相关内容:

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

对于高级读者:

运算符也可以作为最左侧操作数的成员函数进行重载。我们在第 21.5 课 – 使用成员函数重载运算符中讨论了这一点。

重载 operator<< 以打印枚举器

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

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

当评估这个表达式时,编译器会查找一个能够处理 std::ostreamint 类型参数的重载 operator<< 函数。它会找到这样一个函数(这也是标准输入输出库中定义的一部分),并调用它。在该函数内部,使用 std::coutx 输出到控制台(具体实现方式由实现定义)。最后,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 参数。由于 outstd::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
```}]}}}

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"