C++ Primer学习笔记 - 第8章IO库
C++不直接处理输入输出(IO),而通过一组标准库中定义的类型来处理IO。如istream, ostream等类。
已介绍常用IO库:
- istream 输入流类型,提供输入操作
- ostream 输出流类型,提供输出操作
- cin istream对象,从标准输入读取数据
- cout ostream对象,从标准输出写入数据
- cerr ostream对象,用于输出程序错误信息,写入标准错误
- >> 运算符,用来从一个istream对象读取输入数据
- << 运输费,用例从一个ostream对象写入输出数据
- getline()函数,从一个给定的istream读取一行数据,存入一个给定的string对象
8.1 IO类
根据支持字符类型:IO库支持char数据和wchar_t(宽字符)。
根据支持的读写类型:
iostream 定义了用于读写流的基本类型;
fstream 定义了读写文件的类型;
sstream 定义了读写内存string对象的类型;
cin, cout, cerr宽字符版本分别是wcin, wcout, wcerr。
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 |
8.1.1 IO对象无拷贝或赋值
典型地,不能拷贝cin, cout, cerr对象,或者直接赋值。函数形参需要传递时,可以设为引用类型。而且读写一个IO对象会改变其状态,因此传递和返回的引用不能是const。
ofstream out1, out2; // 需要#include <fstream>
out1 = out2; // 错误,流对象不能赋值
ofstream print(ofstream); // 错误,不能初始化ofstream参数
out2 = print(out2); // 错误,不能拷贝流对象
8.1.2 条件状态
IO操作主要分为三类状态:可恢复的错误状态(failbit,一个字符等错误,流还可以继续使用),不可恢复的错误状态(badbit,系统级错误,流无法继续使用),流结尾(eofbit,流达到文件结束)。
eofbit, failbit, badbit任一个被置位,检测流状态的条件会失败。
goodbit: eofbit, failbit, badbit都无效(为0) 时,goodbit才置位(为1)。
IO库条件状态
strm::iostate | strm是一种IO类型,iostate是一种极其相关的类型,提供了表达条件状态的完整功能 |
strm::badbit | 指出流已崩溃 |
strm::failbit | 指出IO操作失败 |
strm::eofbit | 指出流到达了文件结束 |
strm::goodbit | 指出流未处于错误状态,此值保证为0 |
s.eof() | 若流s的eofbit置位,则返回true |
s.fail() | failbit |
s.bad() | badbit |
s.good() | 若流s处于有效状态,则返回true |
s.clear() | 将流s中所有条件状态复位,将流的状态置为有效。返回void |
s.clear(flags) | 复位flags指定的条件状态位。flags类型是strm::iostate类型。返回void |
s.setstate(flags) | 置flags指定条件状态位 |
s.rdstate() | 返回流s当前条件状态,返回值类型为strm::iostate |
IO错误的例子:
// 发生failbit
int val;
cin >> val;
// 控制台输入ABC会导致cin进入错误状态failbit
// 程序检查条件状态
while(cin >> val) {
// ok 读操作成功
...
}
注意:一个流一旦发生错误,后续的IO操作都会失败,一直到流处于无错误状态后,IO操作才能正常进行。
8.1.3 管理输出缓冲
每个输出流都会管理一个缓冲区,保存程序读写的数据。
例如,下面程序可能立即打印,也可能被操作系统保存在缓冲区中,之后再打印。
cout << "Hello, C++";
缓冲刷新,指的是数据真正写到输出设备或文件。发生缓冲刷新的原因主要有:
- 程序正常执行完毕,即main函数return后;
- 缓冲区满;
- 使用endl等操作符显式刷新缓冲区;
- 用unitbuf操纵符设置流的内部状态,来清空缓冲区;
- 关联流,读写被关联的流时,关联到的流的缓冲区会被刷新。如cin和cerr都关联到cout,读cin或cerr都会导致cout的缓冲区被刷新;
刷新缓冲区
cout << "hello" << endl; // 输出hello和一个换行,然后刷新缓冲区
cout << "hello" << flush; // 输出hello和一个换行,然后刷新缓冲区
cout << "hello" << ends; // 输出hello和一个空字符,然后刷新缓冲区
unitbuf操纵符
cout << unitbuf; // 所有输出操作后立即刷新缓冲区
cout << nounitbuf; // 回到正常的缓冲方式
注意:如果程序崩溃,缓冲区不会被刷新。
关联输入和输出流
当一个输入流关联到一个输出流时,任何试图从输入流读取数据的操作,都会先刷新关联的输出流。如将cout和cin关联到一起,
cin >> val;
将导致cout的缓冲区被刷新。
tie有2个重载的版本:不带参数的版本,返回指向输出流的指针,未关联则返回空指针(nullptr);带参数的版本,接受指向ostream的指针。
cin.tie(&cout); // 将cin关联到cout
ostream *old_tie = cin.tie(nullptr); // cin 不与任何流关联
cin.tie(&cerr); // 关联cin和cerr。读取cin会刷新cerr而不是cout
cin.tie(old_tie); // 恢复cin和cout之间的关联关系(根据old_tie值含义,知无任何关联)
8.2 文件输入输出
fstream定义了3个类型来支持文件IO:
- ifstream 从给定文件读取数据;
- ofstream 向一个给定文件写入数据;
- fstream 读写给定文件;
除了集成自iostream类型的操作:cin, cout, <<和>>运算符,getline 功能一样,fstream中定义的类型还增加了些新成员来管理与流关联的文件。
fstream特有操作
fstream fstrm; | 创建一个未绑定的文件流 |
fstream fstrm(s); | 创建一个fstream,打开名为s的文件,s可为string类型或指向C风格字符串的指针。 构造函数是explicit的,默认的文件模式mode依赖于fstream的类型 |
fstream fstrm(s, mode); | 与前一个构造函数类似,但指定mode打开文件 |
fstrm.open(s) | 打开名为s的文件,并将文件与fstrm绑定。返回void |
fstrm.close() | 关闭 fstrm绑定的文件,返回void |
fstrm.is_open() | 返回一个bool值,指出与fstrm关联的文件是否打开且尚未关闭 |
8.2.1 使用文件流对象
ifstream 和ofstream构造函数构建文件流对象,成员函数open/close打开关闭文件。
string s1, s2;
s1 = "records.txt";
s2 = "output.txt";
ifstream input(s1);
ofstream output;
output.open(s2); // 如果open成功,则open会设置流的状态,使得good()为true
if (output) { // 检查open是否成功。如果open成功,就可以使用文件了
string s;
while (getline(input, s)) {
output << s << endl;
}
}
else {
cout << "output cant be open" << endl;
}
output.close();
input.close();
自动构造和析构
当一个fstream对象被销毁时,close会自动被调用。
e.g.
for (auto p = argv + 1; p != argv + argc; ++p) {
ifstream input(*p);
if (input) {
process(input);
}
else {
cerr << "could't open:" << + string(*p);
}
} // 每次循环,input离开作用域就会被销毁,自动调用close
8.2.3 文件模式
见ios_base.h (mingw32),
/// Seek to end before each write.
static const openmode app = _S_app; // app 每次写操作前均定位到文件末尾
/// Open and seek to end immediately after opening.
static const openmode ate = _S_ate; // 打开文件后立即定位到文件末尾
/// Perform input and output in binary mode (as opposed to text mode).
/// This is probably not what you think it is; see
/// https://gcc.gnu.org/onlinedocs/libstdc++/manual/fstreams.html#std.io.filestreams.binary
static const openmode binary = _S_bin; // 以二进制方式进行IO
/// Open for input. Default for @c ifstream and fstream.
static const openmode in = _S_in; // 以读方式打开
/// Open for output. Default for @c ofstream and fstream.
static const openmode out = _S_out; // 以写方式打开
/// Open for input. Default for @c ofstream.
static const openmode trunc = _S_trunc; // 截断文件
构造函数、open打开文件时,可以指定文件模式,但有如下限制:
- 只能对ofstream或fstream对象设定out模式
- 只能对ifstream或fstream对象设定in模式
- 只有当out也没被设定时,才可设定trunc模式
- 只要trunc没被设定,就可以设定app模式。app模式下,即使没有显式指定out模式,文件也总是以输出方式被打开。
- 默认情况下,即使没有指定trunc,以out模式打开的文件也会被截断。如果要保留out模式打开的文件内容,必须同时指定app模式,这样将追加数据写到文件末尾。如果同时指定in模式,可以同时进行读写操作。
- ate和binary模式可用于任何类型的文件流对象,也可以与其它任何文件模式组合
以out模式打开文件会丢失已有数据
// 下面3条语句,file1都会被截断
ofstream out("file1");
ofstream out2("file1", ofstream::out); // 隐含地截断文件
<=>
ofstream out3("file1", ofstream::out | ofstream::trunc);
// 如果要保留文件内容,必须显式指定app模式
ofstream out("file2", ofstream::app);
ofstream out("file2", ofstream::out | ofstream::app);
每次调用open时都会确定文件模式
ofstream out; // 未指定打开模式,文件隐式地以out模式打开。而out模式通常意味着trunc(截断)模式,除非显式指定app(追加)模式
out.open("outfile1"); // 模式隐含设置为输出和截断
out.close(); // 关闭out,可以将其用于其他文件
out.open("outfile2", ofstream::app); // 模式为输出和追加
out.close();
8.3 string流
sstream头文件定义了3各类型支持内存IO,这些类型可以向string写入数据,从string读取数据:
- istringstream 从string读取数据;
- ostringstream 向string写入数据;
- stringstream 既支持可从string读取数据,也可以向string写数据;
string流有何作用?
可以自动推导出需要转换的类型。
例如,将string转换为int。
// 使用C风格sprintf,对格式化符 "%d" 有严格要求,一旦使用不当可能会造成程序崩溃
int n = 1000;
char s[10];
sprintf(s, "%d", n); // s中的内容为"1000",相当于把int转换为string
puts(s);
// 使用sstream,自动推导出转换的类型
n = 2000;
string res;
stringstream stream;
stream << n;
stream >> res; // res中内容为"1000"
cout << res << endl;
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 |
8.3.1 使用istringstream
使用场景:当某些工作是对整行文本进行处理,而其他一些工作是处理行内的单个单词时,通常可以使用istringstream。
例子,希望从如下输入,使用istringstream绑定string对象,进行解析操作
john 123456 877889
tom 897673864 729832789
jess 34673467 67286372 2323678678
struct PersonInfo {
string name;
vector<string> phones;
};
void f() {
string line, word;
vector<PersonInfo> people;
while (getline(cin, line)) {
PersonInfo info;
istringstream record(line); // 绑定line与istringstream
record >> info.name; // 读取人名
// 循环读取电话号码
while (record >> word) {
info.phones.push_back(word);
}
people.push_back(info); // 将记录追加到people末尾
}
8.3.2 使用ostringstream
使用场景:当逐步构造输出,希望最后一起打印时,ostringstream很有用。
例,接着使用上面的数据结构,由于不希望输出有无效电话号码的人,因此对每个人,直到验证完所有电话号码后才可以进行输出操作,但是可以先将输出内容“写入”到一个内存ostringstream中。
for (const auto &entry : people) {
ostringstream formatted;
for (const auto &nums : entry.phones) {
// 将格式化的字符串写入formatted
formatted << " " << nums;
}
cout << entry.name << " " << formatted.str() << endl;
}