4.8 输出流的格式化

输入输出流的设计目标是使用户易于移动和/或格式化字符。如果不能用C语言中提供的printf()及相关函数完成大部分格式化操作,输入输出流将不会有多大用处。在这部分,读者可以学习输入输出流类所有的格式化输出函数,之后可以按照自己的意愿格式化输出数据。

开始学习输入输出流的格式化输出函数的时候可能会引起混淆,因为存在不止一种控制格式化输出的方法:通过成员函数和运算符。之后可能遇到的会引起混淆的地方有:设置状态标志来控制格式化的一般成员函数,如左对齐或右对齐;在十六进制表示法中使用大写字母;始终使用小数点表示浮点数的值等。另一方面,个别的成员函数用来设置和读取填充字符、域宽和精度的值。

为对这些函数进行分类,首先应检查输入输出流的内部格式化数据,以及能够修改这些数据的成员函数。(如果想这样做的话,用成员函数就可以进行所有的操作)。操作符将单独讨论。

4.8.1 格式化标志

类ios包含一些数据成员,用于存储与流有关的所有格式化信息。其中一些数据存储在变量中,其值有一个变动范围:浮点数精度、输出域宽和用于填充输出的字符(一般为空格)。有关格式化的其他信息由格式化标志决定,为了节省空间,通常多个标志组合在一起使用。成员函数ios:flags()可以得到格式化标志所代表的值,这个函数没有参数,函数的返回值为包含当前格式化标志的fmtflags(一般被认为是long的同义词)对象。其他函数用于改变格式化标志并返回格式化标志的原有值。

4.8 输出流的格式化 - 图1

第1个函数强制改变程序需要的所有标志。但更多的时候,是使用其他3个函数,每次改变一个标志。

函数setf()的使用似乎会引起一些混淆。要想知道该使用那个版本的重载函数,就需要知道将要改变的格式化标志的类型。有两种类型的格式化标志:一类是仅被设置为开或关的开/关标志;另一类标志和其他的标志联合使用。开/关标志最简单且容易理解,使用setf(fmtflags)函数打开标志,使用unsetf(fmtflags)函数关闭标志。这些标志在下面的表中说明:

4.8 输出流的格式化 - 图2

例如,为了在cout中显示正号,可以使用语句cout.setf(ios:showpos)。如果使用了cout.unsetf(ios:showpos),则不再显示正号。

unitbuf标志控制着单元缓冲区(unit buffering),这意味着每次在输出流中插入数据后都会立即刷新该输出流。这对于错误跟踪很方便,当程序崩溃时,数据仍然会保存到日志文件中。下面的程序演示了单元缓冲区:

4.8 输出流的格式化 - 图3

在插入任何数据到流对象之前都需要打开单元缓冲区。当把setf()注释掉(comment out)时,某个特殊的编译器仅写入了一个字母'o'到文件log.txt。而使用单元缓冲区则不会丢失任何数据。

默认情况下,标准错误输出流cerr的单元缓冲区处于打开状态。使用单元缓冲会导致大量的系统开销,所以如果程序中频繁使用输出流,则不要使用单元缓冲区,除非不需要考虑程序的执行效率。

4.8.2 格式化域

第2类格式化标志是分组使用的。一次只能设置这些标志中的一个,就像老式汽车收音机上的按钮一样—当按下其中一个按钮时,其他按钮会弹起来。遗憾的是,标志的这种互斥设置不能自动地完成,编程人员必须注意将要设置什么标志,以防错误地使用了setf()函数。例如,对于每一种基数(number base)都有相应的标志:十六进制(hexadecimal)、十进制(decimal)和八进制(octal)。这些标志统称为ios:basefield。如果已经设置了ios:dec标志这时又调用setf(ios:hex),会设置ios:hex标志却不会清除ios:dec标志位,从而导致不确定的行为。作为前一函数的替代,可以调用第2种形式的setf()函数setf(ios:hex, ios:basefield)。这个函数首先清除ios:basefield域中的所有位,然后设置ios:hex标志。这种形式的setf()在设置一种标志的时候会确保同组的其他标志被“清除”。ios:hex操纵算子自动地完成所有工作,因此不需要关心类的内部实现细节,甚至不需要关心ios:basefield是一个二进制标志的集合。接下来,读者将会看到多种操纵算子,在所有使用setf()的地方提供相同的功能。

下面是这些标志及其作用:

4.8 输出流的格式化 - 图4

4.8 输出流的格式化 - 图5

4.8 输出流的格式化 - 图6

4.8.3 宽度、填充和精度设置

用来控制输出域(字段)的宽度、填补输出域的填充字符和显示浮点数精度的内部变量,由与其同名的成员函数进行读写。

4.8 输出流的格式化 - 图7

fill和precision的值是相当直观的,但width的值就需要解释一下。当宽度为0时,在流中插入一个值的结果是,生成表示这个值所需的最少字符数。宽度为正的意思是,在流中插入一个数,至少会产生宽度所规定数量的字符;如果插入字符的个数小于宽度值,则用填充字符填充空余位置。然而,输出的值永远都不会被截断,所以如果试图在宽度为2时打印123,仍会得到123。宽度只能指定输出字符的最小数目;没有设定输出字符最大数目的方法。

宽度还有一个明显的不同,因为每个插入符或提取符都可能受它的值的影响,所以它被每个插入符或提取符隐含自动重置为0。它不是一个真正的状态变量,而是插入符和提取符的隐含参数。如果想得到固定不变的宽度,需要在每次使用插入符或提取符之后调用width()。

4.8.4 一个完整的例子

为了使读者明白如何使用前面讨论过的所有函数,这里给出一个调用了所有这些函数的例子:

4.8 输出流的格式化 - 图8

4.8 输出流的格式化 - 图9

这个例子中用了一种技巧来创建一个跟踪文件,以监视程序执行时发生了什么事情。宏D(a)用预处理器(程序)把a转换成串并显示。然后对a进行重复迭代,所以语句顺次执行。宏把所有信息输出到跟踪文件T。程序的输出为:

4.8 输出流的格式化 - 图10

4.8 输出流的格式化 - 图11

研究这个输出文件可以使读者对输入输出流的格式化成员函数理解得更清晰、明确。