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库类型和头文件
头文件 | 类型 |
|
|
|
|
|
|
为了支持使用宽字符的语言,该库定义了一组用于处理 wchar_t
数据的类型和对象。
宽字符版本的名称以 w
开头。例如,wcin
,wcout
和 wcerr
是分别对应于 cin
,cout
和 cerr
的宽字符对象。
宽字符类型和对象与普通字符类型定义在相同的头文件中。
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 是表8.1中列出的其中一种 IO 类型。iostate 是一种依赖机器的整型,代表流的条件状态。 |
| strm::iostate 值,用来指示流已崩溃 |
| strm::iostate 值,用来指示一个 IO 操作失败。 |
| strm::iostate 值,用来指示流到达文件结束。 |
| strm::iostate 值,用来指示流不处于错误状态。该值保证为零。 |
| 若流 s 的 eofbit 置位,则返回 true。 |
| 若流 s 的 failbit 或 badbit 置位,则返回 true。 |
| 若流 s 的 badbit 置位,则返回 true。 |
| 若流 s 处于有效状态,则返回 true。 |
| 将流 s 中所有条件值重置为有效状态。返回 void。 |
| 将 s 中的条件重置为 flags。flags 的类型是 strm::iostate。返回 void。 |
| 添加指定的条件到 s。flags 的类型是 strm::iostate。返回 void。 |
| 返回 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;
}