Part II: The C++ Library
Chapter 8. The IO Library


前面章节已介绍了大部分 IO 库设施:

  • istream(输入流)类型,提供输入操作
  • ostream(输出流)类型,提供输出操作
  • cin,一个 istream 对象,从标准输入读取数据
  • cout,一个 ostream 对象,向标准输出写入数据
  • cerr,一个 ostream 对象,通常用于输出程序错误信息,写入到向标准错误
  • >> 操作符,用于从一个 istream 对象中读取输入数据
  • << 操作符,用于向一个 ostream 对象写入输出数据
  • getline 函数,从给定的 istream 读取一行输入数据到给定的 string

8.1 IO类

表8.1 IO库类型和头文件

头文件

类型

iostream

istreamwistream 从流读取数据

ostreamwostream 向流写入数据

iostreamwiostream 读写流

fstream

ifstreamwifstream 从文件读取数据

ofstreamwofstream 向文件写入数据

fstreamwfstream 读写文件

sstream

istringstreamwistringstream 从 string 读取数据

ostringstreamwostringstream 向 string 写入数据

stringstreamwstringstream 读写 string

为了支持使用宽字符的语言,该库定义了一组用于处理 wchar_t 数据的类型和对象。
宽字符版本的名称以 w 开头。例如,wcinwcoutwcerr 是分别对应于 cincoutcerr 的宽字符对象。
宽字符类型和对象与普通字符类型定义在相同的头文件中。

IO 类型之间的关系

概念上,设备类型和字符大小都不会影响要执行的 IO 操作。
该库使我们可以忽略不同类型的流之间的差异,这通过使用继承机制

简单来说,继承机制使我们可以声明一个特定的类继承自另一个类。通常,可以使用继承类的对象,像使用基类中同样类型的对象一样。

类型 ifstream 和 istringstream 都继承自 istream。因此,可以像使用 istream 对象一样来使用 ifstream 和 istringstream 对象。可以像使用 cin 一样使用这些类型的对象。
类似地,类型 ofstream 和 ostringstream 继承自 ostream。

IO对象无拷贝或赋值

ofstream out1, out2;
out1 = out2;              // error: cannot assign stream objects
ofstream print(ofstream); // error: can't initialize the ofstream parameter
out2 = print(out2);       // error: cannot copy stream objects

因为不能拷贝 IO 类型,所以流类型不能作为形参或返回类型。进行 IO 操作的函数通常使用引用传递和返回流。读写 IO 对象会改变它的状态,所以引用不能是 const。

条件状态

IO 类定义了一些函数和标志,可以访问和操作流的条件状态

表8.2 IO库条件状态

函数或标志

说明

strm::iostate

strm 是表8.1中列出的其中一种 IO 类型。iostate 是一种依赖机器的整型,代表流的条件状态。

strm::badbit

strm::iostate 值,用来指示流已崩溃

strm::failbit

strm::iostate 值,用来指示一个 IO 操作失败。

strm::eofbit

strm::iostate 值,用来指示流到达文件结束。

strm::gootbit

strm::iostate 值,用来指示流不处于错误状态。该值保证为零。

s.eof()

若流 s 的 eofbit 置位,则返回 true。

s.fail()

若流 s 的 failbit 或 badbit 置位,则返回 true。

s.bad()

若流 s 的 badbit 置位,则返回 true。

s.good()

若流 s 处于有效状态,则返回 true。

s.clear()

将流 s 中所有条件值重置为有效状态。返回 void。

s.clear(flags)

将 s 中的条件重置为 flags。flags 的类型是 strm::iostate。返回 void。

s.setstate(flags)

添加指定的条件到 s。flags 的类型是 strm::iostate。返回 void。

s.rdstate()

返回 s 的当前状态,返回类型为 strm::iostate。

确定一个流对象的状态的最简单方式是将这个对象作为条件来使用:

while (cin >> word)
	// ok: read operation successful . . .

询问流的状态

IO 库定义了一种依赖机器的整型 iostate,用来传达流的状态的相关信息。该类型以位集合类型方式来使用。

IO 类定义了四种 iostate 类型的 constexpr 值,表示特定的位模式。这些值用来表示特定类型的 IO 条件。

  • batbit 表示系统级错误,如不可恢复的读写错误。通常,一旦 badbit 置位,流就不可以使用了。
  • failbit 会在出现一个可恢复的错误后置位,比如在期望读取数值数据时读取字符。这种问题通常可以修正,继续使用流。
  • 到达文件结束位置时,eofbit 和 failbit 都会置位。
  • goodbit 值保证为 0,表示流未发生错误。
  • 如果 badbit,failbit 或 eofbit 置位,检测流的条件会失败。

管理条件状态

// remember the current state of cin
auto old_state = cin.rdstate();   // remember the current state of cin
cin.clear();                      // make cin valid
process_input(cin);               // use cin
cin.setstate(old_state);          // now reset cin to its old state 

// turns off failbit and badbit but all other bits unchanged
cin.clear(cin.rdstate() & ~cin.failbit & ~cin.badbit);

管理输出缓冲区

每个输出流管理一个缓冲区,用来保存程序读写的数据。

因为写入设备很耗时,所以允许操作系统将多个输出操作组合成一个写操作,这带来了很大的性能提升。

有几种情况会导致将缓冲区刷新(即写入)到实际的输出设备或文件:

  • 程序正常结束。作为从 main 函数 return 的一部分,所有输出缓冲区被刷新。
  • 在缓冲区变满时,将在写入下一个值之前将其刷新。
  • 可以使用操纵符如 endl 等显式刷新缓冲区。
  • 在每个输出操作后,使用 unitbuf 操纵符设置流的内部状态来清空缓冲区。默认情况下,cerr 是设置 unitbuf 的,所以写入 cerr 会立即刷新。
  • 一个输出流可能会关联到另一个流。在这种情况下,每当读写关联的流时,都会刷新关联流的缓冲区。默认情况下,cin 和 cerr 都与 cout 关联。因此,读取 cin 或写入 cerr 会刷新 cout 中的缓冲区。

刷新输出缓冲区

操纵符:

  • endl:完成换行,并刷新缓冲区
  • flush:刷新流,但不会添加字符到输出
  • ends:插入一个空字符到缓冲区,然后刷新缓冲区
cout << "hi!" << endl;   // writes hi and a newline, then flushes the buffer
cout << "hi!" << flush;  // writes hi, then flushes the buffer; adds no data
cout << "hi!" << ends;   // writes hi and a null, then flushes the buffer

unitbuf 操纵符

unitbuf 操纵符告诉流在每个随后的写入之后都刷新。nounitbuf 操纵符恢复流,使用正常的、系统管理的缓冲区刷新机制。

cout << unitbuf;       // all writes will be flushed immediately
// any output is flushed immediately, no buffering
cout << nounitbuf;     // returns to normal buffering

警告:如果程序崩溃,缓冲区不会刷新!

将输入流和输出流绑定在一起

当输入流绑定到输出流时,读取输入流会首先刷新与输出流关联的缓冲区。
库将 cout 连接到 cin,所以语句 cin >> ival; 导致 cout 关联的缓冲区被刷新。

注意:交互式系统通常应该将它们的输入流绑定到输出流。这样做,意味着所有输出,包括用户提示信息,都会在读取输入前写出来。

tie 有两个版本:

  • 一个版本不带参数,返回指向这个对象绑定的输出流的指针,如果没有绑定流,返回空指针。
  • 第二个版本接受一个指向 ostream 的指针,并将它本身绑定到这个 ostream。

可以绑定一个 istream 或 ostream 对象到另一个 ostream。

cin.tie(&cout);   // illustration only: the library ties cin and cout for us
// old_tie points to the stream (if any) currently tied to cin
ostream *old_tie = cin.tie(nullptr); // cin is no longer tied
// ties cin and cerr; not a good idea because cin should be tied to cout
cin.tie(&cerr);   // reading cin flushes cerr, not cout
cin.tie(old_tie); // reestablish normal tie between cin and cout

每个流一次最多只能绑定到一个流。但多个流可以将它们自己绑定到同一个 ostream。


8.2 文件输入和输出

除了继承自 iostream 类型中的行为外,定义在 fstream 中的类型增加了成员来管理与流关联的文件。

表8.3 fstream 特有的操作

操作

说明

fstream fstrm;

创建一个未绑定的文件流。fstream 指的是定义在 fstream 头文件的一个类型。

fstream fstrm(s);

创建一个 fstream,打开名为 s 的文件。

s 可以是 string 类型或C风格字符串指针。这些构造函数都是 explicit。默认文件 mode 取决于 fstream 类型。

fstream fstrm(s, mode);

与前一个构造函数类似,但以给定的 mode 打开 s。

fstrm.open(s)

打开名为 s 的文件,并将文件与 fstrm 绑定。默认文件 mode 取决于 fstream 类型。返回 void。

fstrm.open(s, mode)

以给定的 mode 打开 s。

fstrm.close()

关闭与 fstrm 绑定的文件。返回 void。

fstrm.is_open()

返回一个 bool,指示与 fstrm 关联的文件是否成功打开且尚未关闭。

使用文件流对象

ifstream in(ifile); // construct an ifstream and open the given file
ofstream out;       // output file stream that is not associated with any file

在C++11版本中,文件名可以是库 string 或C风格字符数组。以前版本的库只允许C风格字符数组。

用 fstream 替代 iostream&

ifstream input(argv[1]);   // open the file of sales transactions
ofstream output(argv[2]);  // open the output file
Sales_data total;          // variable to hold the running sum
if (read(input, total)) {  // read the first transaction
	Sales_data trans;      // variable to hold data for the next transaction
	while(read(input, trans)) {    // read the remaining transactions
		if (total.isbn() == trans.isbn()) //  check isbns
			total.combine(trans);  // update the running total
		else {
			print(output, total) << endl; //  print the results
			total = trans;         // process the next book
		}
	}
	print(output, total) << endl;  // print the last transaction
} else                             // there was no input
	cerr << "No data?!" << endl;

成员 open 和 close

ifstream in(ifile); // construct an ifstreamand open the given file
ofstream out;       // output file stream that is not associated with any file
out.open(ifile + ".copy");  // open the specified file 

if (out)     // check that the open succeeded
	// the open succeeded, so we can use the file

想要将一个文件流关联到另一个文件,必须首先关闭已关联的文件。

in.close();               // close the file
in.open(ifile + "2");     // open another file

自动构造和析构

// for each file passed to the program
for (auto p = argv + 1; p != argv + argc; ++p) {
	ifstream input(*p);   // create input and open the file
	if (input) {          // if the file is ok, "process" this file
		process(input);
	} else
		cerr << "couldn't open: " + string(*p);
} // input goes out of scope and is destroyed on each iteration

当一个 fstream 对象被销毁时,close 会自动被调用。

文件模式

表8.4 文件模式

模式

说明

in

以读方式打开

out

以写方式打开

app

每次写之前定位到文件末尾

ate

打开文件后立即定位到文件末尾

trunc

截断文件

binary

以二进制方式进行 IO 操作

指定文件模式有以下限制:

  • out 只能设定于 ofstream 或 fstream 对象。
  • in 只能设定于 ifstream 或 fstream 对象。
  • 只有在指定 out 时,才能设定 trunc。
  • 只要没有设定 trunc,就可以设定 app 模式。如果指定 app,文件总是以输出模式打开,即使没有显式指定 out。
  • 默认情况下,以 out 模式打开的文件会被截断,即使没有指定 trunc。为了保存以 out 打开的文件的内容,可以指定 app,这样只会将数据写入到文件末尾;或者指定 in,这样文件可以同时用于输入和输出。
  • ate 和 binary 模式可用于任何文件流对象类型,且可与其他任何模式组合。

每个文件流类型的默认文件模式:

  • 与 ifstream 关联的文件以 in 模式打开;
  • 与 ofstream 关联的文件以 out 模式打开;
  • 与 fstream 关联的文件以 in 和 out 模式打开。

以 out 模式打开的文件会丢弃已存在的数据

// file1 is truncated in each of these cases
ofstream out("file1");   // out and trunc are implicit
ofstream out2("file1", ofstream::out);   // trunc is implicit
ofstream out3("file1", ofstream::out | ofstream::trunc);

// to preserve the file's contents, we must explicitly specify app mode
ofstream app("file2", ofstream::app);   // out is implicit
ofstream app2("file2", ofstream::out | ofstream::app);

8.3 string 流

表8.5 stringstream 特有的操作

操作

说明

sstream strm;

strm 是一个未绑定的 stringstream。sstream 是定义在 sstream 头文件的其中一个类型。

sstream strm(s);

strm 是一个 sstream,存储 string s 的拷贝。这个构造函数是 explicit。

strm.str()

返回 strm 存储的 string 的拷贝。

strm.str(s)

复制 string s 到 strm 中。返回 void。

使用 istringstream

// members are public by default
struct PersonInfo {
	string name;
	vector<string> phones;
};

string line, word;  // will hold a line and word from input, respectively
vector<PersonInfo> people; // will hold all the records from the input
// read the input a line at a time until cin hits end-of-file (or another error)
while (getline(cin, line)) {
	PersonInfo info;      // create an object to hold this record's data
	istringstream record(line); // bind record to the line we just read
	record >> info.name;  // read the name
	while (record >> word)        // read the phone numbers
		info.phones.push_back(word);  // and store them
	people.push_back(info); // append this record to people
}

输入内容类似这样:

morgan 2015552368 8625550123
drew 9735550130
lee 6095550132 2015550175 8005550000

使用 ostringstream

for (const auto &entry : people) {    // for each entry in people
	ostringstream formatted, badNums; // objects created on each loop
	for (const auto &nums : entry.phones) { // for each number
		if (!valid(nums)) {
			badNums << " " << nums;  // string in badNums
		} else            // "writes" to formatted's string
			formatted << " " << format(nums);
	}
	if (badNums.str().empty())      // there were no bad numbers
		os << entry.name << " "     // print the name
		   << formatted.str() << endl; // and reformatted numbers
	else                   // otherwise, print the name and bad numbers
		cerr << "input error: " << entry.name << " invalid number(s) " << badNums.str() << endl;
}