必备技能11.3:重载输入输出运算符
在前面的篇章中,当我们需要对和类相关的数据进行输入或者输出的时候,我们会使用成员函数来专门输入或者是输出类的数据。这种方式本身并没有什么不对的地方,但是C++为我们提供了更好的方式来对类的数据进行输入或者输出:重载>>或者<<运算符。
在C++中,输出运算符<<也被称为是插入运算符,因为它是被用来把数据插入到流中的。类似的,输入运算符>>也被称为是提取运算符,因为它是被用来从流中提取数据的。
在<iostream>中,插入运算符和提取运算已经被针对所有的内置数据类型进行了重载。下面我们来看看如何针对自己创建的类来定义这两个运算符。
创建插入运算
作为第一个简单的示例,我们来为ThreeD类创建插入运算符:
class ThreeD
{
public:
int x, y,z;
ThreeD(int a, int b, int c)
{
x = a; y = b; z = c;
}
};
下面通过重载来实现对 ThreeD 对象插入运算。下面就是一种实现方法:
ostream &operator<<(ostream &stream, ThreeD obj)
{
stream << obj.x << ", ";
stream << obj.y << ", ";
stream << obj.z << "\n";
return stream; //返回该流
};
由于上面函数中的许多特性对所有的插入函数都是适用的,所以我们将仔细讨论一下上面的这个函数。首先,函数的返回值是一个ostream类对象的引用。这样做是必须的,是为了能在复合输入输出语句中可以和多个该类型的插入运算复合使用。接下来我们看到这个函数需要两个参数。其中第一个是出现在<<运算符左侧的流的引用。第二个是出现在<<运算符右侧的对象。(如果我们愿意,第二个参数也可以是对某个对象的引用。)在函数的实现体中,把ThreeD类的对象的三个数值都进行了输出,并且返回了流对象。
下面是一个简单的用来演示插入运算的程序:
//演示自定义的插入运算符
#include <iostream>
using namespace std;
class ThreeD
{
public:
int x, y,z;
ThreeD(int a, int b, int c)
{
x = a; y = b; z = c;
}
};
//显示X,Y,Z坐标-----ThreeD类的插入运算
ostream &operator<<(ostream &stream, ThreeD obj)
{
stream << obj.x << ", ";
stream << obj.y << ", ";
stream << obj.z << "\n";
return stream; //返回该流
};
int main()
{
ThreeD a(1,2,3), b(3,4,5), c(5,6,7);
cout << a << b << c;
return 0;
}
上面的程序输入如下:
1, 2, 3
3, 4, 5
5, 6, 7
如果我们删掉上面程序中针对ThreeD类的处理,就剩下了插入函数的框架如下:
ostream &operator<<(ostream &stream, class_type obj)
{
//详细的和类相关的处理代码
return stream; //返回该流
};
其中obj也可以采用传递引用的方式。
尽管在插入函数中具体要做些什么完全是由我们程序员决定的。但是,让插入函数进行合理的输出才是好的编程习惯。这里唯一确定的就是插入函数返回的是流。
使用友元函数来进行插入运算的重载
在上面示例程序中,被重载的插入运算函数并不是ThreeD类的成员函数。实际上,插入函数和提取函数都是不能作为类的成员函数的。其原因在于,如果一个运算符是类的成员,那么运算符左侧的操作数就应该是该类的对象。这点是不可改变的。然而,当对插入运算进行重载的时候,其左侧的运算数是流,而其右侧的运算数才是类的对象。因此,重载的插入运算只能是作为类的非成员函数。
上面说到的插入运算只能是以非成员函数的方式进行重载这一事实就引发了一个严重的问题:重载的插入运算怎么能访问类的私有成员了?在上面的示例程序中,变量x,y,z都是公有的,这样插入函数是可以访问它们的。但是面向对象的编程中重要的一点就是信息隐藏。因此上述强制地把所有数据都声明是共有的做法显然和这点相悖。这个问题的解决就要用到友元函数:插入函数作为类的友元函数。作为它所定义的类的友元函数,插入函数可以访问到类的私有数据。下面重写了前面的示例程序和ThreeD类。其中把对插入函数的重载是作为类的友元函数来实现的:
//演示自定义的插入运算符
#include <iostream>
using namespace std;
class ThreeD
{
//私有的数据
int x, y,z;
public:
ThreeD(int a, int b, int c)
{
x = a; y = b; z = c;
}
//插入运算符作为该类的友元函数
friend ostream &operator<<(ostream & stream, ThreeD obj);
};
//显示X,Y,Z坐标-----ThreeD类的插入运算
ostream &operator<<(ostream &stream, ThreeD obj)
{
stream << obj.x << ", ";
stream << obj.y << ", ";
stream << obj.z << "\n";
return stream; //返回该流
};
int main()
{
ThreeD a(1,2,3), b(3,4,5), c(5,6,7);
cout << a << b << c;
return 0;
}
注意,其中的x,y,z现在都是类的私有成员,但是在插入函数中仍然可以被直接访问。这样把插入运算(和提取运算)作为它所定义的类的友元函数就保持了面向对象编程的封装原则。
重载提取运算
提取运算的重载和插入运算的重载是相同的。例如,下面的提取函数就把三维坐标输入到了ThreeD的对象中。注意,其中有提示用户进行输入。
istream &operator>>(istream &stream, ThreeD &obj)
{
cout << "Enter x, y, z values:\n";
stream >> obj.x >> obj.y >> obj.z;
return stream;
}
下面的程序演示了针对ThreeD类对象的提取运算:
//演示自定义的插入运算符
#include <iostream>
using namespace std;
class ThreeD
{
//私有的数据
int x, y,z;
public:
ThreeD(int a, int b, int c)
{
x = a; y = b; z = c;
}
//插入运算符作为该类的友元函数
friend ostream &operator<<(ostream &stream, ThreeD obj);
friend istream &operator>>(istream &stream, ThreeD &obj);
};
//显示X,Y,Z坐标-----ThreeD类的插入运算
ostream &operator<<(ostream &stream, ThreeD obj)
{
stream << obj.x << ", ";
stream << obj.y << ", ";
stream << obj.z << "\n";
return stream; //返回该流
};
//输入三维坐标
istream &operator>>(istream &stream, ThreeD &obj)
{
cout << "Enter x, y, z values:\n";
stream >> obj.x >> obj.y >> obj.z;
return stream;
}
int main()
{
ThreeD a(1,2,3);
cout << a;
cin >> a;
cout <<a ;
return 0;
}
上面程序的运行如下:
1, 2, 3
Enter x, y, z values:
5 6 7
5, 6, 7
和插入运算类似,提取运算函数也是不能作为操作对象类的成员函数的。它只能是友元函数或者是独立的函数。
除了必须返回一个对istream对象的引用外,我们可以在提取运算函数中做任何自己想做的事情。但是为了程序的结构清晰,我们最好还是只让它完成输入操作即可。
格式化的输入输出
到目前为止,信息的输入或者是输出都是按照C++输入输出系统提供的缺省格式进行的。然而,我们是可以通过下面两种方式之一来控制数据的格式的:第一就是使用ios类的成员函数。第二种方式就是使用一种特殊的函数叫做控制器。
练习:
1. 什么是插入运算?
2. 什么是提取运算?
3. 为什么在插入运算或者提取运算的中经常要用到友元函数?