======IO类======
C++ Primer 笔记 第八章\\
----
====IO类====
C++ 提供了全面的 //IO// 类用于读写:
* 支持 basic type stream 读写的 //Iostream//
* 支持 File 读写的 //iofstream//
* 支持 In-memory string 读写的 //stringstream//
以上版本都分别拥有应用于 //wchart_t// 类型的版本:
\\ \\
{{ :cs:programming:cpp:cpp_primer:io_class_1.svg?600 |}}
==IO类型之间的关系==
概念上来讲,输入输出操作不应该因操作对象而异。C++ 通过**继承**(//Inheritance//)的方式来保证读 / 写的一致性。//IO// 类的继承关系如下图:
\\ \\ \\
{{ cs:programming:cpp:cpp_primer:082149512609442.png |}}
\\ \\
继承也包括了操作继承。比如 //ifstream// 继承自 //istream//,因此 //ifstream// 也能使用 //stream// 操作符(">>" , "<<")。
===IO对象不能拷贝或赋值===
IO类的对象是不能被拷贝也不能被赋值的。因此:
* //stream// 类型**不能作为** parameter,同时也**不能作为**返回类型。
* 由第一条,//stream// 类型的传递和返回都是**通过引用**。
* 因为 读写操作**一定会**改变 //stream// 对象的状态,**所以传递和返回的必须是** 普通引用。
===Condition States===
IO 类提供了一系列的 flag / 函数 来判断和操作 stream 当前的状态:
\\ \\ {{ :cs:programming:cpp:cpp_primer:io_flag.svg?600 |}} \\ \\
一般情况下,输入的流程如下:
* 输入检测
* 如果检测到与期望不符合输入,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 对象绑定的时候,对其中一个对象的操作会导致另一个对象的清空。例如: //cin// 和 //cout//。
==刷新输出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.
有几点需要注意的是:
* 绑定的过程实际上是在传递被绑定对象的指针。
* 绑定关系是一对一的,但绑定对象是可以替换的。
====文件输入输出====
使用文件输出类的时候,我们需要调用 '''' 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// 如何使用文件:
\\ \\ {{ :cs:programming:cpp:cpp_primer:file_mode.svg?300 |}} \\ \\
一些需要注意的规则:
* //out// 不能对 //ifstream// 对象时使用,反之 //in// 不能对 //ostream// 对象使用(读用 //in//, 写用 //out// )。
* 使用 //out// 的时候,//trunc// 也**同时被设定**(意味着默认情况下**写入文件都会清空**文件以前的内容)。
* //app// (append) 模式的指定会取消 //trunc// 的效果 (意味着 //app// 和 //out// 搭配使用的话,文件是一直 //open// 的,也就是说文件中以前的内容不会被删除)
* //ate// 和 //binary// 适用于**任何** //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 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 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;
}
上面例子希望进行三件事:
- 验证所有人的电话是否合法
- 将合法电话与不合法电话分别进行输出
- 对不合法的电话进行特别的标记
在这里,ostringstream 则是扮演了“暂存分类” 角色:
* 首先循环检查每个人的电话号码:外层循环是以人为单位,内层循环则是检查对应的**所有**号码
* 根据号码是否有效,暂存当前号码到对应的 //ostringstream// 对象中
* 根据无效号码对应的 //ostringstream// 对象 ''badNums'' 是否为空,输出指定的信息。