目录

IO类

C++ Primer 笔记 第八章


IO类

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

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

IO类型之间的关系

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




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

IO对象不能拷贝或赋值

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

Condition States

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



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

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

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

Interrogating the State of a Stream

Flag 的作用是什么?

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

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

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

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

所有的 flag 都支持位运算

Managing the Condition State

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

// 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(刷新):

刷新输出buffer

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

除此之外还有:

使用的方法:

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 如何使用文件:



一些需要注意的规则:

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

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

Stringstreams

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

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 则是扮演了“暂存分类” 角色: