4.9 操纵算子
从前面的程序可以看出,调用成员函数进行流的格式化操作有些冗长乏味。为使读操作和写操作更容易,C++语言提供了操纵算子的集合,这些操纵算子可以实现与相应的成员函数相同的功能。操纵算子使用起来更方便,因为可以在一个表达式中插入它们;不需要单独的函数调用语句。
操纵算子改变流的状态而不是(或同时)处理数据。例如,当在一个输出表达式中插入endl时,不但在流中插入了一个换行符,而且刷新了流(即,将存储在流内部缓冲区中但还未真正输出的所有字符输出)。也可像这样刷新流:
这引起调用成员函数flush()的副作用,即
(没在流中插入任何东西。)其他的基本操纵算子改变数的基数为oct(八进制)、dec(十进制)或hex(十六进制):
既然这样,以后的数字输出将继续保持为十六进制模式,直至修改它。通过在输出流中插入dec或oct来改变这种模式。
也存在一种针对提取操作的操纵算子,它可以“吃掉”空格:
不带参数的操纵算子在头文件<iostream>中定义。包括dec、oct和hex,分别对应于setf(ios:dec, ios:basefield)、setf(ios:oct, ios:basefield)和setf(ios:hex, ios:basefield),但前者更简洁。在头文件<iostream>中也包含ws、endl和flush以及在这里说明的其他操纵算子:
4.9.1 带参数的操纵算子
有6个标准的带参数的操纵算子,如setw()。这些操纵算子在头文件<iomanip>中定义,下表对这些操纵算子做了总结:
如果程序中大量使用流格式化操作,则可以发现使用操纵算子代替调用流类成员函数可以简化代码。这里的例子用操纵算子重写了前面的程序。(程序中删除了D()宏,使得代码更容易阅读。)
读者可以看到,在这个程序中有多处地方,将多条语句合并到一条链式表达的插入语句中。注意对函数setiosflags()的调用,其参数为几个格式化标志的按位或。前一例子中相同的功能由setf()和unsetf()实现。
对输出流使用函数setw()时,输出表达式被格式化输出到一个临时串,格式化串的长度与传递给setw()的参数相比较,根据比较结果决定是否需要用当前填充字符填补空余位置。换言之,setw()影响格式化输出操作的结果字符串。同样,对输入流使用setw()函数进行设置,只在读字符串时有意义,下面的例子很清楚地说明了这一点:
如果试图读一个字符串,函数setw()准确地控制着提取字符的数目,读取过程遇到小数点时结束。第1次提取获得了两个字符,而第2次提取仅获得了一个字符,尽管将读取数目设置为两个。这是因为operator>>()使用空格作为界定符(除非关闭skipws标志)。然而,当试图读取一个数字时,例如读取x,不能用setw()来限定读取字符的个数。对于输入流,setw()只能用于字符串的提取。
4.9.2 创建操纵算子
有时,读者喜欢创建自己的操纵算子,而且创建过程也相当简单。不带参数(零参数,zero-argument)的操纵算子是仅一个函数,例如endl,它只是以ostream对象的引用为参数,返回值为一个ostream对象的引用的函数。endl的声明为:
现在,当执行语句
时,endl将产生该函数的地址。编译器会问,“是否存在一个函数,它以一个函数的地址作为参数?”头文件<i o s t r e a m>中的预定义函数负责这项工作;这些函数称为应用算子(applicator)(因为它们将一个函数应用到流类)。应用算子调用它的函数参数,并传递ostream对象作为自己的参数。在这里,不需要知道应用算子是如何创建操纵算子的;仅需知道操纵算子的存在。这里有一个(简化的)ostream应用算子的代码:
实际的定义因涉及模板会更复杂一些,这行代码说明了这项技术。当一个函数,如*pf(以流作为参数,返回流的引用),被插入到一个流中时,调用上面的应用算子函数,之后执行pf指针指向的函数。ios_base、basic_ios、basic_ostream和basic_istream的应用算子在标准C++库中预定义。
这里有一个比较简明的例子解释了上面所描述的过程,例子中创建了一个叫做nl的操纵算子,它的作用是在流中插入换行符(也就是说,这个操纵算子不刷新流,不像endl那样):
当插入nl到一个输出流如cout时,调用顺序为:
表达式
在函数nl()内部调用ostream:operator(char),它返回一个流对象,这个流对象最终从nl()返回。[1]
4.9.3 效用算子
读者已经看到,零参数的操纵算子很容易创建。但是如何创建带参数的操纵算子呢?如果研究头文件<iomanip>,就会发现一种称作smanip的类型,它返回带参数的操纵算子。读者也许试图仿照smanip定义自己的带参数的操纵算子,但是请不要这样做。因为类型smanip是依赖于系统实现的,所以不具备可移植性。幸运的是,可以使用由Jerry Schwarz提出的叫做效用算子(effector)的技术直接定义独立于机器实现的操纵算子。[2]一个效用算子是一个简单的类,该类的构造函数可以格式化一个字符串,这个字符串描述了读者希望的操作,并将这个字符串连同重载的operator<<一起插入到流中。这里有一个含有两个效用算子的程序例子。第1个效用算子输出一个截断的字符串,第2个效用算子以二进制方式输出一个数。
类Fixw的构造函数创建char*参数的一个被截短的拷贝,由析构函数释放创建拷贝时分配的内存。重载运算符operator<<把第2个参数的内容即Fixw对象插入到第1个参数即ostream对象中,然后返回ostream对象,所以它能够在一个链式表达式中使用。当在一个表达式中使用Fixw时,如下所示:
该语句调用类Fixw的构造函数创建一个Fixw临时对象,这个临时对象被传给operator<<。它的作用相当于带参数的操纵算子。临时Fixw对象在这条语句结束前将一直存在。
Bin效用算子依赖这样一个事实:右移无符号数字时在二进制数的高位补零。可以使用numeric_limits<unsigned long>:max()(产生unsigned long数的最大值,在标准头文件<limits>中定义)利用高位集产生一个值,并且这个值从头至尾进行位移用来询问被测试的数字(通过右移),依次屏蔽每一位。为了具有可读性,现在已经将代码中的字符串文字并列;编译器会将分开的这些字符串合并到一个字符串中。
这项技术历来存在的问题是:一旦为char*创建了Fixw对象或为unsigned long创建了Bin对象,就不允许再为Fixw类或Bin类创建不同的类对象。然而,使用名字空间后这个问题就不存在了。效用算子和操纵算子并不等同,尽管它们可以用来解决相同的问题。如果发现某个问题使用效用算子不能解决,就需要克服操纵算子的复杂性。
[1]在把nl定义到头文件之前,使之成为inline(内联)函数。
[2]Jerry Schwarz是输入输出流的设计者。