面向对象的标准库(续)
[1. 条件状态]
在展开讨论 fstream 和 sstream 头文件中定义的类型之前,
需要了解更多 IO 标准库如何管理其缓冲区及其流状态的相关内容。
谨记本节和下一节所介绍的内容同样适用于普通流、文件流以及 string 流。
所有 IO 对象都有一组条件状态,用来指示是否可以通过该对象进行 IO 操作。
如果出现了错误(例如遇到文件结束符)对象的状态将标志无法再进行输入,直到修正了错误为止。
标准库提供了一组函数设置和检查这些状态,
用来标记给定的 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
int ival;
考虑下面 IO 错误的例子:
cin >> ival;
如果在标准输入设备输入 Borges,则 cin 在尝试将输入的字符串读为 int 型数据失败后,
会生成一个错误状态。类似地,如果输入文件结束符(end-of-file),cin 也会进入错误状态。
而如果输入 1024,则成功读取,cin 将处于正确的无错误状态。
流必须处于无错误状态,才能用于输入或输出。检测流是否用的最简单的方法是检查其真值:
if (cin) // ok to use cin, it is in a valid state while (cin >> word) // ok: read operation successful ...
if 语句直接检查流的状态,而 while 语句则检测条件表达式返回的流,从而间接地检查了流的状态。
如果成功输入,则条件检测为 true。
有些程序则需要更详细地访问或控制流的状态,
此时,除了知道流处于错误状态外,还必须了解它遇到了哪种类型的错误。
例如,程序员也许希望弄清是到达了文件的结尾,还是遇到了 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 将保留其他已存在的状态变量不变。
流状态的查询和控制
可以如下管理输入操作——
int ival; // read cin and test only for EOF; loop is executed even if there are other IO failures while (cin >> ival, !cin.eof()) { if (cin.bad()) // input stream is corrupted; bail out throw runtime_error("IO stream corrupted"); if (cin.fail()) { // bad input cerr<< "bad data, try again"; // warn the user cin.clear(istream::failbit); // reset the stream continue; // get next input } // ok to process ival }
这个循环不断读入 cin,直到到达文件结束符或者发生不可恢复的读取错误为止。
循环条件使用了逗号操作符,其求解过程:
首先计算它的每一个操作数,然后返回最右边操作数作为整个操作的结果。
因此,循环条件只读入 cin 而忽略了其结果。该条件的结果是 !cin.eof() 的值。
如果 cin 到达文件结束符,条件则为假,退出循环。
如果 cin 没有到达文件结束符,则不管在读取时是否发生了其他可能遇到的错误,都进入循环。
在循环中,首先检查流是否已破坏。
如果是的放,抛出异常并退出循环。如果输入无效,则输出警告并清除 failbit 状态。
在本例中,执行 continue 语句回到 while 的开头,读入另一个值 ival。
如果没有出现任何错误,那么循环体中余下的部分则可以很安全地使用 ival。
条件状态的访问
rdstate 成员函数返回一个 iostate 类型值,该值对应于流当前的整个条件状态:
// remember current state of cin
istream::iostate old_state = cin.rdstate();
cin.clear();
process_input(); // use cin
cin.clear(old_state); // now reset cin to old state
多种状态的处理
当需要设置或清除多个状态二进制位的情况,可以通过多次调用 setstate 或者 clear 函数实现。
另外一种方法则是使用按位或(OR)操作符在一次调用中生成“传递两个或更多状态位”的值。
按位或操作使用其操作数的二进制位模式产生一个整型数值。
对于结果中的每一个二进制位,如果其值为 1,
则该操作的两个操作数中至少有一个的对应二进制位是 1。例如:
// sets both the badbit and the failbit
is.setstate(ifstream::badbit | ifstream::failbit);
将对象 is 的 failbit 和 badbit 位同时打开。实参:
is.badbit | is.failbit
生成了一个值,其对应于 badbit 和 failbit 的位都打开了,
也就是将这两个位都设置为 1,该值的其他位则都为 0。
在调用 setstate 时,使用这个值来开启流条件状态成员中对应的 badbit 和 failbit 位。
[2. 输出缓冲区的管理]
每个 IO 对象管理一个缓冲区,用于存储程序读写的数据。如有下面语句:
os << "please enter a value: ";
系统将字符串字面值存储在与流 os 关联的缓冲区中。
下面几种情况将导致缓冲区的内容被刷新,即写入到真实的输出设备或者文件:
1) 程序正常结束。作为 main 返回工作的一部分,将清空所有输出缓冲区。
2) 在一些不确定的时候,缓冲区可能已经满了,在这种情况下,缓冲区将会在写下一个值之前刷新。
3) 用操纵符显式地刷新缓冲区,例如行结束符 endl。
4) 在每次输出操作执行完后,用 unitbuf 操作符设置流的内部状态,从而清空缓冲区。
5) 可将输出流与输入流关联(tie)起来。在这种情况下,在读输入流时将刷新其关联的输出缓冲区。
我们的程序已经使用过 endl 操纵符,用于输出一个换行符并刷新缓冲区。
除此之外,C++ 语言还提供了另外两个类似的操纵符。
第1个经常使用的 flush,用于刷新流,但不在输出中添加任何字符。
第2个则是比较少用的 ends,这个操纵符在缓冲区中插入空字符 null,然后后刷新它。
cout << "hi!" << flush; // flushes the buffer; adds no data cout << "hi!" << ends; // inserts a null, then flushes the buffer cout << "hi!" << endl; // inserts a newline, then flushes the buffer
如果需要刷新所有输出,最好使用 unitbuf 操纵符,在每次执行完写操作后都会刷新流:
cout << unitbuf << "first" << " second" << nounitbuf;
等价于:
cout << "first" << flush << " second" << flush;
nounitbuf 操纵符将流恢复为使用正常的、由系统管理的缓冲区刷新方式。
警告:如果程序崩溃了,则不会刷新缓冲区!
在尝试调试已崩溃的程序时,通常会根据最后的输出找出程序发生错误的区域。
如果崩溃出现在某个特定的输出语句后面,则可知是在程序的这个位置之后出错。
调试程序时,必须保证期待写入的每个输出都确实被刷新了。
因为系统不会在程序崩溃时自动刷新缓冲区,这就可能出现这样的情况:
程序做了写输出的工作,但写的内容并没有显示在标准输出上,仍然存储在输出缓冲区中等待输出。
如果需要使用最后的输出给程序错误定位,则必须确定所有要输出的都已经输出。
为了确保用户看到程序实际上处理的所有输出,
最好的方法是保证所有的输出操作都显式地调用了 flush 或 endl。
如果仅因为缓冲区没有刷新,程序员将浪费大量的时间跟踪调试并没有执行的代码。
基于这个原因,输出时应多使用 endl 而非 '\n'。
使用 endl 则不必担心程序崩溃时输出是否悬而未决(即还留在缓冲区,未输出到设备中)。
[3. 文件的输入和输出]
fstream 头文件定义了三种支持文件 IO 的类型:
ifstream,由 istream 派生而来,提供读文件的功能。
ofstream,由 ostream 派生而来,提供写文件的功能。
fstream,由 iostream 派生而来,提供读写同一个文件的功能。
这些类型都由相应的 iostream 类型派生而来,
这个事实意味着我们已经知道使用 fstream 类型需要了解的大部分内容了。
特别是,可使用 IO 操作符(<< 和 >> )在文件上实现格式化的 IO,
而且在前面章节介绍的条件状态也同样适用于 fstream 对象。
fstream 类型除了继承下来的行为外,还定义了两个自己的新操作——
open 和 close,以及形参为要打开的文件名的构造函数。
fstream、ifstream 或 ofstream 对象可调用这些操作,而其他的 IO 类型则不能调用。
3.1. 文件流对象的使用
需要读写文件时,则必须定义自己的对象,并将它们绑定在需要的文件上。
假设 ifile 和 ofile 是存储希望读写的文件名的 strings 对象,可如下编写代码:
// construct an ifstream and bind it to the file named ifile ifstream infile(ifile.c_str()); // ofstream output file object to write file named ofile ofstream outfile(ofile.c_str());
上述代码定义并打开了一对 fstream 对象。infile 是读的流,而 outfile 则是写的流。
为 ifstream 或者 ofstream 对象提供文件名作为初始化式,就相当于打开了特定的文件。
ifstream infile; // unbound input file stream ofstream outfile; // unbound output file stream
上述语句将 infile 定义为读文件的流对象,将 outfile 定义为写文件的对象。
这两个对象都没有捆绑具体的文件。在使用 fstream 对象之前,还必须使这些对象捆绑要读写的文件:
infile.open("in"); // open file named "in" in the current directory outfile.open("out"); // open file named "out" in the current directory
调用 open 成员函数将已存在的 fstream 对象与特定文件绑定。
为了实现读写,需要将指定的文件打开并定位,open 函数完成系统指定的所有需要的操作。
警告:C++ 中的文件名
由于历史原因,IO 标准库使用的是 C 风格字符串而不是 C++ strings 类型的字符串作为文件名。
在创建 fstream 对象时,如果调用 open 或使用文件名作初始化式,
需要传递的实参应为 C 风格字符串,而不是标准库 strings 对象。
程序常常从标准输入获得文件名。
通常,比较好的方法是将文件名读入 string 对象,而不是 C 风格字符数组。
假设要使用的文件名保存在 string 对象中,则可调用 c_str 成员获取 C 风格字符串。
检查文件打开是否成功
打开文件后,通常要检验打开是否成功,这是一个好习惯:
// check that the open succeeded if (!infile) { cerr << "error: unable to open input file: " << ifile << endl; return -1; }
这个条件与之前测试 cin 是否到达文件尾或遇到某些其他错误的条件类似。
检查流等效于检查对象是否“适合”输入或输出。
如果打开(open)失败,则说明 fstream 对象还没有为 IO 做好准备。
当测试对象 if (outfile) 返回 true 意味着文件已经可以使用。
由于希望知道文件是否未准备好,则对返回值取反来检查流:if (!outfile) 。
将文件流与新文件重新绑定
fstream 对象一旦打开,就保持与指定的文件相关联。
如果要把 fstream 对象与另一个不同的文件关联,
则必须先 close 现在的文件,然后 open 另一个文件:
要点是在尝试打开新文件之前,必须先关闭当前的文件流。
open 函数会检查流是否已经打开。如果已经打开,则设置内部状态,以指出发生了错误。
接下来使用文件流的任何尝试都会失败。
ifstream infile("in"); // opens file named "in" for reading infile.close(); // closes "in" infile.open("next"); // opens file named "next" for reading
清除文件流的状态
考虑这样的程序,它有一个 vector 对象,
包含一些要打开并读取的文件名,程序要对每个文件中存储的单词做一些处理。
假设该 vector 对象命名为 files,程序也许会有如下循环:
// for each file in the vector while (it != files.end()) { ifstream input(it->c_str()); // open the file; // if the file is ok, read and "process" the input if (!input) break; // error: bail out! while(input >> s) // do the work on this file process(s); ++it; // increment iterator to get next file }
每一次循环都构造了名为 input 的 ifstream 对象,打开并读取指定的文件。
构造函数的初始化式使用了箭头操作符。
1) 对 it 进行解引用,从而获取 it 当前表示的 string 对象的 c_str 成员。
文件由构造函数打开,并假设打开成功,读取文件直到到达文件结束符或者出现其他的错误条件为止。
在这个点上,input 处于错误状态。任何读 input 的尝试都会失败。因为 input 是 while 循环的局部变量,在每次迭代中创建。
这就意味着它在每次循环中都以干净的状态即 input.good() 为 true,开始使用。
2) 如果希望避免在每次 while 循环过程中创建新流对象,可将 input 的定义移到 while 之前。
这点小小的改动意味着必须更仔细地管理流的状态。
如果遇到文件结束符或其他错误,将设置流的内部状态,以便之后不允许再对该流做读写操作。
关闭流并不能改变流对象的内部状态。
如果最后的读写操作失败了,对象的状态将保持为错误模式,
直到执行 clear 操作重新恢复流的状态为止。
调用 clear 后,就像重新创建了该对象一样。
3) 如果打算重用已存在的流对象,那么 while 循环必须在每次循环进记得close和clear文件流:
ifstream input; vector<string>::const_iterator it = files.begin(); // for each file in the vector while (it != files.end()) { input.open(it->c_str()); // open the file // if the file is ok, read and "process" the input if (!input) break; // error: bail out! while(input >> s) // do the work on this file process(s); input.close(); // close file when we're done with it input.clear(); // reset state to ok ++it; // increment iterator to get next file }
如果忽略 clear 的调用,则循环只能读入第一个文件。
要了解其原因,就需要考虑在循环中发生了什么:
首先打开指定的文件。假设打开成功,则读取文件直到文件结束或者出现其他错误条件为止。
在这个点上,input 处于错误状态。
如果在close该流前没有调用 clear 清除流的状态,接着在 input 上做的任何输入运算都会失败。
一旦关闭该文件,再打开下一个文件时,在内层 while 循环上读 input 仍然会失败——
毕竟最后一次对流的读操作到达了文件结束符,
事实上该文件结束符对应的是另一个与本文件无关的其他文件。
3.2 文件模式的组合
out 打开文件做写操作,删除文件中已有的数据 out | app 打开文件做写操作,在文件尾写入 out | trunc 与 out 模式相同 in 打开文件做读操作 in | out 打开文件做读、写操作,并定位于文件开头处 in | out | trunc 打开文件做读、写操作,删除文件中已有的数据
上述所有的打开模式组合还可以添加 ate 模式。
对这些模式添加 ate 只会改变文件打开时的初始化定位,在第一次读或写之前,将文件定位于文件末尾处。
3.3. 一个打开并检查输入文件的程序
我们编写一个名为 open_file 的函数实现这个功能。
这个函数有两个引用形参,分别是 ifstream 和 string 类型,
其中 string 类型的引用形参存储与指定 ifstream 对象关联的文件名:
// opens in binding it to the given file ifstream& open_file(ifstream &in, const string &file) { in.close(); // close in case it was already open in.clear(); // clear any existing errors // if the open fails, the stream will be in an invalid state in.open(file.c_str()); // open the file we were given return in; // condition state is good if open succeeded }
由于不清楚流 in 的当前状态,因此首先调用 close 和 clear 将这个流设置为有效状态。
然后尝试打开给定的文件。如果打开失败,流的条件状态将标志这个流是不可用的。
最后返回流对象 in,此时,in 要么已经与指定文件绑定起来了,要么处于错误条件状态。
[4. 字符串流]
iostream 标准库支持内存中的输入/输出,
只要将流与存储在程序内存中的 string 对象绑定起来即可。
此时,可使用 iostream 输入和输出操作符读写这个 string 对象。
标准库定义了三种类型的字符串流:
istringstream,由 istream 派生而来,提供读 string 的功能。 ostringstream,由 ostream 派生而来,提供写 string 的功能。 stringstream,由 iostream 派生而来,提供读写 string 的功能。
要使用上述类,必须包含 sstream 头文件。
与 fstream 类型一样,上述类型由 iostream 类型派生而来,
这意味着 iostream 上所有的操作适用于 sstream 中的类型。
sstream 类型除了继承的操作外,还各自定义了一个有 string 形参的构造函数,
这个构造函数将 string 类型的实参复制给 stringstream 对象。
对 stringstream 的读写操作实际上读写的就是该对象中的 string 对象。
这些类还定义了名为 str 的成员,用来读取或设置 stringstream 对象所操纵的 string 值。
注意到尽管 fstream 和 sstream 共享相同的基类,但它们没有其他相互关系。
特别是,stringstream 对象不使用 open 和 close 函数,而 fstream 对象则不允许使用 str。
下表是 stringstream 特定的操作——
stringstream strm; 创建自由的 stringstream 对象 stringstream strm(s); 创建存储 s 的副本的 stringstream 对象,其中 s 是 string 类型的对象 strm.str(); 返回 strm 中存储的 string 类型对象 strm.str(s); 将 string 类型的 s 复制给 strm,返回 void
stringstream 对象的和使用
对于每次一个单词或每次一行的方式处理输入的程序。
前者用 string 输入操作符,后者则使用 getline 函数。
当程序需要同时使用这两种方式时,可用 stringstreams 对象实现:
string line, word; // will hold a line and word from input, respectively while (getline(cin, line)) { // read a line from the input into line // do per-line processing istringstream stream(line); // bind to stream to the line we read while (stream >> word){ // read a word from line // do per-word processing } }
这里,使用 getline 函数从输入读取整行内容。
然后为了获得每行中的单词,将一个 istringstream 对象与所读取的行绑定起来,
这样只需要使用普通的 string 输入操作符即可读出每行中的单词。
stringstream 提供的转换和/或格式化
stringstream 对象的一个常见用法是,需要在多种数据类型之间实现自动格式化时使用该类类型。
例如,有一个数值型数据集合,要获取它们的 string 表示形式,或反之。
sstream 输入和输出操作可自动地把算术类型转化为相应的 string 表示形式,反过来也可以。
int val1 = 512, val2 = 1024; ostringstream format_message; // ok: converts values to a string representation format_message << "val1: " << val1 << "\n" << "val2: " << val2 << "\n";
这里创建了一个名为 format_message 的 ostringstream 类型空对象,并将指定的内容插入该对象。
重点在于 int 型值自动转换为等价的可打印的字符串。format_message 的内容是以下字符:
val1: 512\nval2: 1024
相反,用 istringstream 读 string 对象,即可重新将数值型数据找回来。
读取 istringstream 对象自动地将数值型数据的字符表示方式转换为相应的算术值。
// str member obtains the string associated with a stringstream istringstream input_istring(format_message.str()); string dump; // place to dump the labels from the formatted message // extracts the stored ascii values, converting back to arithmetic types input_istring >> dump >> val1 >> dump >> val2; cout << val1 << " " << val2 << endl; // prints 512 1024
这里使用 str 成员获取与之前创建的 ostringstream 对象关联的 string 副本。
再将 input_istring 与 string 绑定起来。
在读 input_istring 时,相应的值恢复为它们原来的数值型表示形式
为了读取 input_string,必须把该 string 对象分解为若干个部分。
我们要的是数值型数据;为了得到它们,必须读取(和忽略)处于所需数据周围的标号。
因为输入操作符读取的是有类型的值,因此读入的对象类型必须和由 stringstream 读入的值的类型一致。
在本例中,input_istring 分成 4 个部分:
string 类型的值 val1,接着是 512,然后是 string 类型的值 val2,最后是 1024。
一般情况下,使用输入操作符读 string 时,空白符将会忽略。
于是,在读与 format_message 关联的 string 时,忽略其中的换行符。