What & How & Why

IO类

C++ Primer 笔记 第八章


IO类

C++ 提供了全面的 IO 类用于读写:

  • 支持 basic type stream 读写的 Iostream
  • 支持 File 读写的 iofstream
  • 支持 In-memory string 读写的 stringstream

以上版本都分别拥有应用于 wchart_t 类型的版本:

IO类型之间的关系

概念上来讲,输入输出操作不应该因操作对象而异。C++ 通过继承Inheritance)的方式来保证读 / 写的一致性。IO 类的继承关系如下图:




继承也包括了操作继承。比如 ifstream 继承自 istream,因此 ifstream 也能使用 stream 操作符(“»” , “«”)。

IO对象不能拷贝或赋值

IO类的对象是不能被拷贝也不能被赋值的。因此:

  • stream 类型不能作为 parameter,同时也不能作为返回类型。
  • 由第一条,stream 类型的传递和返回都是通过引用
  • 因为 读写操作一定会改变 stream 对象的状态,所以传递和返回的必须是 普通引用

Condition States

IO 类提供了一系列的 flag / 函数 来判断和操作 stream 当前的状态:



一般情况下,输入的流程如下:

  • 输入检测
  • 如果检测到与期望不符合输入,stream 的状态就会变为错误状态,此时之后的输入也会失败。

通常可以使用条件来判断输入是否有效:

while (cin >> word)
    //ok: read op successful

Interrogating the State of a Stream

Flag 的作用是什么?

stream 做为条件时,只能反馈 stream 是否有效。通过 flag 可以判断 stream 是出于什么原因无效。

一共存在着 4 种类型为 strm::iostate 的 flag,分别指代不同的 IO 状况,其中:

  • badbit: system failure, 不可恢复的读写操作。
  • failbit: 可恢复的错误,比如读取 char 的时候输入一个整数。
  • eofbit: reaching end-of-file.
  • goodbit: 0, 说明 stream 正常运作。

除此之外还有一些用于判定的成员函数。比如 fail() 函数,之前以 stream 对象作为条件的判断:

while(cin)
实际上等同于
while(!cin.fail())

所有的 flag 都支持位运算

Managing the Condition State

除了以上判定函数之外,IO 类还提供了一系列的管理函数用于检测问题的类型和改变 stream 的状态:

  • rdstate():返回当前的状态
  • setstate(flags): 指定当前的状态
  • clear / clear(flags):重置当前的状态到 0 或者指定状态

// 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
使用位运算可以很方便的设定 flag:
// rdstate returns a bit sequence which repersents the status of all bits
// if any bit is on (1),  neg it to 0, then and it with the bit sequence
// so that we can turns off failbit and badbit but all other bits unchanged, in this case
cin.clear(cin.rdstate() & ~cin.failbit & ~cin.badbit);

管理输出buffer

Buffer 的作用是存储程序的输入和输出。Buffer的由 ostream 管理,可以将好几种不同类型的输出组合成单一的系统级别操作。这样的方式可有效提升输出的执行效率。
Buffer 在以下条件下会被 flush(刷新):

  • 程序正常完成。
  • 缓冲区存满,需要刷新后再接受输入
  • 缓冲区被操作符显式清空,比如 endl, flush, ends 之类的操作符
  • unitbuf 操作符可以再每次输出后清空Buffer, 主要用于 cerr 对象
  • 两个 stream 对象绑定的时候,对其中一个对象的操作会导致另一个对象的清空。例如: cincout
刷新输出buffer

上面提到了有三种显式刷新 buffer 的方法:endl, flush, ends。 他们的区别主要在于:

  • endl 将内容输出→换行→flush buffer。
  • flush 将内容输出→flush buffer。
  • ends 将内容输出并在默认添加一个空字符,, 然后 flush buffer

除此之外还有:

  • unitbuf ,用于每次输出内容后flush buffer
  • nounitbuf,恢复上面的状态到正常的 flush buffering

使用的方法:

cout << endl;
cout << flush;
cout << ends;
cout << unitbuf;
cout << nounitbuf;

注意:如果程序非正常退出(崩溃),buffer 是不会自动 flush 的。所以在 debug 的时候一定要注意考虑 buffer 里的内容:你很可能更新了程序,但得到的还是原来的结果,原因就是 buffer 在作怪。

绑定输入输出

为什么要进行绑定?

之所以这么做事因为我们通常希望在进行交互的时候给予用于一个干净的 Buffer 用于输入。绑定的结果是对某一个绑定对象进行输入时,会刷新另外一个对象的 buffer。通过绑定,我们可以确保在用于输入信息之前,buffer 区的输出信息已经刷新完毕了。

C++ 通过什么手段进行绑定?

通过 tie 成员函数。该函数分两个版本:

tie(); // return a pointer that points to a tied output stream, or a nullptr if the stream is not tied.
tie(&ostream); //return the pointer that points to the ostream and ties ostream to the caller.
常见用法:
cin.tie(&cout); //demonstration only, cin / cout has been tied by default.
ostream *old_tie = cin.tie(nullptr); // The pointer old_tie points to the ostream that cin is currently tying.
cin.tie(&cerr); //ties cin to cerr
cin.tie(old_tie); //ties cin & cout again.
有几点需要注意的是:

  • 绑定的过程实际上是在传递被绑定对象的指针。
  • 绑定关系是一对一的,但绑定对象是可以替换的。

文件输入输出

使用文件输出类的时候,我们需要调用 <fstream> header。这个 header 定义了三种类型来支持文件输出输出:ifstream, ofstream, iofstream。因为这个类继承自 iostream,所以我们对 fstream 对象也可以使用 cin, cout 等对象使用的类成员。而继承类自己也定义了一些类型和函数用于特定的操作:

fstream fs; // creates an unbound file stream
fstream fs(s); // create a file stream and open the file s.s can be a string or pointer to a cstring.
fstream fs(s, mode); // create a fstream named s, with specified mode.
fs.open(s); //open the file named s.
fs.open(s, mode); // open the file name s, with specified mode.
fs.close(); // close fstream fs.
fs.isopen(); // return a bool that indicating whether the file associated with fs was opened, or closed.

使用 fstream 对象

对文件进行读写,首先需要两个对象:

ifstream in(ifile); //ifstream type 'in', reading from file 'ifile'.
ofstream out; //ofstream type 'out'
out.open(ifile + ".copy"); //output to file "ifile.copy"
其次,为了确保在输出正常的情况下才进行输入,需要判定输出对象的状态:
if (out)
如果输出对象不在正常状态则不会进行输入。

需要注意的是,文件被打开以后,就会与指定的输入输出对象绑定,因此对已绑定的对象再进行 open 函数的调用会直接导致失败,并设置 failbit flag. 如果希望从不同的文件中读入 input,需要先关闭当前绑定,再建立新的绑定
in.close();
in.open("newfile");

利用 Main 函数的参数自动输入

main函数有两个 parameter: int argc, char argv。利用这两个 parameter 可以循环对多个文件进行输入:

//Since argv[0] stores the program name, the array range for the files begins at 1
for (auto p = argv + 1; p!= argv + argc; ++p) {
    
    //create a file with specific name and open it in each iteration
    ifstream ifs(*p);
    
    //if open successful
    if (ifs) {
        process(ifs);
    } 
    else
        cerr << " couldn't open " + string(*p);
}
由于 ifs 对象属于循环中的局部变量,因此每次迭代完成之后都会自动解除与当前文件的绑定(关闭)。

fstream 对象被销毁时会自动调用 close 函数。

文件模式

open() 函数中的第二个参数:文件模式 ( File Mode) ,决定了 fstream 如何使用文件:



一些需要注意的规则:

  • out 不能对 ifstream 对象时使用,反之 in 不能对 ostream 对象使用(读用 in, 写用 out )。
  • 使用 out 的时候,trunc同时被设定(意味着默认情况下写入文件都会清空文件以前的内容)。
  • app (append) 模式的指定会取消 trunc 的效果 (意味着 appout 搭配使用的话,文件是一直 open 的,也就是说文件中以前的内容不会被删除)
  • atebinary 适用于任何 fstream 对象,也可以和其他 mode 联合使用。
  • mode 如果没有显式声明,会自动调用默认定义(比如 ifstream 对象是 in)。

如果在写入的时候想保留文件中的先前内容,必须指定 app 模式:

ofstream ofs1("file", ofstream::app);
ofstream ofs2("file", ofstream::out | ofstream::app);

Stringstreams

Stringstream 定义了三种类型用于支持 string 在内存中的读写:

  • istringstream: 读取 string
  • ostringsteam: 写 string
  • stringstream: 读 / 写 string

这些类型被定义在 <sstream> 头文件中;除此之外,还有一些相关的操作:

sstream ss; //ss is an umbound stringstream. 
sstream ss(s); //ss is an stringstream that hold a copy of the string s.
s.str(); // returns a copy of the string that ss holds.
ss.str(s); // copies the string s into ss. return void.

使用 stringstream

istringstream 的使用实例

通常情况下,如果我们希望对某一行输入中单个的 string 进行操作,就可以使用到 istringstream 对象。istringstream 会一直读取 stream buffer 中的内容,并以空格作为分界线将之前的内容作为一个 string。该 string 可以存储到对应的变量中。来看看书上的例子:

// members are public by default; see § 7.2 (p. 268)
struct PersonInfo {
	string name;
	vector<string> phones;
};

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
}
整个逻辑是:

  • 使用 getline 获取每一段的输入,这一段的输入包括用户名与用户电话号码
  • 使用临时的 PersonInfo 对象 info 装载数据
  • 使用 isringstream 对象 record 绑定当前的一段输入,对其内部的 string 进行处理:
    • 先添加用户名到 info 对象中,此时 record 输入的是第一个 string
    • 对当前 line 剩下的部分,也就是所有电话号码进行循环读取,并依次存储到成员 info.phone中。
  • 最后将整个 info 对象存储到 PersonInfo vector 中。
ostringstream 的使用实例

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;
}
上面例子希望进行三件事:

  1. 验证所有人的电话是否合法
  2. 将合法电话与不合法电话分别进行输出
  3. 对不合法的电话进行特别的标记

在这里,ostringstream 则是扮演了“暂存分类” 角色:

  • 首先循环检查每个人的电话号码:外层循环是以人为单位,内层循环则是检查对应的所有号码
  • 根据号码是否有效,暂存当前号码到对应的 ostringstream 对象中
  • 根据无效号码对应的 ostringstream 对象 badNums 是否为空,输出指定的信息。