详解scanf()输入的一些问题

昨天的C2上机中出现了一道考验scanf原理的题《后撤步》,输入格式如下:

后撤步输入格式

第一行一个数 。
第二行是棋子的初始坐标 ( x 0 , y 0 ) (x_0,y_0) (x0,y0)
第三行三个数,分别是圆心坐标 p , q p,q p,q和半径 R R R
接下来 n n n行,每行都是一个向量 ( x i , y i ) (x_i,y_i) (xi,yi)

后撤步输入标程

scanf("%d\n(%lld,%lld)\n%lld%lld%lld\n",&n,&x,&y,&p,&q,&r);

其中\n(输入不当都会出错,习惯了傻瓜输入的我手足无措。疯狂百度。
接下来分几点详解scanf部分原理。全篇萌新友好。

缓冲区

键盘缓冲区是在内存的一块区域,可以以为内存中存在一块槽,键盘输入的字符从这一端按顺序进入缓冲区,用户按下回车或者缓冲区满后,scanf从另一端按顺序处理缓冲区中的字符。
在这里插入图片描述
如有这么一行语句:

scanf("%d%d",&a,&b);

用户为了将123赋值给a,将45赋值给b,用键盘输入
1 ,2 ,3, 空格,4, 5,回车
其中,当用户没有按下回车之前,scanf无动于衷,不开始从缓冲区读取数据,用户按下回车以后,scanf开始从1开始逐个处理数据。

%d忽略前导空白符

对于“输入一行两个用空格分隔的数字”,除了上述写法外,部分同学会使用如下代码:

scanf("%d %d",&a,&b);

同学认为,题目要求我用空格分隔,我便在scanf的格式串中写入空格,符合标准。其实大部分情况下没有必要。格式串中的空白符容易引发奇怪的错误,会在下面解释。
%d有个特性便是忽略前导空白符,所以使用

scanf("%d%d",&a,&b);

便可。分析如下。

键盘输入1 2 3 空格 4 5 \n。scanf的格式串%d%d便开始从缓冲区吃数据。

第一个%d吃掉1 2 3,遇到空格,这个空格对于%d是非法字符,只能吃掉数字的%d不可能接受这个空格,第一个%d的读入便到此为止,此时,a拥有了值123,缓冲区中剩余空格 4 5 回车

第二个%d开始开始读取,它首先遇到空格,由于%d拥有忽略前导空白符的性质。他会忽略所有连续的空白符,直到遇到它想要的数字字符为止。所以第二个%d忽略了前导(缓冲区最前面的)空格,此时第二个%d还没有读到任何东西,缓冲区剩余为 4 5 回车

第二个%d吃掉4 5 ,其后是回车,这个回车对于%d是非法字符,只能吃掉数字的%d不可能接受这个回车,第二个%d的读入便到此为止,a=123,b=45,缓冲区剩余回车,格式串中所有的字符都从缓冲区找到了东西与自己对应,这句scanf运行完成。

注意,并不是什么都能忽略前导空白符,%c是不会忽略前导空白符的。

遇到非法输入,整句scanf停止运作

在这里插入图片描述

有代码

int a=99,b=99,c=99,d=99;
	d=scanf("%d%d%d",&a,&b,&c);
	printf("a=%d,b=%d,c=%d,d=%d",a,b,c,d);

键盘输入1 空格 a 空格 1 \n,试图把1赋值给a,'a’赋值给b,1赋值给c,d是scanf的返回值,代表这句scanf成功赋值的个数。

显然,'a’不可能赋值给int类型的b,现在的问题便是,为b赋值失败,接下来的c能不能接受最后的1,还是说整句scanf因为b遇到了非法输入而就此罢工(提前返回)。

看图,答案很简单,是后者。

也就是说,当scanf的格式串的任何一部分遇到了非法输入,整句scanf都会停止运作(排除%d等可以忽略前导空白符的情况)。

这里的任何一部分包括
①格式说明(如%d,%f等)遇到了他们不能接受的类型。
如上图试图把字母’a’赋值给int类型的%d。
②格式串中的字符常量遇到了不完全相同的字符。
如下:
在这里插入图片描述
格式串是(%d,%d),第一次我们向缓冲区输入< 1 , 2 > \n。按下回车后,格式串里的每一个字符(( , ))或者格式说明(%d)都会依次从缓冲区试图吃掉“属于自己的字符”。

首先,格式串最开始的(想要在缓冲区中找到一个和自己一模一样的字符,但是缓冲区开头是<,无法对应,整句scanf都罢工,现在的缓冲区里是< 1 , 2 > \n,什么都没有被吃掉。

第二次我们严格按照格式输入( 1 , 2 ) \n,格式串里最初的(在缓冲区开头找到了一模一样的(,缓冲区剩下1 , 2 ) \n

接下来轮到了第一个%d读取字符,它读掉了1,缓冲区剩下, 2 ) \n
…以此类推…

当整个格式串格式串(%d,%d)都从缓冲区读取完毕,这句scanf执行完成,缓冲区剩下\n

接下来放一个综合练习,可以自行体会。

在这里插入图片描述

(重点)格式串中的空白符与任意数量的任意空白符匹配

看如下代码。
在这里插入图片描述
格式串是date:%d,%d,%d,但是我们向缓冲区输入的时候,在date前面加了一个空格,导致格式串最初的date中的d无法和缓冲区前端的空格配对,导致整句罢工(注意这里的d不会忽略前导空白符),所有字符都留在缓冲区中,没有任何一个%d读到有效信息,这些运用的都是刚刚讲过的知识。

那么如果换一下格式串,换成空格date:%d,%d,%d,仅仅加了一个小小的空格,就会发生巨大的变化。

在这里插入图片描述
如上,我们可以不向缓冲区输入第一个空格就可以正常运行。

在这里插入图片描述
如上,我们可以向缓冲区输入八百个空格,也可以正常运行。

在这里插入图片描述
如上,我们可以一开始向缓冲区输入Tab*n,回车*n,空格*n,也不影响程序输出正确结果。

为什么呢,因为格式串中的空白符与任意数量的任意空白符匹配。
其中,任意数量包括0个(对应第一张),任意种类包括那些\t \n 空格等,对应第三张。

这样,我们便可以解释一个问题,也是初学者遇到最多的问题之一。

在这里插入图片描述
当格式串最后有一个\n时,按理说,应该输入两个\n就可以让scanf结束运行了,为什么按再多次\n也没有效果,只有随便输入一个非空白符以后才能使scanf运行结束,并且缓冲区中根本没有剩下刚刚输入的那么多\n

答案就是,我们向缓冲区输入再多个\n,都会被格式串%d %d\n中最后的那个\n吃掉,因为格式串中的空白符与任意数量的任意空白符匹配

只有最后输入一个非空白字符,格式串最后的\n才发现,自己在缓冲区的最前端遇到了一个吃不掉(无法匹配)的东西!,这句scanf运行才能就此结束。

重新分析后撤步

相信如果你听懂了我刚刚所说的,就可以知道后撤步的输入应该怎么写了。

第一行一个数 。
第二行是棋子的初始坐标 ( x 0 , y 0 ) (x_0,y_0) (x0,y0)
第三行三个数,分别是圆心坐标 p , q p,q p,q和半径 R R R
接下来 n n n行,每行都是一个向量 ( x i , y i ) (x_i,y_i) (xi,yi)

样例输入
5
(2,3)
0 0 8
(6,2)
(-2,3)
(-3,-7)
(4,-2)
(-9,6)
标程
#include <stdio.h>
int n;
long long p, q, r, x, y;
int main()
{
    scanf("%d\n(%lld,%lld)\n%lld%lld%lld\n", &n, &x, &y, &p, &q, &r);
    for (int i = 1; i <= n; i++)
    {
        long long u, v;
        scanf("(%lld,%lld)\n", &u, &v);
        x -= u;
        y -= v;
    }
    if ((p - x) * (p - x) + (q - y) * (q - y) <= r * r)
        printf("No way!\n");
    else
        printf("(%lld,%lld)\n", x, y);
    return 0;
}

让我们从头开始一一分析。

scanf("%d\n(%lld,%lld)\n%lld%lld%lld\n",&n,&x,&y,&p,&q,&r);

第一行我们向缓冲区输入5 \n。5被粗体的%d吃掉,\n被粗体的\n吃掉,缓冲区里什么都没有。

scanf("%d\n (%lld,%lld)\n%lld%lld%lld\n",&n,&x,&y,&p,&q,&r);

第二行我们输入( 2 , 3 ) \n,看到这里便知道了第一行的格式串要用%d\n的理由:

因为如果没有\n,缓冲区里就会剩下我们输入5以后带的\n,而这个剩在缓冲区开头的\n没法和粗体的(配对,导致整句scanf罢工。同理适用于for循环里面的scanf("(%lld,%lld)\n", &u, &v);

但是这份代码有一个问题:
明明以前已经订好了n=5,我们明知道for循环中输入5个坐标就应该结束了,为什么输入最后的第五个坐标以后,按下回车没有反应,并且按很多次回车也没反应呢?

对了,这就是刚刚所讲的格式串中的空白符与任意数量的任意空白符匹配。纵使多少个\n都会被scanf("(%lld,%lld)\n", &u, &v);最后的那个\n吃掉。
但是最后的那个\n又不好删掉,否则会导致下一次输入失败(上一次输入完以后缓冲区里剩的\n和下一次输入的格式串开头的(无法对应)。

有没有什么折中的办法呢。

#include <stdio.h>
int n;
long long p, q, r, x, y;
int main()
{
    scanf("%d\n(%lld,%lld)\n%lld%lld%lld\n", &n, &x, &y, &p, &q, &r);
    for (int i = 1; i <= n; i++)
    {
        long long u, v;
        if(i<n)
		{
        	scanf("(%lld,%lld)\n", &u, &v);
		}
        else
		{
        	scanf("(%lld,%lld)", &u, &v);
		}
        x -= u;
        y -= v;
    }
    if ((p - x) * (p - x) + (q - y) * (q - y) <= r * r)
        printf("No way!\n");
    else
        printf("(%lld,%lld)\n", x, y);
    return 0;
}

哦!原来是这样的道理!

P.S.为了让更多人听懂,文章里保留了一些细节,最重要的细节是,除了键盘缓冲区,还存在一个stdin缓冲区,键盘输入的字符都实时放在了键盘缓冲区里,按下回车或者键盘缓冲区满了,里面的内容全移到stdin缓冲区里,scanf时刻读取stdin缓冲区里面的字符,如果stdin缓冲区为空,就等待键盘缓冲区提供新的字符给stdin缓冲区,如果stdin里面有数据,就不等待用户输入,直接用现成的。本文简化成了只有一个键盘缓冲区。

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值