4.10 输入输出流程序举例
这部分将介绍几个例子,这些例子使用了本章中讲述的知识。尽管存在很多处理字节的工具(UNIX下的流编辑器,如sed和awk或许是最常用的,而一个文本编辑器也属于此类),但一般来说这些工具有一些局限性。sed和awk可能比较慢,而且只能处理前向序列里的行,并且文本编辑器通常需要人机交互,或至少学习一门专用的宏语言。使用输入输出流编写的程序没有这些限制:具有快速性、可移植性和灵活性。
4.10.1 维护类库的源代码
一般来说,当创建一个类时,读者往往会想到有关库的术语:类的声明在头文件Name.h中定义,而类的成员函数的实现在文件Name.cpp中建立。这些文件有特殊的需求:一个特殊的编码标准(这里的程序使用本教材中的代码格式),而且头文件中的预处理语句能避免类的重复声明。(重复声明使编译器不知道哪个类是程序真正需要的。这些类可能不同,所以编译器会输出一个错误信息。)
这个例子创建一个新的头文件/实现文件对,或修改已存在的一个头文件/实现文件对。如果文件已经存在,则对文件进行检查并修改,如果文件不存在则使用合适的格式创建该文件。
首先注意一个有用的函数startsWith(),这个函数的名字说明了它的功能—如果函数的第1个字符串参数的内容以第2个字符串参数的内容开始(即第2个参数为第1个参数的前缀)时,它返回true。在查找期待的注释及相关的包含语句时使用这个函数。定义了字符串数组part之后,就可使用循环从头至尾对源代码文件中所期待查找的语句序列进行操作。如果源代码文件不存在,则仅将语句写到用已经给出的文件名命名的新文件中。如果文件存在,则每次搜索文件中的一行,并进行校验该行的出现。如果期待查找的语句不存在,则将其插入到源码文件中。需要特别注意的是,要确保不要遗漏已经存在的行(参看代码中使用布尔变量lineUsed的地方)。注意,现在是在对一个已经存在的文件使用stringstream对象,所以能够先写文件的内容至该对象,然后再从该对象中读取和搜索信息。
枚举类型bufs中的有名枚举常量分别是:BASE,用大写字母表示不带扩展名的基本文件名;HEADER,头文件名;IMPLEMENT,实现文件(扩展名为cpp)名;HLINE1,头文件中的第1行基本代码;GUARD1、GUARD2和GUARD3,头文件中的“警戒(guard)”行(防止多重包含);CPPLINE1,cpp文件中的第1行;INCLUDE, cpp文件中包含头文件的语句。
如果运行这个程序但不带任何参数,则会创建下面两个文件:
(这里省略了双斜线后面第1行注释中的冒号,以免混淆本教材中的代码提取符。在由执行cppCheck产生的真正输出中会包含在此省略的冒号。)
通过从文件中删去某些行然后重新执行程序,可以对程序完成的功能进行测试。可以看到每次执行程序后被删除的行会被写回文件。文件被修改后,在文件的第1行会加入字符串“//@//”以使读者注意到文件的变化。再次对文件进行处理前需要去掉这行(否则程序cppcheck执行时会假定原文件的第1行注释丢失)。
4.10.2 检测编译器错误
本教材中设计的所有代码在编译时都不会有错误发生。代码中会引起编译时错误的行,将用特殊的注释符号“//!”进行注释。下面的程序删去了这些特殊的注释,并添加了带有文件编号的注释行。当读者在自己的编译器上编译这些程序时,可能会产生错误信息,对所有文件进行编译时会看到所有文件的文件编号。这个程序在一个特殊文件中附加修改过的行,从而可以很容易地找出任何一个没有产生错误的行。
读者可以用自己选择的标记替换文件中的标记。
程序从每个文件中每次读入一行,然后从这行的开头逐个字符搜索指定的标记;修改这一行并把它放入错误行列表和字符串流对象edited中。当所有的文件处理结束后,关闭文件(到达文件范围末尾),作为输出文件重新打开它,将edited中的内容输出到文件中。注意,计数器也被保存到一个外部文件中。在下一次程序执行时,计数器的计数在上次计数值的基础上增加。
4.10.3 一个简单的数据记录器
这个例子说明了一种可以将数据记录到磁盘,然后检索它以便进行处理的方法。程序想要产生一个多点的海洋温度—深度轮廓图。类DataPoint用来保存数据:
类DataPoint包含一个时间标志,时间标志为头文件<ctime>中定义的time_t类型的值,还有经度和纬度坐标,以及深度和温度值。在这里使用插入符进行格式化操作。下面是文件的实现:
使用函数Coord:toString()是必要的,因为类DataPoint的插入符在输出经度和纬度之前调用了setw()。无论何时对Coord对象使用流插入符,宽度只对第1次插入(即插入数据到Coord:deg)有效,因为宽度改变后总是立即重置。调用函数setf()使得输出浮点数时精度是固定的,函数precision()设置精度为小数点后四位十进制数。请注意,程序中是如何恢复在调用插入符之前设置填充字符和数据精度的。
为得到存储在DataPoint:timestamp中的各个测试点的测试时间,可以调用函数std:localtime(),该函数返回指向tm对象的静态指针。结构tm布局(定义)如下:
1.产生测试数据
这里的程序用来建立一个二进制格式的测试数据文件(使用write()函数),使用DataPoint插入符建立ASCII格式的第2个文件。也可以把这些数据显示到屏幕上,但以文件形式查看更方便。
文件data.txt为ASCII格式、采用顺序方法创建的顺序文件,而文件data.bin为二进制格式文件,构造函数根据标志ios:binary建立此文件。为了说明文本文件采用的格式化形式,这里给出文件data.txt的第1行内容(因为行的长度大于本教材页的宽度,所以进行了换行):
标准C库函数time()用执行该语句的当前时间来更新由函数参数指向的time_t的值,在大多数操作平台上,这个时间是从1970年1月1日00:00:00 GMT(Aquarius(水瓶星座)时代的黎明?)开始的秒的计数值。利用标准C中的库函数srand()作为随机数产生器来设置当前时间也是很方便的方法,这里就是如此。
之后,把timer定时器增加55秒,在各个模拟读操作之间产生有趣的间隔。
各采集点的经度和纬度值采用固定值,表示所采集的数据集是在某个特定的区域。深度和温度值由标准C库函数rand()产生,该函数返回一个0到依赖于操作平台的常量RAND_MAX之间的伪随机数,RAND_MAX常量(一般为所在操作平台的无符号整形最大值)在文件<cstdlib>中定义。为把得到的伪随机数限制在一个期望的合理范围内,使用取模运算符%(从整数相除得到余数)和范围的上限来限定。这些伪随机数都是整数,为了添加小数部分,第2次调用rand()以产生小数,将得到的值加1后取倒数(防止除数为0的错误)。
本程序中,文件data.bin被用作数据容器,尽管这个数据容器存在于磁盘而不是RAM中。函数write()把数据以二进制方式输出到磁盘上。函数的第1个参数是源数据块的起始地址—注意必须将参数设置为char*类型,因为函数write()使用专用流(narrow stream)。第2个参数是要写出的字符数目,在这个例子中就是DataPoint类对象的大小(再一次指明,因为使用的是窄字符流)。因为类DataPoint不含指针,所以输出这个类的对象到磁盘上不会产生问题。如果类对象很复杂,则必须实现串行化(serialization)设计,把指针指向的数据写入磁盘,在以后读回数据时再定义新的指针。(不在本卷中讨论串行化—大部分商业化销售的类库都有一些串行化结构来构造它们。)
2.校验和查看数据
为校验以二进制格式存储的数据的正确性,可以用输入流的成员函数read()将数据读到内存,然后和最初由Datagen.cpp生成的文本文件进行比较。下面的例子仅把格式化的结果输出到cout,但读者可以把这些输出重新送到一个文件中,然后用文件比较“实用程序”来进行校验,校验这个文件与最初的文件是否完全相同: