文章

c++ 输入缓冲区

前言

一、io流状态

C++中的输入输出系统负责记录每一个输入输出操作的结果信息,这些当前的__状态信息__被包含在io_state类型的对象中。io_state是一个枚举类型(就像open_mode一样),以下便是它包含的值。

  • eofbit 已到达文件尾
  • failbit 非致命的输入/输出错误,可挽回
  • badbit 致命的输入/输出错误,无法挽回

这三个标志位均用一位二进制位来表示,0表示清除,1表示设置。

这三个整体构成了流的状态。若三个均为0,表示流状态正常,反之,若有一个为1,流状态非正常。包括达到了文件尾,出现可恢复性错误,出现了不可恢复性的错误。

另外还有一个标志位 goodbit 用来测试流状态是否正常。如果流状态正常返回true,输出1,否则输出0。

二、流状态异常 导致cin的无效

当我们设置了ios::failbit,ios::eofbit,ios::badbit流状态后,由于该三种状态为非正常状态,故设置后,对应的输入流或是输出流就处于无效状态,我们此后对于该流的任何使用均为无效,不起作用。

不妨我们来测试一下:

1
2
3
4
5
6
7
8
9
10
#include <iostream>
using namespace std;
int main()
{
    cin.setstate(ios::failbit);
    int i;
    cin >> i;
    cout << "hello!" << endl;
    return 0;
}

终端显示为:

1
hello!

上面这个程序不等待我们输入直接就输出了结果hello!由此可知,cin状态在设置后已处于无效状态,所有对此的操作均为视为透明的,或是无效的,直接跳过。若要将流状态重置为有效,可以参考文下链接。1

三、缓冲区的理解

缓冲区又称为缓存,它是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。

缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。

缓冲区的类型 缓冲区 分为三种类型:全缓冲、行缓冲和不带缓冲。

  1. 全缓冲 在这种情况下,当填满标准I/O缓存后才进行实际I/O操作。全缓冲的典型代表是对磁盘文件的读写。

  2. 行缓冲 在这种情况下,当在输入和输出中遇到换行符时,执行真正的I/O操作。这时,我们输入的字符先存放在缓冲区,等按下回车键换行时才进行实际的I/O操作。典型代表是键盘输入数据。

  3. 不带缓冲 也就是不进行缓冲,标准出错情况 stderr 是典型代表,这使得出错信息可以直接尽快地显示出来。

缓冲区的刷新

下列情况会引发缓冲区的刷新:

  1. 缓冲区满时;
  2. 执行flush语句;
  3. 执行endl语句;
  4. 关闭文件。2

正文 cin详解

输入原理:

程序的输入都有一个缓冲区,即输入缓冲区。

一次输入过程是这样的,当一次键盘输入结束时会将输入的数据存入输入缓冲区(行缓冲),而cin对象直接从输入缓冲区中取数据。正因为__cin对象是直接从缓冲区取数据__的,所以有时候当缓冲区中有残留数据时,cin对象会__直接取得这些残留数据__而不会请求键盘输入。

一、cin»

输入结束条件 :遇到Enter、Space、Tab键。

对结束符的处理 :

  • 当 cin» 从缓冲区中读取数据时,若缓冲区中第一个字符是空格、tab或换行这些分隔符时,cin» 会将其__忽略__并__清除__,继续读取下一个字符;

  • 若缓冲区为空,则会阻塞等待数据的到来,一旦输入缓冲区中有数据,就触发cin的成员函数去读取数据。

  • 但是如果读取成功,字符后面的分隔符是残留在缓冲区的,cin»不做处理。

总结:cin» 会首先忽略所有分隔符,然后读取缓冲区数据,最后在缓冲区留下 ‘\n’ or ‘\t’ or ‘ ‘。

二、cin.get

输入结束条件:Enter键

对结束符处理:不丢弃缓冲区中的Enter (个人觉得,这里应该说是读取一个字符,将这个字符从缓冲区删除,只是恰好一般输入都需要敲回车,导致多了一个enter,看起来不丢弃罢了)

PS: 若读取单个字符,cin.get(char ch)ch = cin.get() 都可,其中 cin.get() 的返回值是int类型,cin.get(char var) 如果成功返回的是cin对象,因此可以支持链式操作。

例子

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
using namespace std;

int main()
{
    char a;
    char b;
    a = cin.get();
    b = cin.get();
    cout << a << b << endl;
    return 0;
}

终端显示:

1
2
3
e //输入
e //显示
 //显示

注意:

cin.get() 会在缓冲区留下’\n’,从而导致缓冲区非空,这和cin >>一致,然而不同点在于,cin.get()从输入缓冲区读取单个字符时不忽略分隔符(空白符),直接将其读取,导致第二次 cin.get() 不会请求键盘的输入,而是直接接受’\n’。

由于以上原因,有两个cin.get()却只有一次输入,并且一次输出e,一次输出 回车。

读取一行的情况:会在读取的字符串后面自动加上’\0’,同样也不丢弃缓冲区中的Enter(或自定义结束符)3

三、cin.getline

同样 会在读取的字符串后面自动加上’‘\0’,但是__结束符直接从输入缓冲区中删除掉__。

cin.get() 与 cin.getline() 区别:

  1. cin.get()当输入的字符串超过规定长度时,不会引起cin函数的错误,后面的cin操作会继续执行,只是直接从缓冲区中取数据。

    cin.getline()当输入超过规定长度时,会引起cin函数的错误,后面的cin操作将不再执行。

  2. cin.get读取一行时,遇到换行符(自定义结束符)时结束读取,但是不对换行符(自定义结束符)进行处理,换行符(自定义结束符)仍然残留在输入缓冲区。(所以一旦连续两次cin.get,上次换行符的残留,会导致第二个cin.get去缓冲区时,直接遇上换行符,不会请求键盘输入,而且换行符会一直残留,来再多cin.get都没效果)

    cin.getline 读取一行字符时,默认遇到’\n’(自定义结束符)时终止,并且将’\n’(自定义结束符)直接从输入缓冲区中删除掉,不会影响下面的输入处理。4

例子:

  • cin.get() 举例

    对于eofbit,failbit,badbit。0表示正常,1表示被设置。

    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
    
    #include <iostream>
    #include <cstring>
    using namespace std;
      
    int main()
    {
        char a[20], b[20], c[20];
        cin.get(a, 3);
        cout << a << endl;
        cout << "输出当前的流状态" << cin.fail() << endl;
      
        cin.get(b, 20);
        cout << b << endl;
        cout << "输出当前的流状态" << cin.fail() << endl;
          
        cin.get(c, 20);
        cout << (int)*c << endl;
        cout << "输出当前的流状态" << cin.fail() << endl;
          
        cin.get(c, 20);
        cout << (int)*c << endl;
        cout << "输出当前的流状态" << cin.fail() << endl;
          
        return 0;
    }
    

    终端显示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    123456 //输入
    12
    输出当前的流状态0
    3456
    输出当前的流状态0
    0
    输出当前的流状态1
    0
    输出当前的流状态1
    

    解释:

    可以看到a获得12之后,cin正常工作;

    之后,当b获取3456后,’\n’并不读入,而是留在缓冲区,所以之后的两次读入都是空,直有’\0’。

  • cin.getline 举例(输入个数达到上限)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    #include <iostream>
    #include <cstring>
    using namespace std;
      
    int main()
    {
        char a[20], b[20];
        cout << "输出当前的流状态" << cin.fail() << endl;
        cin.getline(a, 3);
        cout << "--------------" << endl;
        cout << a << endl;
        cout << "输出当前的流状态" << cin.fail() << endl;
      
        cin.getline(b, 20);
        cout << (int)*b << endl;
        cout << "输出当前的流状态" << cin.fail() << endl;
      
        return 0;
    }
    

    终端显示

    1
    2
    3
    4
    5
    6
    7
    
    输出当前的流状态0
    123456
    --------------
    12
    输出当前的流状态1
    0
    输出当前的流状态1
    

    解释:

    可以看到 当a去缓冲区读取数据,一直到超过规定长度都没看到终止符时,failbit会被置为1,从而导致流状态异常,下一个读取直接失效。

  • cin.getline 举例(遇到终止符)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
    #include <iostream>
    #include <cstring>
    using namespace std;
      
    int main()
    {
        char a[20], b[20];
        cin.getline(a, 20, '9');
        cout << a << endl;
      
        cin.getline(b, 20);
        cout << b << endl;
      
        return 0;
    }
    

    终端显示

    1
    2
    3
    
    12345912345 //输入
    12345
    12345
    

    解释:

    12345被读入,9被丢弃,缓冲区还有12345,所以不需要键盘输入,第二次cin.getline()获取缓冲区剩余的12345。

输入缓冲区清除方式

cin.clear()是用来更改cin的状态标示符的,cin在接收到错误的输入的时候,会设置状态位good。如果good位不为1,则cin不接受输入,直接跳过。如果下次输入前状态位没有改变那么即使清除了缓冲区数据流也无法输入。所以清除缓冲区之前必须要cin.clear()

1
2
3
4
5
6
#include <limits>

cin.ignore(numeric_limits<std::streamsize>::max(),'\n');//清除输入缓冲区的当前行 
cin.ignore();//清除一个字符

cin.get(str,5).get();//可用于清除后面的换行符,验证了之前cin.get()读一个删一个的说法

numeric_limits<std::streamsize>::max()只是流使用的最大值,也可以用一个足够大的整数代替它。 5

参考链接

本文由作者按照 CC BY 4.0 进行授权