重载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»可以让类的屏幕输出和控制台输入变得非常方便。