章节目录

重载I/O运算符

本节阅读量:

对于具有多个成员变量的类,逐个将成员变量打印到屏幕上很快就会变得繁琐。例如,考虑以下类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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}
    {
    }

    double getX() const { return m_x; }
    double getY() const { return m_y; }
    double getZ() const { return m_z; }
};

如果要将这个类的实例打印到屏幕上,就必须这样写:

1
2
3
4
5
Point point { 5.0, 6.0, 7.0 };

std::cout << "Point(" << point.getX() << ", " <<
    point.getY() << ", " <<
    point.getZ() << ')';

当然,编写一个可复用的函数来完成这件事更合理。下面我们创建了print()函数,其工作方式如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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}
    {
    }

    double getX() const { return m_x; }
    double getY() const { return m_y; }
    double getZ() const { return m_z; }

    void print() const
    {
        std::cout << "Point(" << m_x << ", " << m_y << ", " << m_z << ')';
    }
};

虽然这样好多了,但它仍然有一些缺点。因为print()返回void,所以不能在输出语句中间调用它。您必须改成这样:

1
2
3
4
5
6
7
8
int main()
{
    const Point point { 5.0, 6.0, 7.0 };

    std::cout << "My point is: ";
    point.print();
    std::cout << " in Cartesian space.\n";
}

如果可以简单地键入:

1
2
Point point{5.0, 6.0, 7.0};
cout << "My point is: " << point << " in Cartesian space.\n";

并得到相同结果,那就更理想了。这样既不用把输出拆成多个语句,也不必记住您给print函数起的名字。

幸运的是,通过重载«运算符,可以实现该功能!


重载operator«

重载运算符«类似于重载运算符+(它们都是二元运算符),区别主要在于参数类型不同。

考虑表达式"std::cout « point"。运算符«的操作数是什么?左操作数是std::cout对象,右操作数是Point类对象。std::cout实际上是一个std::ostream类型的对象。因此,我们的重载函数将如下所示:

1
2
// std::ostream 是 std::cout 对象的类型
friend std::ostream& operator<< (std::ostream& out, const Point& point);

Point类的operator«实现相当简单。因为C++已经知道如何使用运算符«输出double,并且我们的成员都是double,所以可以直接使用运算符«输出Point的成员变量。下面是在上面的Point类中新增重载运算符«。

 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 类的友元, 所以可以直接访问私有成员变量
    out << "Point(" << point.m_x << ", " << point.m_y << ", " << point.m_z << ')'; // 这里做实际输出

    return out; // 返回 std::ostream 以便可以链式调用 operator<<
}

int main()
{
    const Point point1 { 2.0, 3.0, 4.0 };

    std::cout << point1 << '\n';

    return 0;
}

这非常简单。请注意,我们的输出行与前面编写的print()函数中的那一行非常相似。最明显的区别是,std::cout变成了参数out(调用函数时,它将是对std::cout的引用)。

这里最棘手的部分是返回类型。对于算术运算符,我们通常按值计算并返回一个结果。然而,如果您试图按值返回std::ostream,就会得到编译错误,因为std::ostream明确禁止复制。

在这种情况下,我们将左操作数作为引用返回。这不仅可以避免生成std::ostream的副本,还允许我们把输出操作“链接”起来,例如"std::cout« point « std::endl;"。

考虑一下,如果运算符«返回void会发生什么。当编译器计算"std::cout « point « ‘\n’; “时,由于优先级和结合性规则,它会将该表达式视为”(std::cout « point)« ‘\n’;"。“std::cout « point"将调用我们返回void的重载operator«函数,因此这个子表达式的结果是void。随后,剩余表达式就变成了:” void « ‘\n’;",这毫无意义!

相反,如果返回out参数,(std::cout « point)就会返回std::cout。于是,剩余表达式就变成:" std::cout « ‘\n’;"!

每当我们希望重载的二元运算符能够以这种方式链接调用时,都应该(通过引用)返回左操作数。在这种情况下,通过引用返回左操作数是可以的,因为左操作数由调用方传入,当被调用函数返回时它仍然必须存在。因此,不必担心返回悬空引用。

为了证明它有效,请看下面这个将Point类与重载运算符«一起使用的示例:

 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
35
#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 类的友元, 所以可以直接访问私有成员变量
    out << "Point(" << point.m_x << ", " << point.m_y << ", " << point.m_z << ')';

    return out;
}

int main()
{
    Point point1 { 2.0, 3.5, 4.0 };
    Point point2 { 6.0, 7.5, 8.0 };

    std::cout << point1 << ' ' << point2 << '\n';

    return 0;
}

这将产生以下结果:

1
Point(2, 3.5, 4) Point(6, 7.5, 8)

在上面的示例中,operator«是友元函数,因为它需要直接访问Point的私有成员。然而,如果可以通过getter访问成员,那么operator«也可以实现为非友元函数。


重载operator»

也可以重载输入运算符。它的实现方式与重载输出运算符类似。您需要知道的关键点是,std::cin是一个std::istream类型的对象。下面是添加了重载运算符»的Point类:

 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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#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 类的友元, 所以可以直接访问私有成员变量
    out << "Point(" << point.m_x << ", " << point.m_y << ", " << point.m_z << ')';

    return out;
}

// 注意 point 参数是非 const,因为我们需要修改它
// 注 这里实现为非友元函数
std::istream& operator>> (std::istream& in, Point& point)
{
    double x{};
    double y{};
    double z{};
    
    in >> x >> y >> z;

    if (in)                     // 如果所有的输入都ok
        point = Point{x, y, z}; // 覆盖现有的point对象
        
    return in;
}

int main()
{
    std::cout << "Enter a point: ";

    Point point{};
    std::cin >> point;

    std::cout << "You entered: " << point << '\n';

    return 0;
}

假设用户输入3.0 4.5 7.26,程序将产生以下结果:

1
You entered: Point(3, 4.5, 7.26)

在这个实现中,我们使用operator=覆盖point中的值。因为operator=是public可用的,所以operator»不需要成为友元函数。


防止部分提取

您可能会期待Point的重载operator»实现得更像这样:

1
2
3
4
5
6
7
8
// 假设这个运算符是Point的友元函数,可以直接修改私有成员
std::istream& operator>> (std::istream& in, Point& point)
{
    // 这版本的代码可能有部分提取的问题
    in >> point.m_x >> point.m_y >> point.m_z;
    
    return in;
}

然而,这种实现可能导致部分提取。考虑一下,如果用户输入“3.0 a b”会发生什么。3.0将被提取到m_x。对m_y和m_z的提取都会失败,这意味着m_y与m_z将被设置为0.0。我们的point会被部分输入值覆盖,部分被零覆盖。

对于Point对象,这可能是一个可接受的结果。但假设我们输入的是分数。提取分母失败会将分母设置为0.0,这可能会在以后导致除以零错误。

因此,最好先保存所有输入,等确认所有输入都成功后,再覆盖对象。


结论

重载operator«和operator»可以让类的屏幕输出和控制台输入变得非常方便。


21.2 使用普通函数重载运算符

上一节

21.4 使用成员函数重载运算符

下一节