章节目录

重载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类的运算符«的实现相当简单——因为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重载操作符« 函数,该函数返回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)

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


重载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)

在这个实现中,我们使用操作符=来覆盖point中的值。因为operator=是public可用的,这意味着不需要operator»成为友元函数。


防止部分提取

您可能期望看到Point的重载操作符»实现得更像这样:

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 使用成员函数重载运算符

下一节