首页 > 技术文章 > C++ Primer学习笔记 - 第8章IO库

fortunely 2021-02-26 23:31 原文

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:

  1. ifstream 从给定文件读取数据;
  2. ofstream 向一个给定文件写入数据;
  3. 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读取数据:

  1. istringstream 从string读取数据;
  2. ostringstream 向string写入数据;
  3. 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;
}

推荐阅读