IOstream 标准库

C++ 的输入/输出(input/output)由标准库提供。
标准库定义了一族类型,支持对文件和控制窗口等设备的读写(IO)。
还定义了其他一些类型,使 string对象能够像文件一样操作,从而使我们无须 IO 就能实现数据与字符之间的转换。
这些 IO 类型都定义了如何读写内置数据类型的值。

前面的程序已经使用了多种 IO 标准库提供的工具:
? istream(输入流)类型,提供输入操作。
? ostream(输出流)类型,提供输出操作。
? cin(发音为 see-in):读入标准输入的 istream 对象。
? cout(发音为 see-out):写到标准输出的 ostream 对象。
? cerr(发音为 see-err):输出标准错误的 ostream 对象。cerr 常用于程序错误信息。
? >> 操作符,用于从 istream 对象中读入输入。
? << 操作符,用于把输出写到 ostream 对象中。
? getline 函数,需要分别取 istream 类型和 string 类型的两个引用形参,
其功能是从 istream 对象读取一个单词,然后写入 string 对象中。

8.1. 面向对象的标准库
如果两种类型存在继承关系,则可以说一个类“继承”了其父类的行为——接口。
C++ 中所提及的父类称为基类(base class),而继承而来的类则称为派生类(derived class)

IO 类型在三个独立的头文件中定义:
iostream 定义读写控制窗口的类型,
fstream 定义读写已命名文件的类型,
sstream 所定义的类型则用于读写存储在内存中的 string 对象。
在 fstream 和 sstream 里定义的每种类型都是从iostream 头文件中定义的相关类型派生而来。

表 8.1. IO 标准库类型和头文件

  1. 国际字符的支持
    迄今为止,所描述的流类(stream class)读写的是由 char 类型组成的流。
    此外,标准库还定义了一组相关的类型,支持 wchar_t 类型。
    每个类都加上“w”前缀,以此与 char 类型的版本区分开来。

  2. IO 对象不可复制或赋值
    出于某些原因,标准库类型不允许做复制或赋值操作

ofstream out1, out2;
out1 = out2; // error: cannot assign stream objects

// print function: parameter is copied
ofstream print(ofstream);
out2 = print(out2); // error: cannot copy stream objects

8.2. 条件状态
表 8.2. IO 标准库的条件状态

  1. 流状态的查询和控制
    可以如下管理输入操作

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,直到到达文件结束符或者发生不可恢复的读取错误为止

  1. 条件状态的访问
    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

  1. 多种状态的处理
    常常会出现需要设置或清除多个状态二进制位的情况。
    此时,可以通过多次调用 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 位

8.3. 输出缓冲区的管理
每个 IO 对象管理一个缓冲区,用于存储程序读写的数据。
如有下面语句:
os << “please enter a value: “;

系统将字符串字面值存储在与流 os 关联的缓冲区中。

下面几种情况将导致缓冲区的内容被刷新,即写入到真实的输出设备或者文件:
1. 程序正常结束。作为 main 返回工作的一部分,将清空所有输出缓冲区。
2. 在一些不确定的时候,缓冲区可能已经满了,在这种情况下,缓冲区将会在写下一个值之前刷新。
3. 用操纵符显式地刷新缓冲区,例如行结束符 endl。
4. 在每次输出操作执行完后,用 unitbuf 操作符设置流的内部状态,从而清空缓冲区。
5. 可将输出流与输入流关联(tie)起来。在这种情况下,在读输入流时将刷新其关联的输出缓冲区。

  1. 输出缓冲区的刷新
    我们的程序已经使用过 endl 操纵符,用于输出一个换行符并刷新缓冲区。
    除此之外,C++ 语言还提供了另外两个类似的操纵符。
    第一个经常使用的 flush,用于刷新流,但不在输出中添加任何字符。
    第二个则是比较少用的 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

  1. unitbuf 操纵符
    如果需要刷新所有输出,最好使用 unitbuf 操纵符。
    这个操纵符在每次执行完写操作后都刷新流:

cout << unitbuf << “first” << ” second” << nounitbuf;
等价于:
cout << “first” << flush << ” second” << flush;

nounitbuf 操纵符将流恢复为使用正常的、由系统管理的缓冲区刷新方式。

  1. 警告:如果程序崩溃了,则不会刷新缓冲区
    如果程序不正常结束,输出缓冲区将不会刷新。
    在尝试调试已崩溃的程序时,通常会根据最后的输出找出程序发生错误的区域。
    如果崩溃出现在某个特定的输出语句后面,则可知是在程序的这个位置之后出错。
    调试程序时,必须保证期待写入的每个输出都确实被刷新了。
    因为系统不会在程序崩溃时自动刷新缓冲区,这就可能出现这样的情况:
    程序做了写输出的工作,但写的内容并没有显示在标准输出上,仍然存储在输出缓冲区中等待输出。
    如果需要使用最后的输出给程序错误定位,则必须确定所有要输出的都已经输出。
    为了确保用户看到程序实际上处理的所有输出,最好的方法是保证所有的输出操作都显式地调用了 flush 或 endl。
    如果仅因为缓冲区没有刷新,程序员将浪费大量的时间跟踪调试并没有执行的代码。
    基于这个原因,输出时应多使用 endl 而非 ‘\n’。
    使用endl 则不必担心程序崩溃时输出是否悬而未决(即还留在缓冲区,未输出到设备中)。

  2. 将输入和输出绑在一起
    当输入流与输出流绑在一起时,任何读输入流的尝试都将首先刷新其输出流关联的缓冲区。
    标准库将 cout 与 cin 绑在一起,因此语句:

cin >> ival;

导致 cout 关联的缓冲区被刷新。

交互式系统通常应确保它们的输入和输出流是绑在一起的。
这样做意味着可以保证任何输出,包括给用户的提示,都在试图读之前输出。
tie 函数可用 istream 或 ostream 对象调用,使用一个指向 ostream 对象的指针形参。
调用 tie 函数时,将实参流绑在调用该函数的对象上。
如果一个流调用 tie 函数将其本身绑在传递给 tie 的 ostream 实参对象上,
则该流上的任何 IO 操作都会刷新实参所关联的缓冲区。

cin.tie(&cout); // illustration only: the library ties cin and cout for us
ostream *old_tie = cin.tie();

cin.tie(0); // break tie to cout, cout no longer flushed when cin is read
cin.tie(&cerr); // ties cin and cerr, not necessarily a good idea!

// …
cin.tie(0); // break tie between cin and cerr
cin.tie(old_tie); // restablish normal tie between cin and cout

一个 ostream 对象每次只能与一个 istream 对象绑在一起。
如果在调用tie 函数时传递实参 0,则打破该流上已存在的捆绑。

8.4. 文件的输入和输出
fstream 头文件定义了三种支持文件 IO 的类型:
1. ifstream,由 istream 派生而来,提供读文件的功能。
2. ofstream,由 ostream 派生而来,提供写文件的功能。
3. fstream,由 iostream 派生而来,提供读写同一个文件的功能。

这些类型都由相应的 iostream 类型派生而来,
这个事实意味着我们已经知道使用 fstream 类型需要了解的大部分内容了。
特别是,可使用 IO 操作符(<<和 >> )在文件上实现格式化的 IO,
而且在前面章节介绍的条件状态也同样适用于 fstream 对象。

fstream 类型除了继承下来的行为外,
还定义了两个自己的新操作—— open和 close,以及形参为要打开的文件名的构造函数。
fstream、ifstream 或ofstream 对象可调用这些操作,而其他的 IO 类型则不能调用。

8.4.1. 文件流对象的使用
迄今为止,我们的程序已经使用过标准库定义的对象:cin、cout 和 cerr。
需要读写文件时,则必须定义自己的对象,并将它们绑定在需要的文件上。
假设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 函数完成系统指定的所有需要的操作。

  1. 警告:C++ 中的文件名
    由于历史原因,IO 标准库使用 C 风格字符串而不是 C++ strings 类型的字符串作为文件名。
    在创建 fstream 对象时,如果调用open 或使用文件名作初始化式,需要传递的实参应为 C 风格字符串,
    而不是标准库 strings 对象。程序常常从标准输入获得文件名。
    通常,比较好的方法是将文件名读入 string 对象,而不是 C 风格字符数组。
    假设要使用的文件名保存在 string 对象中,则可调用 c_str 成员获取 C 风格字符串。

  2. 检查文件打开是否成功
    打开文件后,通常要检验打开是否成功,这是一个好习惯:

// check that the open succeeded
if (!infile) {
cerr << “error: unable to open input file: ” << ifile << endl;
return -1;
}

这个条件与之前测试 cin 是否到达文件尾或遇到某些其他错误的条件类似。
检查流等效于检查对象是否“适合”输入或输出。如果打开(open)失败,
则说明 fstream 对象还没有为 IO 做好准备。

当测试对象
if (outfile) // ok to use outfile?

返回 true 意味着文件已经可以使用。由于希望知道文件是否未准备好,则对返回值取反来检查流:

if (!outfile) // not ok to use outfile?

  1. 将文件流与新文件重新捆绑
    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

  1. 清除文件流的状态
    考虑这样的程序,它有一个 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 对象,打开并读取指定的文件。
构造函数的初始化式使用了箭头操作符对 it 进行解引用,从而获取 it 当前表示的 string 对象的 c_str 成员。
文件由构造函数打开,并假设打开成功,读取文件直到到达文件结束符或者出现其他的错误条件为止。

在这个点上,input 处于错误状态。任何读 input 的尝试都会失败。
因为 input是 while 循环的局部变量,在每次迭代中创建。
这就意味着它在每次循环中都以干净的状态即 input.good() 为 true,开始使用。

如果希望避免在每次 while 循环过程中创建新流对象,可将 input 的定义移到 while 之前。
这点小小的改动意味着必须更仔细地管理流的状态。
如果遇到文件结束符或其他错误,将设置流的内部状态,以便之后不允许再对该流做读写操作。
关闭流并不能改变流对象的内部状态。如果最后的读写操作失败了,对象的状态将保持为错误模式,
直到执行 clear 操作重新恢复流的状态为止。调用 clear 后,就像重新创建了该对象一样。

如果打算重用已存在的流对象,那么 while 循环必须在每次循环进记得关闭(close)和清空(clear)文件流:

ifstream input;
vector::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 仍然会失败——毕竟最后一次对流的读操作到达了文件结束符,
事实上该文件结束符对应的是另一个与本文件无关的其他文件。

如果程序员需要重用文件流读写多个文件,必须在读另一个文件之前调用 clear 清除该流的状态。

8.4.2. 文件模式
在打开文件时,无论是调用 open 还是以文件名作为流初始化的一部分,都需指定文件模式(file mode)。
每个 fstream 类都定义了一组表示不同模式的值,用于指定流打开的不同模式。
与条件状态标志一样,文件模式也是整型常量,在打开指定文件时,可用位操作符设置一个或多个模式。
文件流构造函数和 open 函数都提供了默认实参(第 7.4.1 节)设置文件模式。
默认值因流类型的不同而不同。此外,还可以显式地以模式打开文件。
表 8.3 列出了文件模式及其含义。

表 8.3 文件模式
in 打开文件做读操作
out 打开文件做写操作
app 在每次写之前找到文件尾
ate 打开文件后立即将文件定位在文件尾
trunc 打开文件时清空已存在的文件流
binary 以二进制模式进行 IO 操作

out、trunc 和 app 模式只能用于指定与 ofstream 或 fstream 对象关联的文件;
in 模式只能用于指定与 ifstream 或 fstream 对象关联的文件。
所有的文件都可以用 ate 或 binary 模式打开。
ate 模式只在打开时有效:
文件打开后将定位在文件尾。以 binary 模式打开的流则将文件以字节序列的形式处理,而不解释流中的字符。

默认时,与 ifstream 流对象关联的文件将以 in 模式打开,该模式允许文件做读的操作:
与 ofstream 关联的文件则以 out 模式打开,使文件可写。
以out 模式打开的文件会被清空:丢弃该文件存储的所有数据。
从效果来看,为 ofstream 对象指定 out 模式等效于同时指定了 out 和 trunc 模式。
对于用 ofstream 打开的文件,要保存文件中存在的数据,唯一方法是显式地指定 app 模式打开:

// output mode by default; truncates file named “file1”
ofstream outfile(“file1”);

// equivalent effect: “file1” is explicitly truncated
ofstream outfile2(“file1”, ofstream::out | ofstream::trunc);

// append mode; adds new data at end of existing file named “file2”
ofstream appfile(“file2”, ofstream::app);

outfile2 的定义使用了按位或操作符将相应的文件同时以out 和 trunc 模式打开。

  1. 对同一个文件作输入和输出运算
    fstream 对象既可以读也可以写它所关联的文件。fstream 如何使用它的文件取决于打开文件时指定的模式。

默认情况下,fstream 对象以 in 和 out 模式同时打开。
当文件同时以 in和 out 打开时不清空。
如果打开 fstream 所关联的文件时,只使用 out 模式,而不指定 in 模式,则文件会清空已存在的数据。
如果打开文件时指定了 trunc模式,则无论是否同时指定了 in 模式,文件同样会被清空。

下面的定义将copyOut 文件同时以输入和输出的模式打开:

// open for input and output
fstream inOut(“copyOut”, fstream::in | fstream::out);

  1. 模式是文件的属性而不是流的属性
    每次打开文件时都会设置模式

ofstream outfile;

// output mode set to out, “scratchpad” truncated
outfile.open(“scratchpad”, ofstream::out);
outfile.close(); // close outfile so we can rebind it

// appends to file named “precious”
outfile.open(“precious”, ofstream::app);
outfile.close();

// output mode set by default, “out” truncated
outfile.open(“out”);

第一次调用 open 函数时,指定的模式是 ofstream::out。
当前目录中名为“scratchpad”的文件以输出模式打开并清空。而名为“precious”的文件,则
要求以添加模式打开:
保存文件里的原有数据,所有的新内容在文件尾部写入。
在打开“out”文件时,没有明确指明输出模式,该文件则以 out 模式打开,
这意味着当前存储在“out”文件中的任何数据都将被丢弃。

只要调用 open 函数,就要设置文件模式,其模式的设置可以是显式的也可以是隐式的。
如果没有指定文件模式,将使用默认值。

  1. 打开模式的有效组合
    并不是所有的打开模式都可以同时指定。
    有些模式组合是没有意义的,例如同时以 in 和 trunc 模式打开文件,准备读取所生成的流,
    但却因为 trunc 操作而导致无数据可读。

表 8.4 列出了有效的模式组合及其含义。
表 8.4 文件模式的组合
out 打开文件做写操作,删除文件中已有的数据
out | app 打开文件做写操作,在文件尾写入
out | trunc 与 out 模式相同
in 打开文件做读操作
in | out 打开文件做读、写操作,并定位于文件开头处
in | out | trunc 打开文件做读、写操作,删除文件中已有的数据

上述所有的打开模式组合还可以添加 ate 模式。
对这些模式添加 ate 只会改变文件打开时的初始化定位,在第一次读或写之前,将文件定位于文件末尾处。

8.4.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 要么已经与指定文件绑定起来了,要么处于错误条件状态。

8.5. 字符串流
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。

表 8.5. stringstream 特定的操作
stringstream strm; 创建自由的 stringstream 对象
stringstream
strm(s);

创建存储 s 的副本的 stringstream 对象,
其中 s 是string 类型的对象strm.str() 返回 strm 中存储的 string 类型对象
strm.str(s) 将 string 类型的 s 复制给 strm,返回 void

  1. 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 输入操作符即可读出每行中的单词

  1. 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 对象自动地将数值型数据的字符表示方式转换为相应的算术值。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章