哀小茶吧 关注:12贴子:988
  • 13回复贴,共1

【哀小茶】关于C语言scanf的讲解

只看楼主收藏回复

一楼留空


来自Android客户端1楼2019-02-14 12:52回复
    scanf 字面意思 scan+f(ormat) 按格式扫描
    用户的输入在某种奇怪的设计哲学里是当作文件处理的,或者对应 C++ 里的流——就是一个可以单方向读入的(字符)串,读入以后再也没法回头读(有例外)。
    在入门教材、学校/培训机构课程中的练习、示例当中涉及到交互的时候,经常会说 scanf 是用来“输入一个整数,输入一个字符串”的东西,仿佛能直接输入对应类型的对象,仿佛两个对象的输入之间是完全分开的。
    其实实际的机制不那么直观,很多入门教材、课程以入门介绍为目的不会好好讲解 scanf 的原理和用法(也没有那个必要)。但是很多入门学习的人就是会在这些地方踩到陷阱,或者就是喜欢在这种地方钻牛角尖。


    来自Android客户端2楼2019-02-14 12:53
    回复
      2025-06-02 15:36:37
      广告
      scanf 字面意思 scan+f(ormat) 按格式扫描
      用户的输入在某种奇怪的设计哲学里是当作文件处理的,或者对应 C++ 里的流——就是一个可以单方向读入的(字符)串,读入以后再也没法回头读(有例外)。
      在入门教材、学校/培训机构课程中的练习、示例当中涉及到交互的时候,经常会说 scanf 是用来“输入一个整数,输入一个字符串”的东西,仿佛能直接输入对应类型的对象,仿佛两个对象的输入之间是完全分开的。
      其实实际的机制不那么直观,很多入门教材、课程以入门介绍为目的不会好好讲解 scanf 的原理和用法(也没有那个必要)。但是很多入门学习的人就是会在这些地方踩到陷阱,或者就是喜欢在这种地方钻牛角尖。


      来自Android客户端3楼2019-02-14 12:53
      回复
        scanf 字面意思 scan+f(ormat) 按格式扫描
        用户的输入在某种奇怪的设计哲学里是当作文件处理的,或者对应 C++ 里的流——就是一个可以单方向读入的(字符)串,读入以后再也没法回头读(有例外)。
        在入门教材、学校/培训机构课程中的练习、示例当中涉及到交互的时候,经常会说 scanf 是用来“输入一个整数,输入一个字符串”的东西,仿佛能直接输入对应类型的对象,仿佛两个对象的输入之间是完全分开的。
        其实实际的机制不那么直观,很多入门教材、课程以入门介绍为目的不会好好讲解 scanf 的原理和用法(也没有那个必要)。但是很多入门学习的人就是会在这些地方踩到陷阱,或者就是喜欢在这种地方钻牛角尖。


        来自Android客户端4楼2019-02-14 12:53
        回复
          scanf的第一个参数是格式参数,入门教材里会说 "%d" 就会读入一个整数,然后赋值给对应位置参数指针指向的对象。scanf("%d",ptr); 效果就行是 *ptr=getInt(); (如果真是这样该多好,省得解释这么多了)
          事实上,scanf 把你的输入当成一个字符串处理。你输入的时候大致会这样操作——输入几位数字,然后按下回车确认,这时候命令行换了一行,程序从 scanf 的地方继续向下执行。
          假设你想输入的数是42,你的输入实际上是"42\n"。scanf从第一个参数(格式字符串参数)里读到了%d,就知道应该从你的输入字符串里最开头读取一个整数(然后以int形式保存)。具体过程是这样的——读入'4',是一个数字,继续读下一个字符'2',还是数字,继续读'\n',不是数字,于是数的输入就结束了,'\n'被留在输入的字符串里,'4'和'2'已经被读过,后面再也不可能回来读了。然后读出来的数字'4'和'2'经过计算表示十进制数42,写入对应位置参数指针指向的对象。


          来自Android客户端5楼2019-02-14 12:54
          回复
            所有 scanf 调用都操作的是同一个输入文件(字符串),前一个 scanf 舍弃掉的字符,后一个就读不到了,每一个 scanf 都从上一次 scanf 读到的进度继续阅读输入的字符串。
            这个例子结束为止一切和入门教程上说的没有什么区别。
            但事实上有一个矛盾——输入应该是一个字符串,你有接连的几个scanf,中间还夹着计算、输出:
            例1.
            int a,b;
            scanf("%d",&a);\\类似*(&a)=getInt();
            printf("a=%d\n",a);
            scanf("%d",&b);\\类似*(&b)=getInt();
            printf("b=%d\n",b);
            命令行当中看起来像这样
            12
            a=12
            450
            b=12450
            用户的输入即使在按了回车以后还是没有结束的(实际上只有输入特殊字符才能结束用户输入)
            这个例子用教科书里的说法(模型)可以解释的通,但是用这里说的“scanf 阅读字符串”就说不通——输入都还没有完成(没有整个字符串都输入完),输出就先蹦出来了


            来自Android客户端6楼2019-02-14 12:55
            回复
              如果按照先前的解释,输入输出应该像这个样子:
              12
              450
              ^z
              a=12
              b=450
              其中^z就是我说的结束输入的特殊字符,输入方式是ctrl+z;
              回到文件的说法,用户的输入是一个单向读取的文件,与之相对,输入过程中用户就相当于在写入这个文件。如果用户一次性写完再让程序读取,就不可能进行“交互”,在输入以后立马看到结果,再根据看到的结果决定下一步输入什么。


              来自Android客户端7楼2019-02-14 12:56
              回复
                接下来的部分请认真阅读:
                “用户输入”这个文件,其实可以间断进行读取和写入,先写一点,再读一点,再写一点,再读一点。
                就好像你在写一本小说,你朋友很好奇在旁边盯着你看。你一边写着,他就一边读。类比过来,一般情况下(单线程的模式),命令行用户输入的时候程序是不能继续运行的,也不能读取输入——就好像因为地方太窄,你朋友想要看你写好的部分的时候,你就不得不停笔,把稿子给他看完以后再继续写。
                而程序进行计算,产生输出的过程,就像你的朋友对你写好的部分提意见。命令行输入的模式当中,每当你调用一次 scanf ,程序会先读取你写的还没有读取的部分,全部读取完这个 scanf 还没结束的话,它再让你输入。就相像你的朋友想要读小说,你如果之前有写好他还没看的部分,他就先接着看,不急着要你写;如果他看完了还不足以发表一次意见,或者他想读的时候你根本都还没写,他就会喊你过来接着写,直到你写完之前都等着你。
                而你却不知道你的朋友需要读多少才够发表一次意见,就只有写一段请他过来读一下。在命令行输入当中对应的,就是你输入的时候换了一次行。之前因为输入不够在等待你的 scanf ,听到你输入了 '\n' 以后就暂时过来接手稿子,继续阅读看看能不能读完这一次 scanf 的目的。然后 scanf 一直重复上述的过程——不够就让你接着写,写到 '\n' 就看看有没有写够,写够了再结束这个 scanf 继续执行下面的代码,好好就着你写的小说计算、思考,把计算的结果 printf 出来,直到又遇到程序里的下一个 scanf。


                来自Android客户端8楼2019-02-14 12:56
                回复
                  2025-06-02 15:30:37
                  广告
                  而你就可以根据程序对你写的小说的反馈,决定接下来写什么内容。
                  例2.
                  //学生管理系统的片段,程序只用来说明,请不要模仿这种写法
                  printf("please input the operation:");
                  scanf("%s",opBuff);//读取要进行的操作
                  if(!strcmp(opBuff,"add")){//添加学生
                  printf("please input id, name, age:");
                  scanf("%d %s %d",&id,name,&age);//读取要进行的操作
                  student_record sr;
                  make_student_record(&sr,id,name,age);
                  if(insert(&sr)) printf("insert succeeded");
                  else printf("insert failed");
                  }else if(...){//其他操作
                  }
                  ...


                  来自Android客户端9楼2019-02-14 12:57
                  回复
                    要时刻记着,scanf 只能从你输入的字符串里推测你输入的哪一段对应他的哪一个格式说明符,如果有两种以上的对应方法,就会按前面所说的“最长匹配”的原则来匹配。abcde如果可以切分成abc/de ab/cde两种切分方法都符合,scanf 一定会选择abc/de。


                    来自Android客户端14楼2019-02-14 14:18
                    回复
                      有了以上的知识以后再讲讲怎么处理输入错误——
                      scanf 是有返回值的。在输入没有正确进行的时候,scanf 会返回比格式说明符数量少的整数,也即成功输入的格式说明符的数量,比如scanf("%d%d%d"),如果在读到第三个%d时出错了,scanf 会中止,并返回2。并且,错误输入的部分就留着不会继续读下去。
                      下一次 scanf ,还会从上一个 scanf 放弃的地方开始。
                      这次你扮演读者,如果是读取真正一次性写好一次性读取的文件,出了错误是不可能纠正的,但是交互程序就可以——你可以要求用户重新输入之前输入错误的部分。
                      问题是,你不知道从哪里开始是用户重新输入的部分。怎么跳过错误的部分从重新输入的部分开始读起呢?
                      有些书会教你 fflush(STDIN); 然而这是 UB(请自行搜索)。你要做的是,一直读取,直到发现上一次失败 scanf 应该结束的位置。
                      其实可以简单做个推理,scanf 能继续执行,一定是输入了回车。在用户看到“输入错误”的提示之前他一定刚刚输入了回车,在继续输入之前一定来得及看到“输入错误”的提示。


                      来自Android客户端15楼2019-02-14 14:18
                      回复
                        对于 scanf 来说,他是没有时间概念的
                        错例2.
                        while(scanf("%d",&a)<1){
                        scanf(magic);//假设magic可以匹配任意字符而且丢弃(实际上并不存在)
                        printf("input error, please input an integer:");
                        }
                        对于 scanf 来说,他是没有时间概念的,丢弃错误输入的scanf(magic);不知道到底该什么时候停,他不知道自己被调用之前用户实际上输入了多少——他丢完了已有的输入以后,还会继续要求输入,然后把你的输入再丢掉,读完又问你要,周而复始。


                        来自Android客户端16楼2019-02-14 14:19
                        回复
                          正确的实现是
                          例5.
                          while(scanf("%d",&a)<1){
                          scanf("%*[^\n]");
                          printf("input error, please input an integer:");
                          }
                          你可以当成一个模板,写的时候类似这样:
                          while(scanf(...)<n){
                          scanf("%*[^\n]");
                          printf("input error, please input (what you specified up there):");
                          }
                          scanf("%*[^\n]"); 的意思是读取任意不是换行的字符,并且丢弃不保存,有兴趣的话可以看 cppref 的解释。


                          来自Android客户端17楼2019-02-14 14:20
                          回复
                            补充两个错误案例:
                            scanf("%d\n",&a);
                            scanf("%d,%d");//当输入是 5 1 的时候
                            第一个 scanf 输入完数字以后无论按下多少次回车键都不行——只要读者看懂了前面的说明就知道,他们都和 '\n' 匹配。只有再输入空白符以外的东西,并且回车才能唤醒 scanf。
                            第二个 scanf 也许本身并不算错,只是你必须按 [ \n\t][0-9]+,[ \n\t][0-9]+ (任意数目空白符,一个以上的数字,逗号,任意数目空白符,一个以上的数字)的形式输入。强行用空格分段就会导致 ',' 匹配失败,读取不到第二个数。


                            来自Android客户端18楼2019-02-14 14:20
                            回复