关于C/C++ stdin缓冲区以及对字符输入的一些经验和心得

关于C/C++ stdin缓冲区以及对字符输入的一些经验和心得

  在使用C/C++编写控制台应用或acm竞赛的时候,I/O方式无非是标准输入输出,特别是acm竞赛,就本人来说,由C语言入门,输入方式还只会scanf,自从学了C++,便深深地被 cin/cout输入输出流的简洁用法所吸引,相信有这种感觉的不止我一个人。
  所以很长一段时间,日常的训练和各种线上比赛,再也没有使用过scanf,反手一个cin感觉很炫酷。然而好景不长,一次bestcoder的常规线上赛,前期发挥稳定,手感相当好,1001和1002快而准地ac,1003也很快来了思路(两年前的事情了,细节什么的早忘了),咔咔咔敲完代码提交ac,最后剩下充足的时间攻克1004,虽然到结束也没做出来,但是3题铁定涨分啊……
  然而终判的结果让我大吃一鲸,T!L!E!,居然超时,百思不得其解之时(其实之前知道cin效率低,但是用着太顺手了就没在意),想到了会不会是数据太多?然后等着终判结束题目开放,抱着试一试的心态,把cin改成了scanf,然后……居然……秒过……
  然后网上查阅资料(只说输入,输出大同小异),cin慢的原因很多,其中很重要的一点是为了使cinscanf可以兼容混合使用,cin在内部实现的时候会同步输入缓冲区,也就是说,输入流会时刻与输入缓冲保持同步,这是一个很耗时的操作,所以就导致了在大量输入数据的时候,cin会比scanf慢很多,可以说,这个慢,是数量级上的差异。
  如果你可以保证程序中不会出现标准输入与流输入混用的情况,可以在程序开始时使用ios::sync_with_stdio(false);关闭同步来提高速度,但是在大量数据面前输入速度仍显得乏力,相比scanf还是慢了一些(上面说的1003题我用cin关同步还是超时,只有scanf能过),个人认为原因在于对输入流对象的封装和>>这个符号的运算符重载导致执行时间变慢。
  所以从那以后,在acm生涯里再也没有使用过cin……硬生生地改回了scanf的习惯。简洁和效率总要舍弃一个,对于算法竞赛来说,效率才是关键吧……


  说了这么多cin与scanf的速度比较,接下来重点说一下scanf,用法不必多说,结合多年来竞赛经验,介绍一下格式符%c与其他格式符的区别和特定使用场景的注意事项。

  %c是一个很奇葩的设定,单独读入一个字符,包括不可见的控制字符(换行等),而其他格式化符号(如%d %lld %f %lf %s等)会在读入未完成时将换行符、空格、制表符等空白字符统统舍弃忽略,直到读到了足够的数据或遇到文件结尾才结束。我们平时控制台输入时通常按行输入,也就是输入数据后要敲击回车才能被读取,这样就导致了换行符在%c与其他格式符号并存的程序中出现各种问题,例如无法获得理想输入数据,字符串错误错误导致程序崩溃等。

例如下面这段程序片段:

int a,b;
char c;
scanf("%d %d", &a, &b);
printf("a=%d b=%d\n", a, b);
scanf("%c", &c);
printf("char=%c\n", c);

理想状态下,我们输入以下数据:

11 66
a

  输出结果应该与输入一致,也就是说输入11 66后敲下回车紧接着就会出现a=11 b=66,然后再输入a敲回车会出现char=a。但事实上输入11 66敲回车得到a=11 b=66以后,并不会再等待输入字符,而是在出现char=一个空行之后,直接结束,如下图:

运行结果

  为什么会出现这种情况?我们先来想两个问题,为什么程序在遇到scanf等输入操作的时候,会停在那里发生阻塞?为什么我们输入完成后,还要敲一下回车才能有反应?

为什么发生阻塞?
简单点说,程序在scanf处发生了I/O请求,需要数据,而scanf需要从输入缓冲区读取数据,程序刚运行的时候,这个缓冲区是空的,所以scanf得不到数据,就会阻塞程序,一直等待缓冲区内出现数据,此时我们从控制台输入内容,敲下回车,输入的内容便会传送给程序输入缓冲区,被scanf阻塞的程序发现缓冲区里有内容了,就会让scanf继续执行,读入数据。


为什么要敲回车?
默认情况下,我们在控制台的输入内容是不会立刻同步到缓冲区的,也许是为了防止误输入或效率问题,只有敲下回车的时候,输入内容连同换行符才会被一起传送至缓冲区,但实际上,被传送至缓冲区的换行符通常是我们所不需要的,它只是我们从控制台输入内容时所要按下的一个键而已,并不是我们需要的数据。

  当缓冲区内有了数据,scanf便开始按照设定的格式进行读取,除%c格式符以外,scanf会按照格式里的内容从左至右读取指定格式的数据。

  我们回看之前的例子,单步解读程序,格式内容"%d %d",我们输入11 66敲回车后,输入缓冲区内容变为11 66\n(用\n代表换行符),scanf先尝试读取格式内容里的第一个%d,也就是读整数,从缓冲区里成功读到了11,此时缓冲区剩下66\n(注意开头有个空格),然后尝试读取第二个%d,由于此时缓冲区开头的内容是空格,%d不理睬 ,忽略开头若干空白字符,然后遇到66,成功读入,此时scanf没有别的要读的了,结束,函数返回2(读入的数量),此时缓冲区剩余\n,然后程序执行printf,再执行scanf("%c", &c);,由于此时缓冲区有内容\n,所以不阻塞,直接读取,开头说到%c会读取一切字符,所以换行符自然而然被读到了 ,所以变量c的内容是'\n',带入printf("char=%c\n", c);,就可以明白为什么会出现图示的现象了。

  如何解决?方案就是在每次使用scanf之后,调用fflush(stdin);来清空输入缓冲区(也就是清掉那个恼人的换行符),然而这样做很麻烦并且几乎所有oj都拒绝这种危险的操作,所以一般使用getchar();来消除换行符(实际就是读入但不赋值给变量)。

  getchar()scanf("%c")一样,可以读入任意字符,所以我们每次要使用scanf("%c")时,不妨先检查在此之前是否有其他的输入行,如果有的话,记得在这两次输入之间加上getchar()来抵消敲击回车所产生的换行符。上例,修改后如下:

int a,b;
char c;
scanf("%d %d", &a, &b);
printf("a=%d b=%d\n", a, b);
getchar();
scanf("%c", &c);
printf("char=%c\n", c);

运行,我们依次输入11 66,敲回车,输入a,敲回车,结果如下:

运行结果2

这下就正常了。

下面是一个典型的输入场景:


矩阵类字符输入
描述: 第一行输入两个整数n,m,接着n行,每行m个字符
样例:

2 3
abc
def

分析:

我们先不考虑怎么一步步输的,假设把整个输入内容送到缓冲区,然后我们缓冲区内的字符序列是这样的:2 3\nabc\ndef\n,观察这个序列,a,d前面都有换行,所以读取这两个字符时,换行会产生干扰,当然最后一个换行程序都快结束了留着也没什么卵用,所以我们需要抵消三个换行,分别在输完2 3以后和每行的字符输完以后。

实现1:

char a[10][10];
int n,m;
scanf("%d %d", &n, &m);
getchar(); // 接下来要读字符,而这里产生了额外的换行,吃掉
for(int i = 0 ; i < n ; i++)
{
    for(int j = 0 ; j < m ; j++)
    {
        scanf("%c", &a[i][j]);
    }
    getchar(); // 读完了m个字符,也就是一行,产生了一个换行符,而接下来的外层循环要读下一行的字符,所以要吃掉它
}

实现2(输入不包含空格):

如果题目明确表示或暗示输入字符不包含空格,可以将每行字符作为一个不含空格的字符串输入,也就是使用scanf(“%s”),这样我们就不用考虑换行符的抵消问题了,因为前面说过,除了%c,其他格式符号都不会care换行符。
下面实现的前提是字符中不包含空格

char a[10][10];
int n,m;
scanf("%d %d", &n, &m);
for(int i = 0 ; i < n ; i++)
{
    scanf("%s", a[i]);
    //  这样相当于直接将abc按照顺序依次存入a[0][0],[0][1],[0][2],并自动在a[0][3]加入字符串结束标志'\0',方便调试。
}

实现3(包含空格但不想用scanf):

如果觉得scanf太麻烦,而且输入的字符的确包含空格,那么可以用gets()函数,不过这个函数由于在设计时存在缓冲区溢出漏洞,C++标准里并不推荐使用,但是日常训练和竞赛只要稍加注意并不会出现溢出问题(只要字符数组够大就没事),所以这个函数也是很好用的。
gets()函数会读入一行字符,它会一直读输入缓冲区内的字符序列直到遇到了'\n'才会停止,值得注意的是,gets()以换行符为界,并不会把换行符作为输入的一部分而读进字符串,但会消耗掉换行符,这点与scanf有所不同。
例如输入缓冲区内容为abc def gh 666\n233,gets函数从该缓冲区读取到的内容为abc def gh 666,而缓冲区剩余233'\n'为gets函数做“路标”但惨遭gets函数“抛弃”。当然,如果缓冲区第一个字符就是换行符,则gets会读入一个空字符串并消耗掉缓冲区的这个换行符。上例代码:

char a[10][10];
int n,m;
scanf("%d %d", &n, &m);
getchar(); // 此时缓冲区第一个字符是换行符,不消除的话下一个gets会读到空字符串
for(int i = 0 ; i < n ; i++)
{
    gets(a[i]);
    // 这里不需要为下一次循环的gets做换行抵消,因为gets本身就会在本行输入结束时抵消换行符
}

  总结来说,了解了缓冲区的作用和各种输入对于控制符的处理,再处理起字符类输入问题就得心应手了,acm生涯里不乏遇到各种姿势的变态输入格式,有的时候数据输入完了再输出来就变得不一样了,还有的时候输入数据复杂到整个题目的时间都用在了研究输入上,所以掌握好基本的数据输入才是ac的第一步。

你可能感兴趣的:(心得体会,SDUT,OJ)