【C++基础】第四十七课:[IO库]IO类

IO库,IO操作,IO类,iostream,fstream,sstream,wcin,wcout,wcerr,iostate,badbit,eofbit,failbit,goodbit,good(),eof(),fail(),bad(),rdstate(),clear(),setstate(),刷新输出缓冲区,unitbuf,nounitbuf,tie()

Posted by x-jeff on July 31, 2022

【C++基础】系列博客为参考《C++ Primer中文版(第5版)》C++11标准)一书,自己所做的读书笔记。
本文为原创文章,未经本人允许,禁止转载。转载请注明出处。

1.前言

C++语言不直接处理输入输出,而是通过一族定义在标准库中的类型来处理IO。这些类型支持从设备读取数据、向设备写入数据的IO操作,设备可以是文件、控制台窗口等。还有一些类型允许内存IO,即,从string读取数据,向string写入数据。

IO库定义了读写内置类型值的操作。此外,一些类,如string,通常也会定义类似的IO操作,来读写自己的对象。

在之前的博客中,我们已经使用了大部分IO库设施:

  • istream(输入流)类型,提供输入操作。
  • ostream(输出流)类型,提供输出操作。
  • cin,一个istream对象,从标准输入读取数据。
  • cout,一个ostream对象,向标准输出写入数据。
  • cerr,一个ostream对象,通常用于输出程序错误消息,写入到标准错误。
  • >>运算符,用来从一个istream对象读取输入数据。
  • <<运算符,用来向一个ostream对象写入输出数据。
  • getline函数,从一个给定的istream读取一行数据,存入一个给定的string对象中。

2.IO类

下表列出了不同种类的IO处理操作,分别定义在三个独立的头文件中:iostream定义了用于读写流的基本类型,fstream定义了读写命名文件的类型,sstream定义了读写内存string对象的类型。

为了支持使用宽字符的语言,标准库定义了一组类型和对象来操纵wchar_t类型的数据。宽字符版本的类型和函数的名字以一个w开始。例如,wcin、wcout和wcerr是分别对应cin、cout和cerr的宽字符版对象。宽字符版本的类型和对象与其对应的普通char版本的类型定义在同一个头文件中。例如,头文件fstream定义了ifstream和wifstream类型。

2.1.IO类型间的关系

概念上,设备类型和字符大小都不会影响我们要执行的IO操作。例如,我们可以用>>读取数据,而不用管是从一个控制台窗口,一个磁盘文件,还是一个string读取。类似的,我们也不用管读取的字符能存入一个char对象内,还是需要一个wchar_t对象来存储。

标准库使我们能忽略这些不同类型的流之间的差异,这是通过继承机制(inheritance)实现的。简单地说,继承机制使我们可以声明一个特定的类继承自另一个类。我们通常可以将一个派生类(继承类)对象当作其基类(所继承的类)对象来使用。

后续博文会对继承机制有更详细的介绍。

类型ifstream和istringstream都继承自istream。因此,我们可以像使用istream对象一样来使用ifstream和istringstream对象。也就是说,我们是如何使用cin的,就可以同样地使用这些类型的对象。例如,可以对一个ifstream或istringstream对象调用getline,也可以使用>>从一个ifstream或istringstream对象中读取数据。类似的,类型ofstream和ostringstream都继承自ostream。因此,我们是如何使用cout的,就可以同样地使用这些类型的对象。

本节剩下部分所介绍的标准库流特性都可以无差别地应用于普通流、文件流和string流,以及char或宽字符流版本。

3.IO对象无拷贝或赋值

我们不能拷贝或对IO对象赋值:

1
2
3
4
ofstream out1, out2;
out1 = out2; //错误:不能对流对象赋值
ofstream print(ofstream); //错误:不能初始化ofstream参数
out2 = print(out2); //错误:不能拷贝流对象

由于不能拷贝IO对象,因此我们也不能将形参或返回类型设置为流类型。进行IO操作的函数通常以引用方式传递和返回流。读写一个IO对象会改变其状态,因此传递和返回的引用不能是const的。

4.条件状态

IO操作一个与生俱来的问题是可能发生错误。一些错误是可恢复的,而其他错误则发生在系统深处,已经超出了应用程序可以修正的范围。下表列出了IO类所定义的一些函数和标志,可以帮助我们访问和操纵流的条件状态(condition state)

下面是一个IO错误的例子:

1
2
int ival;
cin >> ival;

如果我们在标准输入上键入Boo,读操作就会失败。代码中的输入运算符期待读取一个int,但却得到了一个字符B。这样,cin会进入错误状态。类似的,如果我们输入一个文件结束标识,cin也会进入错误状态。

一个流一旦发生错误,其上后续的IO操作都会失败。只有当一个流处于无错状态时,我们才可以从它读取数据,向它写入数据。由于流可能处于错误状态,因此代码通常应该在使用一个流之前检查它是否处于良好状态。确定一个流对象的状态的最简单的方法是将它当作一个条件来使用:

1
2
while (cin >> word)
	//ok : 读操作成功......

while循环检查>>表达式返回的流的状态。如果输入操作成功,流保持有效状态,则条件为真。

4.1.查询流的状态

将流作为条件使用,只能告诉我们流是否有效,而无法告诉我们具体发生了什么。有时我们也需要知道流为什么失败。例如,在键入文件结束标识后我们的应对措施,可能与遇到一个IO设备错误的处理方式是不同的。

IO库定义了一个与机器无关的iostate类型,它提供了表达流状态的完整功能。IO库定义了4个iostate类型的constexpr值,表示特定的位模式:

1
2
3
4
5
6
//定义在ios.h中的ios_base类中
typedef unsigned int iostate;
static const iostate badbit  = 0x1;
static const iostate eofbit  = 0x2;
static const iostate failbit = 0x4;
static const iostate goodbit = 0x0;

badbit表示系统级错误,如不可恢复的读写错误。通常情况下,一旦badbit被置位,流就无法再使用了。在发生可恢复错误后,failbit被置位,如期望读取数值却读出一个字符等错误。这种问题通常是可以修正的,流还可以继续使用。如果到达文件结束位置,eofbit和failbit都会被置位。goodbit的值为0,表示流未发生错误。如果badbit、failbit和eofbit任一个被置位,则检测流状态的条件会失败。

标准库还定义了一组函数来查询这些标志位的状态。操作good在所有错误位均未置位的情况下返回true,而bad、fail和eof则在对应错误位被置位时返回true。此外,在badbit被置位时,fail也会返回true。这意味着,使用good或fail是确定流的总体状态的正确方法。实际上,我们将流当作条件使用的代码就等价于!fail()。而eof和bad操作只能表示特定的错误。这4个函数的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//......
iostate         __rdstate_;
//......
inline _LIBCPP_INLINE_VISIBILITY
bool
ios_base::good() const
{
    return __rdstate_ == 0;
}

inline _LIBCPP_INLINE_VISIBILITY
bool
ios_base::eof() const
{
    return (__rdstate_ & eofbit) != 0;
}

inline _LIBCPP_INLINE_VISIBILITY
bool
ios_base::fail() const
{
    return (__rdstate_ & (failbit | badbit)) != 0;
}

inline _LIBCPP_INLINE_VISIBILITY
bool
ios_base::bad() const
{
    return (__rdstate_ & badbit) != 0;
}

4.2.管理条件状态

流对象的rdstate成员返回一个iostate值,对应流的当前状态。

1
2
3
4
5
6
inline _LIBCPP_INLINE_VISIBILITY
ios_base::iostate
ios_base::rdstate() const
{
    return __rdstate_;
}

setstate操作将给定条件位置位,表示发生了对应错误。

1
2
3
4
5
6
inline _LIBCPP_INLINE_VISIBILITY
void
ios_base::setstate(iostate __state)
{
    clear(__rdstate_ | __state);
}

clear成员是一个重载的成员:它有一个不接受参数的版本,而另一个版本接受一个iostate类型的参数。

clear不接受参数的版本清除(复位)所有错误标志位。执行clear()后,调用good会返回true。带参数的clear版本接受一个iostate值,表示流的新状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <iostream>
#include <string>

using namespace std;

int main() {
    auto state1 = cin.rdstate(); //流状态正常,输出0,即goodbit
    cout << "state1 : " << state1 << endl;

    int i;
    cin >> i; //假设此处我们输入一个字符i,这会导致流异常
    auto state2 = cin.rdstate(); //此时流状态异常,输出4,即failbit
    cout << "state2 : " << state2 << endl;

    cin.clear(); //不带参数的clear,相当于直接把流状态置位为goodbit
    auto state3 = cin.rdstate(); //clear之后,state3被置位为0,即goodbit
    cout << "state3 : " << state3 << endl;

    cin.setstate(state2); //当前流的状态是state3,即000(二进制),state2为101(二进制),setstate会对000和101做位的与运算,所以得到101,即4,即failbit
    auto state4 = cin.rdstate(); //state4为failbit,原因见上
    cout << "state4 : " << state4 << endl;

    cin.setstate(state3); //101和000按位做与运算,还是101,即failbit
    auto state5 = cin.rdstate(); //state5为4,即failbit
    cout << "state5 : " << state5 << endl;

    cin.clear(ios_base::eofbit); //带参数的clear,直接把流状态置为参数,这里把流状态置位为eofbit
    auto state6 = cin.rdstate(); //state6为2,即eofbit
    cout << "state6 : " << state6 << endl;
    cin.clear(ios_base::failbit);
    auto state7 = cin.rdstate(); //同理,state7为4,即failbit
    cout << "state7 : " << state7 << endl;
    cin.clear(ios_base::goodbit); //state8为0
    auto state8 = cin.rdstate();
    cout << "state8 : " << state8 << endl;
    cin.clear(ios_base::badbit); //state9为1
    auto state9 = cin.rdstate();
    cout << "state9 : " << state9 << endl;

    cin.setstate(ios_base::goodbit); //此时流状态为state9,即001(二进制,下同),和000做与运算,为001,所以state10还是001,即badbit
    auto state10 = cin.rdstate();
    cout << "state10 : " << state10 << endl;
    cin.setstate(ios_base::badbit); //001和001与运算,为001,所以state11为badbit
    auto state11 = cin.rdstate();
    cout << "state11 : " << state11 << endl;
    cin.setstate(ios_base::failbit); //001与100做与运算,为101,换算为十进制是5,所以state12为5,没有对应的预设状态
    auto state12 = cin.rdstate();
    cout << "state12 : " << state12 << endl;
    cin.setstate(ios_base::eofbit); //101和010做与运算为111,即十进制的7,所以state13为7
    auto state13 = cin.rdstate();
    cout << "state13 : " << state13 << endl;
    cin.setstate(ios_base::eofbit); //同理,state14也为7
    auto state14 = cin.rdstate();
    cout << "state14 : " << state14 << endl;
    cin.setstate(ios_base::failbit); //同理,state15也为7
    auto state15 = cin.rdstate();
    cout << "state15 : " << state15 << endl;
}

5.管理输出缓冲

每个输出流都管理一个缓冲区,用来保存程序读写的数据。例如,如果执行下面的代码:

1
os << "please enter a value: ";

文本串可能立即打印出来,但也有可能被操作系统保存在缓冲区中,随后再打印。有了缓冲机制,操作系统就可以将程序的多个输出操作组合成单一的系统级写操作。由于设备的写操作可能很耗时,允许操作系统将多个输出操作组合为单一的设备写操作可以带来很大的性能提升。

导致缓冲刷新(即,数据真正写到输出设备或文件)的原因有很多:

  • 程序正常结束,作为main函数的return操作的一部分,缓冲刷新被执行。
  • 缓冲区满时,需要刷新缓冲,而后新的数据才能继续写入缓冲区。
  • 我们可以使用操纵符如endl来显式刷新缓冲区。
  • 在每个输出操作之后,我们可以用操纵符unitbuf设置流的内部状态,来清空缓冲区。默认情况下,对cerr是设置unitbuf的,因此写到cerr的内容都是立即刷新的。
  • 一个输出流可能被关联到另一个流。在这种情况下,当读写被关联的流时,关联到的流的缓冲区会被刷新。例如,默认情况下,cin和cerr都关联到cout。因此,读cin或写cerr都会导致cout的缓冲区被刷新。

5.1.刷新输出缓冲区

我们已经使用过操纵符endl,它完成换行并刷新缓冲区的工作。IO库中还有两个类似的操纵符:flush和ends。flush刷新缓冲区,但不输出任何额外的字符;ends向缓冲区插入一个空字符,然后刷新缓冲区:

1
2
3
cout << "hi!" << endl; //输出hi和一个换行,然后刷新缓冲区
cout << "hi!" << flush; //输出hi,然后刷新缓冲区,不附加任何额外字符
cout << "hi!" << ends; //输出hi和一个空字符,然后刷新缓冲区

自己试了一下:

1
2
3
4
5
6
7
int main(int argc, const char * argv[]) {
    cout << "hi"; //这一步执行完,不符合刷新的任何一个条件,按理来说hi应该进缓冲区并且暂时不输出,但实际上还是会输出hi,个人猜测系统应该是有自动刷新机制
    int a = 0;
    a = 1;
    cout << " output" << endl;
    return 0;
}

5.2.unitbuf操纵符

如果想在每次输出操作后都刷新缓冲区,我们可以使用unitbuf操纵符。它告诉流在接下来的每次写操作之后都进行一次flush操作。而nounitbuf操纵符则重置流,使其恢复使用正常的系统管理的缓冲区刷新机制:

1
2
3
cout << unitbuf; //所有输出操作后都会立即刷新缓冲区
//任何输出都立即刷新,无缓冲
cout << nounitbuf; //回到正常的缓冲方式

如果程序异常终止,输出缓冲区是不会被刷新的。当一个程序崩溃后,它所输出的数据很可能停留在输出缓冲区中等待打印。

5.3.关联输入和输出流

当一个输入流被关联到一个输出流时,任何试图从输入流读取数据的操作都会先刷新关联的输出流。标准库将cout和cin关联在一起,因此下面语句:

1
cin >> ival;

导致cout的缓冲区被刷新。

交互式系统通常应该关联输入流和输出流。这意味着所有输出,包括用户提示信息,都会在读操作之前被打印出来。

tie有两个重载的版本:一个版本不带参数,返回指向输出流的指针。如果本对象当前关联到一个输出流,则返回的就是指向这个流的指针,如果对象未关联到流,则返回空指针。tie的第二个版本接受一个指向ostream的指针,将自己关联到此ostream。即,x.tie(&o)将流x关联到输出流o。

我们既可以将一个istream对象关联到另一个ostream,也可以将一个ostream关联到另一个ostream:

1
2
3
4
5
6
7
cin.tie(&cout); //仅仅是用来展示:标准库将cin和cout关联在一起
//old_tie指向当前关联到cin的流(如果有的话)
ostream *old_tie = cin.tie(nullptr); //cin不再与其他流关联
//上一句相当于把指向cout的指针返回给old_tie,cin关联到nullptr,即不再与其他流关联
//将cin与cerr关联;这不是一个好主意,因为cin应该关联到cout
cin.tie(&cerr); //读取cin会刷新cerr而不是cout
cin.tie(old_tie); //重建cin和cout间的正常关联

在这段代码中,为了将一个给定的流关联到一个新的输出流,我们将新流的指针传递给了tie。为了彻底解开流的关联,我们传递了一个空指针。每个流同时最多关联到一个流,但多个流可以同时关联到同一个ostream。