《C++Primer》第八章-标准 IO 库-学习笔记(1)

《C++Primer》第八章-标准 IO 库-学习笔记(1)

日志:
1,2020-02-29笔者提交文章的初版V1.0

作者按:
最近在学习C++ primer,初步打算把所学的记录下来。

传送门/推广
《C++Primer》第二章-变量和基本类型-学习笔记(1)
《C++Primer》第二章-变量和基本类型-学习笔记(2)
《C++Primer》第二章-变量和基本类型-学习笔记(3)
《C++Primer》第三章-标准库类型-学习笔记(1)
《C++Primer》第三章-标准库类型-学习笔记(2)

摘要

C++ 的输入/输出(input/output)由标准库提供。标准库定义了一组类型,支持对文件和控制窗口等设备的读写(IO)。还定义了其他一些类型,使 string对象能够像文件一样操作,从而使我们无须 IO 就能实现数据与字符之间的转换。
这些 IO 类型都定义了如何读写内置数据类型的值。此外,一般来说,类的设计者还可以很方便地使用 IO 标准库设施读写自定义类的对象。类类型通常使用 IO 标准库为内置类型定义的操作符和规则来进行读写。

面向对象的标准库

要同时支持或使用不同类型设备以及不同大小的字符流,其复杂程度似乎相当可怕。为了管理这样的复杂性,标准库使用了继承(inheritance)来定义一组面向对象(object-oriented)类
通过继承关联起来的类型都共享共同的接口。当一个类继承另一个类时,这两个类通常可以使用相同的操作。
更确切地说,如果两种类型存在继承关系,则可以说一个类“继承”了其父类的行为——接口。C++ 中所提及的父类称为基类(base class),而继承而来的类则称为派生类(derived class)

IO 类型在三个独立的头文件中定义:iostream定义读写控制窗口的类型,fstream定义读写已命名文件的类型,而 sstream 所定义的类型则用于读写存储在内存中的 string 对象。在 fstream 和 sstream 里定义的每种类型都是从iostream 头文件中定义的相关类型派生而来。

Header Type
iostream istream从流中读取
ostream 写到流中去
iostream 对流进行读写;从 istream 和 ostream 派生而来
fstream ifstream 从文件中读取;由 istream 派生而来
ofstream 写到文件中去;由 ostream 派生而来
fstream 读写文件;由 iostream 派生而来
sstream istringstream 从 string 对象中读取;由 istream 派生而来
ostringstream 写到 string 对象中去;由 ostream 派生而来
stringstream 对 string 对象进行读写;由 iostream 派生而来

istream 是 ifstream 和istringstream 的基类,同时也是 iostream 的基类,而 iostream 则是stringstream 和 fstream 的基类。
iostream 类型由 istream 和 ostream 两者派生而来。这意味着 iostream 对象共享了它的两个父类的接口。也就是说,可使用 iostream 类型在同一个流上实现输入和输出操作。
对 IO 类型使用继承还有另外一个重要的含义:如果函数有基类类型的引用形参时,可以给函数传递其派生类型的对象。这就意味着:对 istream& 进行操作的函数,也可使用 ifstream 或者istringstream 对象来调用。类似地,形参为 ostream& 类型的函数也可用ofstream 或者 ostringstream 对象调用。因为 IO 类型通过继承关联,所以可以只编写一个函数,而将它应用到三种类型的流上:控制台、磁盘文件或者字符串流(string streams)。

国际字符的支持

之前描述的流类(stream class)读写的是由 char 类型组成的流。
此外,标准库还定义了一组相关的类型,支持 wchar_t 类型。每个类都加上“w”前缀,以此与 char 类型的版本区分开来。于是,wostreamwistreamwiostream 类型从控制窗口读写 wchar_t 数据。
相应的文件输入输出类是wifstreamwofstreamwfstream。而 wchar_t 版本的 string 输入/输出
流则是 wistringstreamwostringstreamwstringstream
标准库还定义了从标准输入输出读写宽字符的对象。这些对象加上“w”前缀,以此与 char 类
型版本区分:wchar_t 类型的标准输入对象是 wcin;标准输出是 wcout;而标准错误则是 wcerr
每一个IO 头文件都定义了 char 和 wchar_t 类型的类和标准输入/输出对象。基于流的 wchar_t 类型的类和对象在 iostream 中定义,宽字符文件流类型在 fstream 中定义,而宽字符 stringstream 则在 sstream 头文件中定义。

IO 对象不可复制或赋值

出于某些原因,标准库类型不允许做复制或赋值操作。

ofstream out1, out2;
out1 = out2; // error: cannot assign stream objects
// print function: parameter is copied ofstream print(ofstream);
out2 = print(out2); // error: cannot copy stream objects

这个要求有两层特别重要的含义。

  • 只有支持复制的元素类型可以存储在 vector 或其他容器类型里。由于流对象不能复制,因此不能
    存储在 vector(或其他)容器中(即不存在存储流对象的 vector 或其他容器)。
  • 第二个含义是:形参或返回类型也不能为流类型。如果需要传递或返回 IO对象,则必须传递或返回指向该对象的指针或引用:
ofstream &print(ofstream&); // ok: takes a reference, no copy
while (print(out2)) { /* ... */ } // ok: pass reference to out2

一般情况下,如果要传递 IO 对象以便对它进行读写,可用非 const 引用的方式传递这个流对象。对 IO 对象的读写会改变它的状态,因此引用必须是非const 的。

条件状态

我们需要了解更多IO 标准库如何管理其缓冲区及其流状态的相关内容。谨记所介绍的内容同样适用于普通流、文件流以及 string 流。
实现 IO 的继承正是错误发生的根源。

  • 一些错误是可恢复的;
  • 一些错误则发生在系统底层,位于程序可修正的范围之外。

IO 标准库管理一系列条件状态(condition state)成员用来标记给定的 IO 对象是否处于可用状态,或者碰到了哪种特定的错误。
标准库定义的一组函数和标记,提供访问和操纵流状态的手段。

条件状态 作用
strm::iostate 机器相关的整型名,由各个 iostream 类定义,用于定义条件状态
strm::badbit strm::iostate 类型的值,用于指出被破坏的流
strm::failbit strm::iostate 类型的值,用于指出失败的 IO 操作
strm::eofbit strm::iostate 类型的值,用于指出流已经到达文件结束符
s.eof() 如果设置了流 s 的 eofbit 值,则该函数返回 true
s.fail() 如果设置了流 s 的 failbit 值,则该函数返回 true
s.bad() 如果设置了流 s 的 badbit 值,则该函数返回 true
s.good() 如果流 s 处于有效状态,则该函数返回 true
s.clear() 将流 s 中的所有状态值都重设为有效状态
s.clear(flag) 将流 s 中的某个指定条件状态设置为有效。flag 的类型是strm::iostate
s.setstate(flag) 给流 s 添加指定条件。flag 的类型是 strm::iostate
s.rdstate() 返回流 s 的当前条件,返回值类型为 strm::iostate
表 2. IO 标准库的条件状态

考虑下面 IO 错误的例子:

int ival;
cin >> ival;

如果在标准输入设备输入 Borges,则 cin 在尝试将输入的字符串读为 int型数据失败后,会生成一个错误状态。类似地,如果输入文件结束符(end-of-file),cin 也会进入错误状态。而如果输入 1024,则成功读取,cin将处于正确的无错误状态。
流必须处于无错误状态,才能用于输入或输出。检测流是否用的最简单的方法是检查其真值:

if (cin)// ok to use cin, it is in a valid state
while (cin >> word)// ok: read operation successful ...

if 语句直接检查流的状态,而 while 语句则检测条件表达式返回的流,从而间接地检查了流的状态。如果成功输入,则条件检测为 true。

条件状态的类型

许多程序只需知道是否有效。而某些程序则需要更详细地访问或控制流的状态,此时,除了知道流处于错误状态外,还必须了解它遇到了哪种类型的错误。例如,程序员也许希望弄清是到达了文件的结尾,还是遇到了 IO 设备上的错误。
所有流对象都包含一个条件状态成员,该成员由 setstateclear 操作管理。这个状态成员为 iostate 类型,这是由各个 iostream 类分别定义的机器相关的整型。该状态成员以二进制位(bit)的形式使用。

每个 IO 类还定义了三个iostate 类型的常量值,分别表示特定的位模式。这些常量值用于指出特定类型的 IO 条件,可与位操作符一起使用,以便在一次操作中检查或设置多个标志badbit标志着系统级的故障,如无法恢复的读写错误。如果出现了这类错误,则该流通常就不能再继续使用了。如果出现的是可恢复的错误,如在希望获得数值型数据时输入了字符,此时会导致设置 failbit 标志,这种导致设置 failbit的问题通常是可以修正的。eofbit是在遇到文件结束符时会设置的,此时同时还设置了failbit

流的状态由 bad、fail、eof 和 good 操作提示。如果 bad、fail 或者 eof中的任意一个为 true,则检查流本身将显示该流处于错误状态。类似地,如果这三个条件没有一个为 true,则 good 操作将返回 true。
clearsetstate 操作用于改变条件成员的状态。

  • clear 操作将条件重设为有效状态。在流的使用出现了问题并做出补救后,如果我们希望把流重设为有效状态,则可以调用 clear 操作。
  • 使用 setstate 操作可打开某个指定的条件,用于表示某个问题的发生。除了添加的标记状态,setstate 将保留其他已存在的状态变量不变。(也就是出了问题时用来设置状态的)

流状态的查询和控制

int ival;
// read cin and test only for EOF; loop is executed even if there are other IO failures
while (cin >> ival, !cin.eof()) {
	if (cin.bad()) // input stream is corrupted; bail out
	throw runtime_error("IO stream corrupted");
	if (cin.fail()) { // bad input
	cerr<< "bad data, try again"; // warn the user
	cin.clear(istream::failbit); // reset the stream
	continue; // get next input
	}
// ok to process ival
}

这个循环不断读入 cin,直到到达文件结束符或者发生不可恢复的读取错误为止。
循环条件使用了逗号操作符。回顾逗号操作符的求解过程首先计算它的每一个操作数,然后返回最右边操作数作为整个操作的结果。因此,循环条件只读入 cin 。该条件的结果!cin.eof() 的值。如果 cin 到达文件结束符,条件则为假,退出循环。如果 cin 没有到达文件结束符,则不管在读取时是否发生了其他可能遇到的错误,都进入循环。
在循环中,首先检查流是否已破坏。如果是的抛出异常并退出循环。如果输入无效,则输出警告并清除 failbit 状态。在本例中,执行 continue 语句回到 while 的开头,读入另一个值 ival。如果没有出现任何错误,那么循环体中余下的部分则可以很安全地使用 ival。

条件状态的访问

rdstate 成员函数返回一个 iostate 类型值,该值对应于流当前的整个条件状态:

// remember current state of cin
istream::iostate old_state = cin.rdstate();
cin.clear();
process_input(); // use cin
cin.clear(old_state); // now reset cin to old state

多种状态的处理

常常会出现需要设置或清除多个状态二进制位的情况。此时,可以通过多次调用 setstate 或者 clear 函数实现。另外一种方法则是使用按位或(OR)操作符在一次调用中生成“传递两个或更多状态位”的值。按位或操作使用其操作数的二进制位模式产生一个整型数值。对于结果中的每一个二进制位,如果其值为 1,则该操作的两个操作数中至少有一个的对应二进制位是 1。
例如:

// sets both the badbit and the failbit
is.setstate(ifstream::badbit | ifstream::failbit);

将对象 is 的 failbit 和 badbit 位同时打开。实参:

is.badbit | is.failbit

生成了一个值,其对应于 badbit 和 failbit 的位都打开了,也就是将这两个位都设置为 1,该值的其他位则都为 0。在调用 setstate 时,使用这个值来开启流条件状态成员中对应的 badbit 和 failbit 位。

输出缓冲区的管理

每个IO 对象管理一个缓冲区,用于存储程序读写的数据。如有下面语句:

os << "please enter a value: ";

系统将字符串字面值存储在与流 os 关联的缓冲区中。下面几种情况将导致缓冲区的内容被刷新,即写入到真实的输出设备或者文件:

  1. 程序正常结束。作为 main 返回工作的一部分,将清空所有输出缓冲区。
  2. 在一些不确定的时候,缓冲区可能已经满了,在这种情况下,缓冲区将会在写下一个值之前刷新。
  3. 操纵符显式地刷新缓冲区,例如行结束符 endl。
  4. 在每次输出操作执行完后,用 unitbuf 操作符设置流的内部状态,从而清空缓冲区。
  5. 可将输出流与输入流关联(tie)起来。在这种情况下,在读输入流时将刷新其关联的输出缓冲区。

输出缓冲区的刷新

endl 操纵符,用于输出一个换行符并刷新缓冲区。
除此之外,C++ 语言还提供了另外两个类似的操纵符。

  • 第一个经常使用的flush,用于刷新流,但不在输出中添加任何字符。
  • 第二个则是比较少用的 ends,这个操纵符在缓冲区中插入空字符 null,然后后刷新它:
cout << "hi!" << flush; // flushes the buffer; adds no data
cout << "hi!" << ends; // inserts a null, then flushes the
buffer
cout << "hi!" << endl; // inserts a newline, then flushes the
buffer

unitbuf 操纵符

如果需要刷新所有输出,最好使用 unitbuf操纵符。这个操纵符在每次执行完写操作后都刷新流:

cout << unitbuf << "first" << " second" << nounitbuf;

等价于:

cout << "first" << flush << " second" << flush;

nounitbuf操纵符将流恢复为使用正常的、由系统管理的缓冲区刷新方式。

将输入和输出绑在一起

当输入流与输出流绑在一起时,任何读输入流的尝试都将首先刷新其输出流关联的缓冲区。标准库将 cout 与 cin 绑在一起,因此下面这条语句导致 cout 关联的缓冲区被刷新:

cin >> ival;

交互式系统通常应确保它们的输入和输出流是绑在一起的。这样做意味着可以保证任何输出,包括给用户的提示,都在试图读之前输出。
tie 函数可用 istream 或 ostream 对象调用,使用一个指向 ostream 对象的指针形参。调用 tie 函数时,将实参流绑在调用该函数的对象上。如果一个流调用 tie 函数将其本身绑在传递给 tie 的 ostream 实参对象上,则该流上的任何 IO 操作都会刷新实参所关联的缓冲区。

cin.tie(&cout); // illustration only: the library ties cin and cout for us
ostream *old_tie = cin.tie();   ////返回指向绑定的输出流的指针。
cin.tie(0); // break tie to cout, cout no longer flushed when cin is read 解除绑定
cin.tie(&cerr); // ties cin and cerr, not necessarily a good idea!
// ...
cin.tie(0); // break tie between cin and cerr  解除绑定
cin.tie(old_tie); // restablish normal tie between cin and cout

一个 ostream 对象每次只能与一个 istream 对象绑在一起。如果在调用tie 函数时传递实参 0,则打破该流上已存在的捆绑。

如果程序崩溃了,则不会刷新缓冲区

如果程序不正常结束,输出缓冲区将不会刷新。在尝试调试已崩溃的程序时,通常会根据最后的输出找出程序发生错误的区域。如果崩溃出现在某个特定的输出语句后面,则可知是在程序的这个位置之后出错
调试程序时,必须保证期待写入的每个输出都确实被刷新了。因为系统不会在程序崩溃时自动刷新缓冲区,这就可能出现这样的情况:程序做了写输出的工作,但写的内容并没有显示在标准输出上,仍然存储在输出缓冲区中等待输出。
如果需要使用最后的输出给程序错误定位,则必须确定所有要输出的都已经输出。为了确保用户看到程序实际上处理的所有输出,最好的方法是保证所有的输出操作都显式地调用了 flush 或 endl。
如果仅因为缓冲区没有刷新,程序员将浪费大量的时间跟踪调试并没有执行的代码。基于这个原因,输出时应多使用 endl 而非 ‘\n’。使用endl 则不必担心程序崩溃时输出是否悬而未决(即还留在缓冲区,未输出到设备中)。

参考资料

【1】C++ Primer 中文版(第四版·特别版)
【2】《C++Primer》第二章-变量和基本类型-学习笔记(3)

注解

【3】多种 IO 标准库提供的工具:

  • istream(输入流)类型,提供输入操作。
  • ostream(输出流)类型,提供输出操作。
  • cin(发音为 c-in):读入标准输入的 istream 对象。
  • cout(发音为 c-out):写到标准输出的 ostream 对象。
  • cerr(发音为 c-err):输出标准错误的 ostream 对象。cerr 常用于程序错误信息。
  • >> 操作符,用于从 istream 对象中读入输入。
  • << 操作符,用于把输出写到 ostream 对象中。
  • getline 函数,需要分别取 istream 类型和 string 类型的两个引用形参,其功能是从 istream 对象读取一个单词,然后写入 string 对象中。

本文许可证

本文遵循 CC BY-NC-SA 4.0(署名 - 非商业性使用 - 相同方式共享) 协议,转载请注明出处,不得用于商业目的。
CC BY-NC-SA 4.0

发布了52 篇原创文章 · 获赞 72 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/engineerxin/article/details/104577831