IO 类型在三个独立的头文件中定义:
Header Type
iostream istream 从流中读取 ostream 写到流中去 iostream 对流进行读写;从 istream 和 ostream 派生而来 fstream ifstream 从文件中读取;由 istream 派生而来 ofstream 写到文件中去;由 ostream 派生而来 fstream 读写文件;由 iostream 派生而来 sstream istringstream 从 string 对象中读取;由 istream 派生而来 ostringstream 写到 string 对象中去;由 ostream 派生而来 stringstream 对 string 对象进行读写;由 iostream 派生而来
出于某些原因,标准库类型不允许做复制或赋值操作。
1 | ofstream out1, out2; |
这个要求有两层特别重要的含义:
1 | ofstream &print(ofstream&); // ok: takes a reference, no copy |
一般情况下,如果要传递 IO 对象以便对它进行读写,可用非 const 引用的方式传递这个流对象。对 IO 对象的读写会改变它的状态,因此引用必须是非 const 的。
fstream 头文件定义了三种支持文件 IO 的类型:
这些类型都由相应的 iostream 类型派生而来,这个事实意味着我们已经知道使用 fstream 类型需要了解的大部分内容了。特别是,可使用 IO 操作符 <<
和 >>
在文件上实现格式化的 IO,而且在前面章节介绍的条件状态也同样适用于 fstream 对象。
fstream 类型除了继承下来的行为外,还定义了两个自己的新操作—— open 和 close,以及形参为要打开的文件名的构造函数。fstream、ifstream 或 ofstream 对象可调用这些操作,而其他的 IO 类型则不能调用。
1 | // construct an ifstream and bind it to the file named ifile |
上述代码定义并打开了一对 fstream 对象。infile 是读的流,而 outfile 则是写的流。为 ifstream 或者 ofstream 对象提供文件名作为初始化式,就相当于打开了特定的文件。
下面的代码与上述代码等效:
1 | ifstream infile; // unbound input file stream |
由于历史原因,IO 标准库使用 C 风格字符串而不是 C++ strings 类型的字符串作为文件名。在创建 fstream 对象时,如果调用 open 或使用文件名作初始化式,需要传递的实参应为 C 风格字符串,而不是标准库 strings 对象。程序常常从标准输入获得文件名。通常,比较好的方法是将文件名读入 string 对象,而不是 C 风格字符数组。假设要使用的文件名保存在 string 对象中,则可调用 c_str
成员获取 C 风格字符串。
例如,假设 ifile 和 ofile 是存储希望读写的文件名的 strings 对象,可如下编写代码:
1 | infile.open(str1.c_str()); // open file named "in" in the current directory |
打开文件后,通常要检验打开是否成功,这是一个好习惯:
1 | // check that the open succeeded |
这个条件与之前测试 cin 是否到达文件尾或遇到某些其他错误的条件类似。检查流等效于检查对象是否“适合”输入或输出。如果打开(open)失败,则说明 fstream 对象还没有为 IO 做好准备。当测试对象
1 | if (outfile) // ok to use outfile? |
返回 true 意味着文件已经可以使用。由于希望知道文件是否未准备好,则对返回值取反来检查流:
1 | if (!outfile) // not ok to use outfile? |
fstream 对象一旦打开,就保持与指定的文件相关联。如果要把 fstream 对象与另一个不同的文件关联,则必须 先关闭 (close)现在的文件,然后 再打开 (open)另一个文件:要点是在尝试打开新文件之前,必须先关闭当前的文件流。open 函数会检查流是否已经打开。如果已经打开,则设置内部状态,以指出发生了错误。
接下来使用文件流的任何尝试都会失败。
1 | ifstream infile("in"); // opens file named "in" for reading |
在打开文件时,无论是调用 open 还是以文件名作为流初始化的一部分,都需指定文件模式(file mode)。每个 fstream 类都定义了一组表示不同模式的值,用于指定流打开的不同模式。与条件状态标志一样,文件模式也是整型常量,在打开指定文件时,可用位操作符设置一个或多个模式。文件流构造函数和 open 函数都提供了默认实参设置文件模式。默认值因流类型的不同而不同。此外,还可以显式地以模式打开文件。
in
打开文件做读操作
out
打开文件做写操作
app
追加模式:在每次写之前找到文件尾
ate
打开文件后立即将文件定位在文件尾
trunc
清空模式:打开文件时清空已存在的文件流
binary
以二进制模式进行 IO 操作
并不是所有的打开模式都可以同时指定。有些模式组合是没有意义的,例如同时以 in 和 trunc 模式打开文件,准备读取所生成的流,但却因为 trunc 操作而导致无数据可读。下表列出了有效的模式组合及其含义。
out
打开文件做写操作,删除文件中已有的数据
out | app
打开文件做写操作,在文件尾写入
out | trunc
与 out 模式相同
in
打开文件做读操作
in | out
打开文件做读、写操作,并定位于文件开头处
in | out | trunc
打开文件做读、写操作,删除文件中已有的数据
out、trunc 和 app 模式只能用于指定与 ofstream 或 fstream 对象关联的文件;in 模式只能用于指定与 ifstream 或 fstream 对象关联的文件。所有的文件都可以用 ate 或 binary 模式打开。ate 模式只在打开时有效:文件打开后将定位在文件尾。以 binary 模式打开的流则将文件以字节序列的形式处理,而不解释流中的字符。
默认时,与 ifstream 流对象关联的文件将以 in 模式打开,该模式允许文件做读的操作:与 ofstream 关联的文件则以 out 模式打开,使文件可写。以 out 模式打开的文件会被清空:丢弃该文件存储的所有数据。
对于用 ofstream 打开的文件,要保存文件中存在的数据,唯一方法是显式地指定 app 模式打开:
1 |
|
outfile2 的定义使用了按位或操作符将相应的文件同时以 out 和 trunc 模式打开。
fstream 对象既可以读也可以写它所关联的文件。fstream 如何使用它的文件取决于打开文件时指定的模式。
默认情况下,fstream 对象以 in 和 out 模式同时打开。 当文件同时以 in 和 out 打开时不清空 。如果打开 fstream 所关联的文件时,只使用 out 模式,而不指定 in 模式,则文件会清空已存在的数据。如果打开文件时指定了 trunc 模式,则无论是否同时指定了 in 模式,文件同样会被清空。下面的定义将 copyOut 文件同时以输入和输出的模式打开:
1 | // open for input and output |
我们编写一个名为 open_file
的函数实现这个功能。这个函数有两个引用形参,分别是 ifstream 和 string 类型,其中 string 类型的引用形参存储与指定 ifstream 对象关联的文件名:
1 | // opens in binding it to the given file |
由于不清楚流 in 的当前状态,因此首先调用 close 和 clear 将这个流设置为有效状态。然后尝试打开给定的文件。如果打开失败,流的条件状态将标志这个流是不可用的。最后返回流对象 in,此时,in 要么已经与指定文件绑定起来了,要么处于错误条件状态。
标准库定义了三种类型的字符串流:
要使用上述类,必须包含 sstream 头文件。
与 fstream 类型一样,上述类型由 iostream 类型派生而来,这意味着 iostream 上所有的操作适用于 sstream 中的类型。sstream 类型除了继承的操作外,还各自定义了一个有 string 形参的构造函数,这个构造函数将 string 类型的实参复制给 stringstream 对象。对 stringstream 的读写操作实际上读写的就是该对象中的 string 对象。这些类还定义了名为 str 的成员,用来读取或设置 stringstream 对象所操纵的 string 值。
stringstream strm; 创建自由的 stringstream 对象 stringstream strm(s); 创建存储 s 的副本的 stringstream 对象,其中 s 是 string 类型的对象 strm.str() 返回 strm 中存储的 string 类型对象 strm.str(s) 将 string 类型的 s 复制给 strm,返回 void
前面已经见过以每次一个单词或每次一行的方式处理输入的程序。第一种程序用 string 输入操作符,而第二种则使用 getline 函数。然而,有些程序需要同时使用这两种方式:有些处理基于每行实现,而其他处理则要操纵每行中每个单词。可用 stringstreams 对象实现:
1 | string line, word; // will hold a line and word from input, respectively |
这里,使用 getline 函数从输入读取整行内容。然后为了获得每行中的单词,将一个 istringstream 对象与所读取的行绑定起来,这样只需要使用普通的 string 输入操作符即可读出每行中的单词。
stringstream 对象的一个常见用法是, 需要在多种数据类型之间实现自动格式化时使用该类类型。例如,有一个数值型数据集合,要获取它们的 string 表示形式,或反之。sstream 输入和输出操作可自动地把算术类型转化为相应的 string 表示形式,反过来也可以。
1 | int val1 = 512, val2 = 1024; |
这里创建了一个名为 format_message
的 ostringstream 类型空对象,并将指定的内容插入该对象。重点在于 int 型值自动转换为等价的可打印的字符串。format_message
的内容是以下字符:
1 | val1: 512\nval2: 1024 |
相反,用 istringstream 读 string 对象,即可重新将数值型数据找回来。读取 istringstream 对象自动地将数值型数据的字符表示方式转换为相应的算术值。
1 | // str member obtains the string associated with a stringstream |
这里使用 .str 成员获取与之前创建的 ostringstream 对象关联的 string 副本。再将 input_istring
与 string 绑定起来。在读 input_istring
时,相应的值恢复为它们原来的数值型表示形式
为了读取 input_string
,必须把该 string 对象分解为若干个部分。我们要的是数值型数据;为了得到它们,必须读取(和忽略)处于所需数据周围的标号。
IO 标准库管理一系列条件状态(condition state)条件状态(condition state)成员,用来标记给定的 IO 对象是否处于可用状态,或者碰到了哪种特定的错误。下列出了标准库定义的一组函数和标记,提供访问和操纵流状态的手段。
strm::iostate
机器相关的整型名,由各个 iostream 类定义,用于定义条件状态
strm::badbit
strm::iostate 类型的值,用于指出被破坏的流
strm::failbit
strm::iostate 类型的值,用于指出失败的 IO 操作
strm::eofbit
strm::iostate 类型的值,用于指出流已经到达文件结束符
s.eof()
如果设置了流 s 的 eofbit 值,则该函数返回 true
s.fail()
如果设置了流 s 的 failbit 值,则该函数返回 true
s.bad()
如果设置了流 s 的 badbit 值,则该函数返回 true
s.good()
如果流 s 处于有效状态,则该函数返回 true
s.clear()
将流 s 中的所有状态值都重设为有效状态
s.clear(flag)
将流 s 中的某个指定条件状态设置为有效。flag 的类型是 strm::iostate
s.setstate(flag)
给流 s 添加指定条件。flag 的类型是 strm::iostate
s.rdstate()
返回流 s 的当前条件,返回值类型为 strm::iostate
许多程序只需知道是否有效。而某些程序则需要更详细地访问或控制流的状态,此时,除了知道流处于错误状态外,还必须了解它遇到了哪种类型的错误。例如,程序员也许希望弄清是到达了文件的结尾,还是遇到了 IO 设备上的错误。 所有流对象都包含一个条件状态成员,该成员由 setstate 和 clear 操作管理。这个状态成员为 iostate 类型,这是由各个 iostream 类分别定义的机器相关的整型。该状态成员以二进制位(bit)的形式使用。
每个 IO 类还定义了三个 iostate 类型的常量值,分别表示特定的位模式。这些常量值用于指出特定类型的 IO 条件,可与位操作符一起使用,以便在一次操作中检查或设置多个标志。 badbit 标志着系统级的故障,如无法恢复的读写错误。如果出现了这类错误,则该流通常就不能再继续使用了。如果出现的是可恢复的错误,如在希望获得数值型数据时输入了字符,此时则设置 failbit 标志,这种导致设置 failbit 的问题通常是可以修正的。eofbit 是在遇到文件结束符时设置的,此时同时还设置了 failbit。
流的状态由 bad、fail、eof 和 good 操作提示。如果 bad、fail 或者 eof 中的任意一个为 true,则检查流本身将显示该流处于错误状态。类似地,如果这三个条件没有一个为 true,则 good 操作将返回 true。 clear 和 setstate 操作用于改变条件成员的状态。clear 操作将条件重设为有效状态。在流的使用出现了问题并做出补救后,如果我们希望把流重设为有效状态,则可以调用 clear 操作。使用 setstate 操作可打开某个指定的条件,用于表示某个问题的发生。除了添加的标记状态,setstate 将保留其他已存在的状态变量不变。
clear 和 setstate 操作用于改变条件成员的状态。clear 操作将条件重设为有效状态。在流的使用出现了问题并做出补救后,如果我们希望把流重设为有效状态,则可以调用 clear 操作。使用 setstate 操作可打开某个指定的条件,用于表示某个问题的发生。除了添加的标记状态,setstate 将保留其他已存在的状态变量不变。
可以如下管理输入操作
1 | int ival; |
考虑这样的程序,它有一个 vector 对象,包含一些要打开并读取的文件名,程序要对每个文件中存储的单词做一些处理。假设该 vector 对象命名为 files,程序也许会有如下循环:
1 | // for each file in the vector |
每一次循环都构造了名为 input 的 ifstream 对象,打开并读取指定的文件。构造函数的初始化式使用了箭头操作符 对 it 进行解引用,从而获取 it 当前表示的 string 对象的 c_str
成员。文件由构造函数打开,并假设打开成功,读取文件直到到达文件结束符或者出现其他的错误条件为止。
在这个点上,input 处于错误状态。任何读 input 的尝试都会失败。因为 input 是 while 循环的局部变量,在每次迭代中创建。这就意味着它在每次循环中都以干净的状态即 input.good() 为 true,开始使用。
如果希望避免在每次 while 循环过程中创建新流对象,可将 input 的定义移到 while 之前。这点小小的改动意味着必须更仔细地管理流的状态。如果遇到文件结束符或其他错误,将设置流的内部状态,以便之后不允许再对该流做读写操作。关闭流并不能改变流对象的内部状态。如果最后的读写操作失败了,对象的状态将保持为错误模式,直到执行 clear 操作重新恢复流的状态为止。调用 clear 后,就像重新创建了该对象一样。
如果打算重用已存在的流对象,那么 while 循环必须在每次循环进记得关闭(close)和清空(clear)文件流:
1 | ifstream input; |
如果忽略 clear 的调用,则循环只能读入第一个文件。要了解其原因,就需要考虑在循环中发生了什么:首先打开指定的文件。假设打开成功,则读取文件直到文件结束或者出现其他错误条件为止。在这个点上,input 处于错误状态。如果在关闭(close)该流前没有调用 clear 清除流的状态,接着在 input 上做的任何输入运算都会失败。一旦关闭该文件,再打开下一个文件时,在内层 while 循环上读 input 仍然会失败——毕竟最后一次对流的读操作到达了文件结束符,事实上该文件结束符对应的是另一个与本文件无关的其他文件。