【C++】【C++ Primer】8-IO库
1 IO类
目前为止,我们已经使用过以下IO库设施:
- istream类型:提供输入操作
- ostream类型:提供输出操作
- cin:istream类型对象,从标准输入读取数据
- cout:ostream类型对象,向标准输出写入数据
- cerr:ostream类型对象,向标准错误输出错误信息
- >>运算符:用于从istream类型对象读取数据
- <<运算符:用于向ostream类型对象写入数据
- getline函数:从给定的istream类型对象读取一行数据,存入给定的string对象中。
默认情况下,这些对象都是关联到控制台窗口的。但应用程序常常要读写文件、使用IO操作处理string中的字符、读写需要宽字符支持的语言。为了支持这些不同种类的的IO操作,在istream和ostream之外,标准库还定义了一些IO类型。
表1 IO库类型和头文件 | |
---|---|
头文件 | IO库类型和头文件 |
iostream | istream、wistream从流读取数据 |
ostream、wostream向流写入数据 | |
iostream、wiostream读写流 | |
fstream | ifstream、wifstream从文件读取数据 |
ofstream、wofstream向文件写入数据 | |
fstream、wfstream读写文件 | |
sstream | istringstream、wistringstream从string读取数据 |
ostringstream、wostringstream向string写入数据 | |
stringstream、wstringstream读写string |
为了支持使用宽字符的语言,标准库定义了一组类型和对象来操纵wchar_t类型的数据。宽字符版本的类型和函数名以w开始。
1.1 IO类型间的关系
设备类型和字符大小都不会影响我们要执行的IO操作。譬如用>>读取数据,不用管是从console、磁盘文件还是string读取,也不用管读取的字符是存入char对象内还是存入wchar_t对象内。
标准库之所以能忽略不同类型的流之间的差异,是通过继承机制实现的。类型ifstream和istringstream都继承自istream。因此可以像使用istream对象一样使用ifstream和istringstream对象。
1.2 IO对象不能拷贝或赋值
不能拷贝或对IO对象赋值,因此不能将形参或返回类型设置为流类型。进行IO操作的函数通常以引用方式传递和返回流。
读写一个IO对象会改变其状态,所以IO对象的引用不能是const的。
1.3 条件状态
IO操作可能会发生错误。一些错误是可恢复的,发生在系统深处的错误则超出了应用程序可以修复的范围。下表列出了IO类定义的一些函数和标志,用于访问和操纵流的条件状态。
表2 IO库类型和头文件 | |
---|---|
strm::iostate | strm是表1中的IO类型。iostate是机器相关的类型,提供了表达条件状态的完整功能 |
strm::badbit | strm::badbit用来指出流已崩溃 |
strm::failbit | strm::failbit用来指出一个IO操作失败了 |
strm::eofbit | strm::eofbit用来指出流到达了文件结束 |
strm::goodbit | strm::goodbit用来指出流未处于错误状态。此值保证为0 |
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) | 根据给定的flags标志位,将流s中对应条件状态位复位。flags的类型为strm::iostate。返回void |
s.setstate(flags) | 根据给定的flags标志位,将流s中对应条件状态位置位。flags的类型为strm::iostate。返回void |
s.rdstate() | 返回流s的当前状态,返回值类型为strm::iostate |
如果从标准输入读取的数据和接收数据的变量类型不符,则读操作失败,cin进入错误状态。如果输入EOF,cin也会进入错误状态。
一旦一个流发生错误,后续的IO操作都会失败。只有当一个流处于无错状态时,才能对其读写数据。
由于流可能处于错误状态,因此在使用流之前要先检查。最简单的方式是将其作为条件使用。流处于有效状态时,条件为真。
while (cin >> word)
1.3.1 查询流的状态
IO库定义了一个与机器无关的iostate类型,它提供了表达流状态的完整功能。iostate应作为一个位集合来使用。
IO库定义了4个iostate类型的constexpr值,表示特定的位模式。这些值用于表示特定类型的IO条件,可以与位运算符一起使用来一次性检测或设置多个标志位。
- badbit:系统级错误,如不可恢复的读写错误。一旦badbit被置位,流就无法使用了。
- failbit:可恢复错误,如期望读取数值却读出字符。这种问题通常可以修复,流可以继续使用。
- eofbit:到达文件结束位置,eofbit被置位。此时failbit也会被置位。
- gootbit:goodbit值为0时,表示流未发生错误。
badbit、failbit、eofbit中的任意一个被置位,检测流状态的条件都会失败。
标准库还定义了一组函数来查询这些标志位的状态。s.good()在所有错误均未置位时返回true。s.bad()、s.fail()、s.eof()在对应错误被置位时返回true。badbit置位时,s.fail()也会返回true。因此,使用s.good()或s.fail()来确定流的总体状态。将流作为条件使用的代码就等价于!s.fail()。
1.3.2 管理流的状态
s.rdstate()返回一个iostate值,表示流的当前状态。
s.setstate()操作将给定条件位置位,表示发生了对应错误。
s.clear()是一个重载成员:
- s.clear()复位所有错误标志位。执行后s.good()会返回true。
- s.clear(flags)接受一个iostate值,表示流的新状态。需要复位单一条件状态位时,先用rdstate读出当前条件状态,然后用位操作将所需位复位,生成新的状态,然后再通过s.clear(flags)复位。
cin.clear(cin.rdstate() & ~cin.failbit & ~cin.badbit);
1.4 管理输出缓冲
每个输入流都管理一个缓冲区,用来保存程序读写的数据。有了缓冲机制,OS可以将程序的多个输出操作组合成单一的系统级写操作。由于系统级写操作比较耗时,缓冲机制可以带来很大的性能提升。
刷新缓冲的原因有以下几种:
- 程序正常结束,main函数的return操作会刷新缓冲。
- 缓冲区满,会刷新缓冲,以容纳新的数据。
- 使用操纵符显式刷新缓冲区。
- 使用操纵符unitbuf设置流,使流立即刷新。cerr默认是设置unibuf的,因此写入cerr的数据会立刻刷新。
- 一个输出流可以关联到另一个流。当读写关联的流时,该输出流的缓冲区会被刷新。默认情况下cin和cerr关联到cout。因此读cin或写cerr都会导致cout的缓冲区被刷新。
1.4.1 刷新输出缓冲区
共有三个操纵符可以刷新缓冲区。
表3 刷新缓冲区的操纵符 | |
---|---|
操作符 | 功能 |
endl | 换行,然后刷新缓冲区 |
ends | 向缓冲区插入一个空字符,然后刷新缓冲区 |
flush | 刷新缓冲区,不输出任何额外字符 |
示例代码:
cout << "hi" << endl; // 输出hi和一个换行,然后刷新缓冲区
cout << "hi" << ends; // 输出hi和一个空字符,然后刷新缓冲区
cout << "hi" << flush; // 输出hi,然后刷新缓冲区
1.4.2 unitbuf操纵符
每次输出都写上操纵符比较繁琐,我们可以使用unitbuf操纵符设置流,之后每次写操作都会自动做一次flush。
使用nounitbuf操纵符设置流则可以使其恢复正常的缓冲区刷新机制。
cout << unitbuf; // 设置任何输出后立即刷新
// 此时任何输出都会立即刷新,无缓冲
cout << nounitbuf; // 恢复正常缓冲机制
1.4.3 关联输入流和输出流
当一个输入流被关联到一个输出流时,从输入流读取数据的操作会先刷新关联的输出流。交互式系统应当将输入流和输出流关联。从而确保所有输出都在读操作之前被打印出来。
标准库将cout和cin关联在一起,因此以下语句会导致cout的缓冲区被刷新:
cin >> ival;
使用tie函数来关联流。
不带参数的tie函数返回指向输出流的指针。如果本对象当前关联到一个输出流,则返回指向这个流的指针。如果对象未关联到流,则返回空指针。
tie函数的第二个版本接受一个指向ostream的指针,将自己关联到ostream。即x.tie(&o)将流x关联到输出流o。
不仅可以将一个istream关联到另一个ostream,也可以将两个ostream关联在一起。
如果要将某个流重新绑定到另一个流上,只要将新流的指针传递给tie即可。如果想彻底解开流的关联,调用tie时传入空指针即可。
每个流最多同时关联到一个流,但多个流可以同时关联到一个流。
2 文件输入输出
头文件fstream定义了三个类型,用于支持文件IO:
- ifstream从一个给定文件读取数据;
- ofstream向一个给定文件写入数据;
- fstream读写给定文件。
ifsream、ofstream、fstream和cin、cout的使用方法一样。可以使用IO运算符(<<、>>)来读写文件,也可以用getline从一个ifstream读取数据。除了继承自iostream类型的行为外,fstream中定义的类型还增加了一些新的成员来管理与流关联的文件:
表4 fstream特有的操作 | |
---|---|
fstream fstrm; | 创建一个未绑定的文件流。fstream是头文件fstream中定义的类型 |
fstream fstrm(s); | 创建一个fstream,并打开名为s的文件。s可以是string类型或指向C风格字符串的指针。这些构造函数都是explicit的。默认的文件模式mode依赖于fstream的类型 |
fstream fstrm(s, mode); | 与前一个构造函数类似,但按指定的mode打开文件。 |
fstrm.open(s) | 打开名为s的文件,并将文件与fstrm绑定。s可以是string类型或指向C风格字符串的指针。默认的文件mode依赖于fstream的类型,返回void |
fstrm.close() | 关闭与fstrm绑定的文件。返回void |
fstrm.is_open() | 返回一个bool值,指出与fstrm关联的文件是否成功打开且尚未关闭 |
2.1 使用文件流对象
读写文件时,可以定义一个文件流对象,并将其与文件关联起来。
2.1.1 成员函数open与close
每个文件流函数都定义了名为open的成员函数,定位给定的文件并视情况打开为读或写模式。
如果定义了一个空文件流对象,可以随后调用open函数将其与文件关联起来。
如果在创建文件流对象时提供文件名,则open函数会被自动调用。在C++新标准中,文件名既可以是string对象,也可以是指向C风格字符串的指针。
ifstream in(ifile); // 构造了一个ifstream对象,并自动调用open函数,打开ifile
ofstream out; // 构造了一个ofstream对象,但未关联到具体文件
out.open(ofile); // 将out与ofile关联起来
如果调用open失败,failbit会被置位。考虑到失败的可能性,应检测open是否成功。
if (out) {
// 执行操作
}
一旦一个文件流成功打开,就会保持与对应文件的关联。对已经打开的文件流再次调用open会失败,此时failbit会被置位,随后对该文件流的操作都会失败。如果想将该文件流关联到其他文件,首先要调用close关闭已经关联的文件,然后重新打开新的文件。
in.close();
in.open(ifile2);
如果open成功,则会设置流的状态,使得good()为true。
当fstream对象被销毁时,close会被自动调用。
while (1) {
ifstream input(ifile);
if (input) {
// 执行操作
}
} // ifstream是局部变量,每次循环执行结束均会销毁
2.2 文件模式
每个流都有一个关联的文件模式,用于指出如何使用文件。
表5 文件模式 | |
---|---|
in | 以读方式打开 |
out | 以写方式打开 |
app | 每次写操作前均定位到文件末尾 |
ate | 打开文件后立即定位到文件末尾 |
trunc | 截断文件 |
binary | 以二进制形式进行IO |
无论是使用文件名初始化流来隐式打开文件,还是调用open显式打开文件,都可以指定文件模式。指定文件模式有以下限制:
- 只能对ofstream或fstream对象设定out模式(不能对ifstream对象设置out模式)
- 只能对ifstream或fstream对象设定in模式(不能对ofstream对象设置in模式)
- 只有out模式被设定时,才能设置trunc模式
- 只要trunc模式没有被设定,就可以设定为app模式。如果app模式被设定,即使不显式指定out模式,文件也总是以输出方式打开。
- 默认情况下,即使不指定trunc,以out模式打开的文件也会被截断。为了保留以out模式打开的文件的内容,必须同时指定app模式或同时指定in模式。
- ate和binary模式可用于任何类型的文件流对象,且可以与其他任何文件模式组合使用。
每个文件流类型都定义了默认的文件模式,如果我们不显式指定文件模式,就使用默认模式。
- 与ifstream关联的文件默认以in模式打开
- 与ofstream关联的文件默认以out模式打开
- 与fstream关联的文件默认以in和out模式打开
// 以下三条语句,file1都被截断
ofstream out("file1"); // 未显式指定mode,默认以out模式打开文件,因此会截断文件
ofstream out2("file1", ofstream::out); // 显式将mode指定为out,因此会截断文件
ofstream out3("file1", ofstream::out | ofstream::trunc); // 显式将mode指定为out+trunc,因此会截断文件
// 为了保留文件内容,必须显式指定app模式
ofstream app("file2", ofstream::app); // 显式将mode指定为app,隐含out
ofstream app2("file2", ofstream::out | ofstream::app); // 显式将mode指定为out+app
对于一个给定流,每次打开文件时都可以改变其文件模式。譬如以下代码,第一次open时没有显式指定mode,默认以out模式打开(out模式还隐含地使用trunc模式)。第二次open时显示指定了app模式。
ofstream out;
out.open("scratchpad");
out.close();
out.open("precious", ofstream::app);
out.close();
3 string流
sstream头文件定义了三个类型来支持内存IO。istringstream从string读取数据,ostringstream向string写入数据,stringstream可以读写string。
头文件sstream中定义的类型都继承自iostream头文件中定义的类型。除了继承的操作,sstream中定义的类型还增加了一些成员,用于管理与流关联的string。
表6 stringstream特有的操作 | |
---|---|
sstream strm; | strm是一个未绑定的stringstream对象 |
sstream strm(s); | strm是一个sstream对象,保存string s的一个拷贝。这个构造函数是explicit的 |
strm.str() | 返回strm保存的string的拷贝 |
strm.str(s) | 将string s 拷贝到strm中,返回void |
4 示例
文件phonebook.txt中存有一些人和他们的电话号码,内容如下:
Morgan 2015552368 8625550123
drew 9735550130
lee 6095550132 2015550175 8005550000
我们的程序读取phonebook.txt中的数据,逐个验证电话号码并改变其格式,如果所有号码都是有效的,则输出到formartted.txt中。如果存在无效的号码,则不输出到formatted.txt中,而是打印包含人名和无效号码的错误信息。
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
using std::cin;
using std::cerr;
using std::cout;
using std::endl;
using std::ifstream;
using std::istringstream;
using std::ofstream;
using std::ostringstream;
using std::string;
using std::vector;
struct PersonInfo {
string name;
vector<string> phones;
};
void readPhonebook(string infile, vector<PersonInfo> &people)
{
string line;
ifstream fin(infile);
while (getline(fin, line)) {
PersonInfo info;
string word;
istringstream record(line);
record >> info.name;
while (record >> word) {
info.phones.push_back(word);
}
people.push_back(info);
}
return;
}
bool valid(const string &nums)
{
return true;
}
const string format(const string &nums)
{
return nums;
}
void writePhonebook(string outfile, vector<PersonInfo> &people)
{
ofstream fout(outfile);
for (const auto &entry : people) {
ostringstream formatted, badNums;
for (const auto &nums : entry.phones) {
if (!valid(nums)) {
badNums << " " << nums;
} else {
formatted << " " << format(nums);
}
}
if (badNums.str().empty()) {
fout << entry.name << " " << formatted.str() << endl;
} else {
cerr << "input error: " << entry.name
<< " invalid number(s) " << badNums.str() << endl;
}
}
}
int main(int argc, char **argv)
{
vector<PersonInfo> people;
string infile("./phonebook.txt");
string outfile("./formatted.txt");
readPhonebook(infile, people);
writePhonebook(outfile, people);
return 0;
}