关于C/C++缓冲区的问题

探究:溢出和清理

Posted by Welt Xing on February 27, 2021

杂谈:关于输入输出缓冲区

引入

当我们运行这样的程序:

#include <stdio.h>
int main() {
    int a, b;
    while(scanf("%d%d", &a, &b)) {
        printf("a : %d, b = %d\n", a, b);
    }
    return 0;
}

该程序会接受两个输入,赋值给ab,然后输出它们的值:

1 2
a : 1, b : 2
3 4
a : 3, b : 4

我们再进行这样的测试:

1 2 3
a : 1, b : 2
4
a : 3, b : 4

发现我们在第一次输入中多余的内容,会在第二次输出。我们发现这个模型与一个队列(queue)相似:

  1. 第一次向队列里输入三个元素1,2,3;

     queue.push(1);
     queue.push(2);
     queue.push(3);
    

    此时的队列:队列尾端$\to\begin{bmatrix}3&2&1\end{bmatrix}\to$队列首端。

  2. scanf函数让队列推出两个元素,按顺序赋值给ab

     a = queue.pop();
     b = queue.pop();
    

    此时的队列:队列尾端$\to\begin{bmatrix}3\end{bmatrix}\to$队列首端,第一个被推出的是$1$,所以a被赋为1,第二个被推出的是$2$,所以b被赋值为2。

  3. 第二次输入4:

     queue.push(4);
    

    此时的队列:队列尾端$\to\begin{bmatrix}4&3\end{bmatrix}\to$队列首端。

  4. scanf函数再次让队列推出两个元素,按顺序赋值给ab

     a = queue.pop();
     b = queue.pop();
    

    此时的队列:队列尾端$\to\begin{bmatrix}\end{bmatrix}\to$队列首端,第一个被推出的是$3$,所以a被赋为3,第二个被推出的是$4$,所以b被赋值为4。

缓冲区(buffer)就是这样的队列。

关于缓冲区溢出

我们在测试程序中,第一次输入的三个数,那如果是300个数,3000个数甚至是30000个数呢?我们知道计算机中的资源是有限的,缓冲区这个队列肯定也是有长度限制的,如果我们往缓冲区中推入过量的数据,就会产生缓冲区溢出(buffer overflow)。当然,上面所举的例子是输入流的缓冲区,实际的计算机系统里到处都是缓冲区:堆、栈都是一个缓冲区,都会发生缓冲区溢出。

清理缓冲区的必要性

相比于缓冲区溢出,在与输入输出相关的情况下,更让人烦恼的其实还是缓冲区残余内容对后续输入的影响,我们再举一个输入相关的例子,下面的程序是输入数组,然后求他们的平均数:

int main() {
    int n;
    while(cin >> n) {
        double sum = 0;
        double temp;
        for(int i = 0;i < n; i++) {
            cin >> temp;
            sum += temp;
        }
        cout << sum / (double)n << endl;
    }
}

加入用户第一次想输入三个数字,但一不小心输入了四个数字,那么第四个数字就会被赋给n影响后续的计算:

3
1 2 3 4
2
3 1 2 3
2.25

第二次计算的是4个数字的均值sum(3, 1, 2, 3) / 4,但用户想要计算三个数字的均值sum(1, 2, 3) / 3,这样无疑会影响程序使用者的用户体验。因此在每次输入后往往需要清除缓冲区。

清除缓冲区的方法

fflush(stdin)

C语言中使用fflush(stdin)语句来清除输入缓冲区,我们可以将其加在程序中查看效果:

int main() {
    int n;
    while(cin >> n) {
        double sum = 0;
        double temp;
        for(int i = 0;i < n; i++) {
            cin >> temp;
            sum += temp;
        }
        cout << sum / (double)n << endl;
        fflush(stdin);
    }
}

采用相同的输入,查看测试结果:

3
1 2 3 4
2
3 1 2 3
2

发现第一次输入中多余的部分被舍弃。第二次计算的结果是符合预期的。

再看一个简单的程序:

#include <stdio.h>
int main()
{
    int i;
    for (;;) {
        fputs("Please input an integer: ", stdout);
        scanf("%d", &i);
        printf("%d\n", i);
        // fflush(stdin);
    }
    return 0;
}

你可以输入一个字母,或是一个小数,看看会输出什么,然后在注释的位置加入fflush(stdin),再进行相同的输入,观察结果变成了什么。

而实际上,==这种用法是不规范的,至少是移植性不好的==,首先是有些编译器并不支持该用法,第二是如果stream指向输入流(如 stdin),那么fflush函数的行为是不确定的。这点可以参考C99手册(Linux环境下命令行输入man fflush)即可查阅。

C的清空缓冲区

我们可以在scanf后面增加一些内容来实现缓冲区的清除:

#include <stdio.h>
int main()
{
    int i;
    for (;;) {
        fputs("Please input an integer: ", stdout);
        if (scanf("%d", &i) != EOF) { /* 如果用户输入的不是 EOF */
            /* while循环会把输入缓冲中的残留字符清空 */
            /* 读者可以根据需要把它改成宏或者内联函数 */
            while ((c = getchar()) != '\n' && c != EOF) {
                ;
            }
        printf("%d\n", i);
        }
    }
    return 0;
}

C++的清空缓冲区

C++中我们会先调用cin.clear()来清除输入流的错误标记,然后调用

cin.ignore( std::numeric_limits<std::streamsize>::max( ), '\n' );

来清理缓冲区。