参考资料:
- 《C++ Primer》第5版
- 《C++ Primer 习题集》第5版
8.1 IO类(P278)
我们目前使用过的 IO 对象(cin
、cout
)都是关联到控制台窗口、操纵 char
数据的。有时,我们需要对命名文件或者 string
IO 操作。为了支持不同种类的 IO 操作,标准库定义了一些 IO 类型:
IO类型间的关系
概念上,设备类型和字符大小不会影响我们要执行的 IO 操作,例如我们可以用 >>
读取数据,这是通过**继承机制(inheritance)**实现的。
8.1.1 IO对象无拷贝或赋值(P279)
我们不能拷贝或对 IO 对象赋值,进行 IO 操作的函数通常以引用的方式传递和返回流。读写 IO 对象会改变其状态,所以传递和返回的引用不能是 const
的。
8.1.2 条件状态(P279)
IO 操作可能发生不同类型的错误,IO 类定义了一些函数和标志,可以帮助我们访问和操纵流的条件状态(condition state)。
例如我们使用输入运算符并期待读入一个 int
,而实际上却得到了一个字符 B
,此时输入流将进入错误状态。只有当一个流处于无错状态时,我们才可以向它读写数据。最简单的判断流对象是否处于良好状态的方法:
while(cin >> word){ ... }
查询流的状态
IO 库定义了一个机器无关的 iostate
类型,提供表达流状态的完整功能。iostate
应该作为位集合来使用,IO 库定义了 4 个特定 constexpr
来表示特定的位:
badbit
表示系统级错误,通常为不可恢复的读写错误。failbit
表示可恢复错误,如期望读取数值却得到字符。eofbit
表示文件结束,此时failbit
也会被置位。goodbit
为0
表示流未发生错误(个人推测,前 3 个位在正常状态下为0
,goodbit
就是它们的或)。
标准库还定义了一组函数来查询上述标志位的状态:
good
在所有错误位均未被置位的情况下返回true
。bad
、fail
、eof
在对应位被置位时返回true
,此外,fail
在badbit
被置位时也会返回true
。
管理条件状态
auto old_state = cin.rdstate();
cin.clear();
cin.setstate(old_state);
// 将failbit、badbit复位,其他位保持不变
cin.clear(cin.rdstate() & ~cin.failbit & ~cin.badbit);
上述代码中,rastate
函数返回一个 iostate
值,对应当前流的状态;setstate
相当于把输入的 iostate
“叠加”到流的原状态上,可以理解为或运算;无参数的 clear
复位所有错误标志位,带参数的 clear
接受一个 iostate
参数,表示流的新状态。
8.1.3 管理输出缓冲(P281)
每个输出流都有一个缓冲区,用来保存程序读写的数据。保存在缓冲区中的数据可能随后再打印。
导致缓冲刷新(数据真正写到输出设备或文件)的原因:
- 程序正常结束,作为
main
函数return
操作的一部分。 - 缓冲区满。
- 使用操纵符(如
endl
)显式刷新缓冲区。 - 使用操纵符
unitbuf
设置流的内部状态。cerr
是默认unitbuf
的,因此写到cerr
里的内容是立即刷新的。 - 和输入流关联的输出流在输入流读取数据前会刷新缓冲区。
刷新输出缓冲区
cout << "hi" << endl; // 输出hi和一个换行,然后刷新缓冲区
cout << "hi" << ends; // 输出hi和一个空字符,然后刷新缓冲区
cout << "hi" << flush; // 输出hi,然后刷新缓冲区
unibuf
操纵符
cout << unitbuf; // 将流设置为:接下来每一次写操作之后都进行一次flush操作
cout << nounitbuf; // 回到正常的缓冲方式
如果程序异常终止,缓冲区是不会刷新的。所以我们在调试崩溃的程序时,需要检查输出是否停留在缓冲区内。
关联输入和输出流
当一个输入流被关联到一个输出流时,任何试图从输入流中读取数据的操作都会先刷新输出流的缓冲区。标准库将 cin
和 cout
关联在一起。
// 将cin关联到cout(仅仅用来展示,相当于什么都没有做)
cin.tie(&cout);
ostream *old_tie = cin.tie();
// 让cin不与任何流关联
cin.tie(nullptr);
// 将cin关联到cerr
cin.tie(&cerr);
cin.tie(old_tie);
tie
的无参数版本返回指向输出流的指针,如果本对象关联到一个输出流,则返回那个输出流的指针;如果本对象没有关联的输出流,则返回 nullptr
。tie
的单参数版本接受一个指向输出流的指针,将自己关联到此输出流。
每个流最多与一个流关联,但多个流可以同时关联到一个输出流(单向关联)。
8.2 文件输入输出(P283)
头文件 fstream
定义了三个类型来支持文件 IO :
ifstream
:从一个给定文件读取数据。ofstream
:向一个给定文件写入数据。fstream
:读写给定文件。
除了继承自 iostream
的操作外,fstream
中还定义了一些新的成员:
8.2.1 使用文件流对象(P284)
当我们想要读写一个文件时,可以定义一个文件流对象,并将之与文件关联起来。每个文件流类都定义了名为 open
成员函数来定位并以指定模式打开文件。创建文件流对象时,如果我们提供了文件名,则 open
会自动调用:
ifstream in(ifile); // 构造一个ifstream并打开文件
ofstream out;
在 C++ 新标准中,文件名可以是 string
对象或 C 风格字符数组,旧版本的标准库只允许 C 风格字符数组。
用fstream
代替iostream&
在要求使用基类对象的地方,我们可以用继承类对象代替。这意味着一个接受 iostream
类型引用或指针参数的函数,可以用相应的 fstream
对象代替。
成员函数open
和close
ifstream in(ifile); // 构造一个ifstream并打开文件
ofstream out;
out.open(ifile + ".copy");
如果 open
调用失败,fallbit
会被置位。检测 open
是否成功是个好习惯:
if(out)
当一个文件流与对应文件成功关联,我们称文件流已经打开。用一个已经打开的文件流调用 open
会导致 failbit
被置位。所以如果我们将文件流关联到另一个文件,我们必须先使用 close
关闭当前文件:
in.close();
自动构造和析构
当一个 fstream
对象被销毁时,close
会自动调用。
8.2.2 文件模式(P286)
每个流都有一个关联的文件模式(file mode),用来指出如何使用文件:
无论用哪种方式打开文件,我们都可以指定文件模式:
- 只有
ofstream
和fstream
能使用out
模式。 - 只有
ifstream
和fstream
能使用in
模式。( VS2022 似乎没这两条限制?这两条似乎与后面的冲突啊 … ) - 只有当
out
模式被设定时,trunc
模式才能被设定。 - 只要
trunc
模式没被设定,就可以设定app
模式。在app
模式下,文件综艺输出方式打开。 - 默认情况下,以
out
模式打开的文件会默认被截断,为了保留文件内容,可以同时指定add
或in
模式。 ate
和binary
模式可以用于任何类型的文件流对象。
每个文件流类型都有默认的文件模式,如果我们没有指定文件模式,就使用此模式。ifstream
默认使用 in
模式,ofstream
默认使用 out
模式,fstream
默认使用 in
和 out
模式。
以out
模式打开会丢失数据
默认情况下,我们用 ofstream
对象打开文件时,文件内容的会被丢弃。
每次调用open
时会确定文件模式
对于一个给定的流,每当打开文件时都可以改变文件模式。
8.3 string
流
sstream
头文件定义了三个支持内存 IO :
istringstream
:从string
读取数据。ostringstream
:向string
写入数据。stringstream
:读写string
数据。
8.3.1 使用istringstream
假设我们要处理一个文件,文件的内容时人名和他们使用的电话号码:
morgan 2015552368 8625550123
drew 973550130
lee 6095550132 2015550175 800555000
我们首先定义一个类来保存数据:
struct PersonInfo {
string name;
vector<string> phones;
};
然后完成程序:
string line, word;
vector<PersonInfo> people;
while (getline(cin, line)) {
PersonInfo info;
istringstream record(line);
record >> info.name;
while (record >> word) {
info.phones.push_back(word);
}
people.push_back(info);
}
8.3.2 使用ostringstream
(P289)
接上面的例子,如果我们希望逐个验证电话号码并改变其格式 ,并将改变格式后的有效代码输出,并打印无效号码的错误信息:
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())
cout << entry.name << " " << formatted.str() << endl;
else
cerr << "input error: " << entry.name << "invalid number(s)" << badNums.str() << endl;
}
}