未格式化的输入/输出操作
到目前为止,我们的程序只使用过格式化IO操作。输入和输出运算符(<<和>>)根据读取或写入的数据类型来格式化它们。输入运算符忽略空白符,输出运算符应用补白、精度等规则。
标准库还提供了一组低层操作,支持未格式化IO(unformattedIO)。这些操作允许我们将一个流当作一个无解释的字节序列来处理。
单字节操作
有几个未格式化操作每次一个字节地处理流。这些操作列在表中,它们会读取而不是忽略空白符。
例如,我们可以使用未格式化IO操作get和put来读取和写入一个字符:
char ch;
while (cin.get(ch))
cout.put (ch);
此程序保留输入中的空白符,其输出与输入完全相同。它的执行过程与前一个使用noskipws的程序完全相同。
is.get (ch) | 从istream is读取下一个字节存入字符ch中。返回is |
os.put (ch) | 将字符ch 输出到ostream os。返回os |
is.get() | 将is的下一个字节作为int返回 |
is.putback(ch) | 将字符ch放回is。返回is |
is.unget() | 将is向后移动一个字节。返回is |
is.peek() | 将下一个字节作为int返回,但不从流中删除它 |
将字符放回输入流
有时我们需要读取一个字符才能知道还未准备好处理它。在这种情况下,我们希望将字符放回流中。
标准库提供了三种方法退回字符,它们有着细微的差别:
- peek 返回输入流中下一个字符的副本,但不会将它从流中删除,peek 返回的值仍然留在流中。
- unget 使得输入流向后移动,从而最后读取的值又回到流中。即使我们不知道最后从流中读取什么值,仍然可以调用unget。
- putback是更特殊版本的unget:它退回从流中读取的最后一个值,但它接受个参数,此参数必须与最后读取的值相同。
一般情况下,在读取下一个值之前,标准库保证我们可以退回最多一个值。即,标准库不材教辅 保证在中间不进行读取操作的情况下能连续调用putback或unget。
从输入操作返回的int值
函数 peek和无参的get版本都以int类型从输入流返回一个字符。这有些令人吃惊,可能这些函数
返回一个char看起来会更自然。
这些函数返回一个int的原因是:可以返回文件尾标记。我们使用char范围中的每个值来表示一个真实字符,因此,取值范围中没有额外的值可以用来表示文件尾。
返回int的函数将它们要返回的字符先转换为unsigned char,然后再将结果提升到int。因此,即使字符集中有字符映射到负值,这些操作返回的int 也是正值。
而标准库使用负值表示文件尾,这样就可以保证与任何合法字符的值都不同。头文件cstdio定义了一个名为EOF的const,我们可以用它来检测从get返回的值是否是文件尾,而不必记忆表示文件尾的实际数值。
对我们来说重要的是,用一个int 来保存从这些函数返回的值:
int ch; //使用一个int,而不是一个char来保存get()的返回值
//循环读取并输出输入中的所有数据
while ((ch = cin.get()) != EOF)
cout,put (ch);
此程序与上面的程序完成相同的工作,唯一的不同是用来读取输入的get版本不同。
多字节操作
一些未格式化IO操作一次处理大块数据。
如果速度是要考虑的重点问题的话,这些操作是很重要的,但类似其他低层操作,这些操作也容易出错。
特别是,这些操作要求我们自己分配并管理用来保存和提取数据的字符数组。
is.get (sink, size, delim) | 从is中读取最多size个字节,并保存在字符数组中,字符数组的起始地址由sink给出。读取过程直至遇到字符delim或读取了size个字节或遇到文件尾时停止。如果遇到了delim,则将其留在输入流中,不读取出来存入sink |
is.getline(sink, size, delim) | 与接受三个参数的get版本类似,但会读取并丢弃delim |
is.read(sink, size) | 读取最多size个字节,存入字符数组sink中。返回is |
is.gcount() | 返回上一个未格式化读取操作从is读取的字节数 |
os.write(source, size) | 将字符数组source中的size个字节写入 os。返回is |
s.ignore(size,delim) | 读取并忽略最多size个字符,包括de11m,与其他未格式化函数不同,lgnore有默认参数:size的默认值为1,delim的默认值为文件尾 |
get 和getline函数接受相同的参数,它们的行为类似但不相同。在两个函数中,sink都是一个char数组,用来保存数据。两个函数都一直读取数据,直至下面条作之一发生
- 已读取了size-1个字符
- 遇到了文件尾
- 遇到了分隔符
两个函数的差别是处理分隔符的方式:get将分隔符留作stream中的下一个字符,getline 则读取并去弃分隔符。 而无论哪个函数都不会将分隔符保存在sink中。
一个常见的错误是本想从流中删除分隔符,但却忘了做。
确定读取了多少个字符
某些操作从输入读取未知个数的字节。我们可以调用gcount来确定最后一个未格式化输入操作读取了多少个字符。
应该在任何后续未格式化输入操作之前调用gcount。
特别是,将字符退回流的单字符操作也属于未格式化输入操作。
如果在调用gcount之前调用了peek、unget 或putback,则gcount的返回值为0。
小心:低层函数容易出错
一般情况下,我们主张使用标准军提供的高层抽象。返回int的IO操作很好地解
释了原因。一个常见的编程错误是将get或peek的返回值赋予一个char而不是一个int。这样做是错误的,但编译器却不能发现这个错误。最终会发生什么依赖于程序运行于哪台机器以及输入数据是什么。
例如,在一台char被实现为unsigned char的机器上,下面的循环永远不会停止:
char ch; //此处使用char就是引入灾难! // 从cin.get返回的值被转换为char,然后与一个int 比较 while ((ch =cin.get()) != EOF) cout.put(ch);
问题出在当get返回EOF时,此值会被转换为一个 unsigned char。转换得到的值与EOF的int值不再相等,因此循环永远也不会停止。
这种错误很可能在调试时发现。
在一台char被实现为 signed char的机器上,我们不能确定循环的行为。当一个越界的值被赋予一个signed变量时会发生什么完全取决于编译器。
在很多机器上,这个循环可以正常工作,除非输入序列中有一个字符与EOF值匹配。虽然在普通数据中这种字符不太可能出现,但低层IO通常用于读取二进制值的场合,而这些二进制值不能直接映射到普通字符和数值。
例如,在我们的机器上,如果输入中包含有一个值为'377'的字符,则循环会提前终止。因为在我们的机器上,将-1转换为一个signed char,就会得到'377'。如果输入中有这个值,则它会被(过早)当作文件尾指示符。
当我们读写有类型的值时,这种错误就不会发生。如果你可以使用标准库提供的类型更加安全、更高层的操作,就应该使用它们。
流随机访问
各种流类型通常都支持对流中数据的随机访问。我们可以重定位流,使之跳过一些数据,首先读取最后一行,然后读取第一行,依此类推。
标准库提供了一对函数,来定位(seek)到流中给定的位置,以及告诉(tell)我们当前位置。
随机IO本质上是依赖于系统的。为了理解如何使用这些特性,你必须查询系统文档。
虽然标准库为所有流类型都定义了seek和tell函数,但它们是否会做有意义的事情依赖于流绑定到哪个设备。
在大多数系统中,绑定到cin、cout、cerr和clog的流不支持随机访问——毕竟,当我们向cout直接输出数据时,类似向回跳十个位置这种操作是没有意义的。
对这些流我们可以调用seek和tell函数, 但在运行时会出错,将流置于一个无效状态。
由于istream和ostream 类型通常不支持随机访问,所以本节剩余内容只适用于fstream和sstream类型。
seek和tell函数
为了支持随机访问,IO类型维护一个标记来确定下一个读写操作要在哪里进行。它们还提供了两个函数:一个函数通过将标记seek到一个给定位置来重定位它;另一个函数tell我们标记的当前位置。
标准库实际上定义了两对seek和tell函数。
一对用于输入流,另一对用于输出流。输入和输出版本的差别在于名字的后缀是g还是p。g版本表示我们正在“获得”(读取)数据,而p版本表示我们正在“放置”(写入)数据。
tellg() tellp() | 返回一个输入流中(tellg)或输出流中(tellp)标记的当前位置 |
seekg (pos) seekp(pos) | 在一个输入流或输出流中将标记重定位到给定的绝对地址。pos通常是前一个tellg或tellp返回的值 |
seekp(off,from) seekg(off,from) | 在一个输入流或输出流中将标记定位到from之前或之后off个seekg(off, from) 字符,from可以是下列值之一
|
从逻辑上讲,我们只能对 istream 和派生自istream 的类型 ifstream和istringstream使用g版本
同样只能对ostream和派生自ostream 的类型 ofstream和ostringstream使用p版本。
fstream或stringstream既能读又能写关联的流,因此对这些类型的对象既能使用g版本又能使用p版本。
只有一个标记
标准库区分seek和tell函数的“放置”和“获得”版本这一特性可能会导致误解。
即使标准库进行了区分,但它在一个流中只维护单一的标记——并不存在独立的读标记和写标记。
当我们处理一个只读或只写的流时,两种版本的区别甚至是不明显的。我们可以对这此流只使用g或只使用p版本。
如果我们试图对一个ifstream流调用tellp,编译器会报告错误。类似的,编译器也不允许我们对一个ostrinastream调用seekg。
fstream和stringstream类型可以读写同一个流。在这些类刑中,有单一的缓冲区用于保存读写的数据,同样,标记也只有一个,表示缓冲区中的当前位置。标准库将g和p版本的读写位置都映射到这个单一的标记。
由于只有单一的标记,因此只要我们在读写操作间切换,就必须进行 seek 操 766Note 作来重定位标记。
重定位标记
seek函数有两个版本:一个移动到文件中的“绝对”地址:另一个移动到一个给定位置的指定偏移量:
//将标记移动到一个固定位置
seekg(new_position);// 将读标记移动到指定的 pos_type 类型的位置
seekp(new_position);// 将写标记移动到指定的pos_type类型的位置// 移动到给定起始点之前或之后指定的偏移位置
seekg(offset,from);// 将读标记移动到距from偏移量为offset的位置
seekp(offset, from);//将写标记移动到距from偏移量为offset的位置
from的可能值如表17.21所示。
参数new position和offset的类型分别是pos_type和off_type,这两个类型都是机器相关的,它们定义在头文件istream和ostream中。pos_type表示一个文件位置,而off_type表示距当前位置的一个偏移量。一个off_type类型的值可以是正的也可以是负的,即,我们可以在文件中向前移动或向后移动。
访问标记
函数tellg和tellp返回一个pos_type值,表示流的当前位置。tell函数通常用来记住一个位置,以便稍后再定位回来:
// 记住当前写位置
Ostringstream writestr;// 输出stringstream
ostringstream::pos_type mark = writestr.tellp();
//...
if (cancelEntry)
// 回到刚才记住的位置
writestr.seekp (mark);
读写同一个文件
我们来考察一个编程实例。假定已经给定了一个要读取的文件,我们要在此文件的末尾写入新的一行,这一行包含文件中每行的相对起始位置。
例如,给定下面文件:
abed
efg
hi
j
程序应该生成如下修改过的文件:
abed
efg
hi
j
5 9 12 14
注意,我们的程序不必输出第一行的偏移——它总是从位置0开始。
还要注意,统计偏移量时必须包含每行末尾不可见的换行符。
最后,注意输出的最后一个数是我们的输出开始那行的偏移量。
在输出中包含了这些偏移量后,我们的输出就与文件的原始内容区分开来了。
我们可以读取结果文件中最后一个数,定位到对应偏移量,即可得到我们的输出的起始地址。
我们的程序将逐行读取文件。对每一行,我们将递增计数器,将刚刚读取的一行的长度加到计数器上,则此计数器即为下一行的起始地址:
int main()
{// 以读写方式打开文件,并定位到文件尾
// 文件模式参数参见8.2.2节(第286页)
fstream inout("copyout",fstream::ate |fstream::in | fstream::out);if (!inout) {
cerr << "Unable to open file!" << endl;
return EXIT_FAILURE;// EXIT FAILURE
}
// inOut以ate模式打开,因此一开始就定义到其文件尾
auto end_mark = inout.tellg(); //记住原文件尾位置
inOut.seekg(0, fstream::beg); // 重定位到文件开始
size_t cnt =0; // 字节数累加器
string line; // 保存输入中的每行
//继续读取的条件:还未遇到错误且还在读取原数据
while (inout && inout. tellg() != end_mark&& getline(inOut, line))( //且还可获取一行输入
cnt += line,size() + 1;
auto mark =inout.tellg(); //加1表示换行符
inOut.seekp(0,fstream::end); // 记住读取位置
inOut << cnt; // 将写标记移动到文件尾
//如果不是最后一行,打印一个分隔符// 输出累计的长度
if (mark != end_mark) inOut <<"";
inOut.seekg (mark);
// 恢复读位置
inOut.seekp(0, fstream::end);
// 定位到文件尾
inOut <<"\n";
return 0; //在文件尾输出一个换行符
}
我们的程序用in、out和ate模式打开fstream。前两个模式指出我们想读写同一个文件。指定 ate会将读写标记定位到文件尾。与往常一样,我们检查文件是否成功打开,如果失败就退出
由于我们的程序向输入文件写入数据,因此不能通过文件尾来判断是否停止读取,而是应该在达到原数据的末尾时停止。
因此,我们必须首先记住原文件尾的位置。由于我们是以ate模式打开文件的,因此inOut已经定位到文件尾了。我们将当前位置(即,原文件尾)保存在end_mark中。记住文件尾位置之后,我们seek 到距文件起始位置偏移量为0的地方,即,将读标记重定位到文件起始位置。
while循环的条件由三部分组成:首先检查流是否合法:如果合法,通过比较当前读位置(由tellg返回)和记录在end mark中的位置来检查是否读完了原数据;最后,假定前两个检查都已成功,我们调用getline读取输入的下一行,如果getline成功,则执行 while循环体。
循环体首先将当前位置记录在mark中。我们保存当前位置是为了在输出下一个偏移量后再退回来。接下来调用seekp将写标记重定位到文件尾。我们输出计数器的值,然后调用 seekg 回到记录在mark中的位置。回退到原位置后,我们就准备好继续检查循环条件了。
每步循环都会输出下一行的偏移量。因此,最后一步循环负责输出最后一行的偏移量但是,我们还需要在文件尾输出一个换行符。与其他写操作一样,在输出换行符之前我们调用 seekp来定位到文件尾。