接下来的部分请认真阅读:
“用户输入”这个文件,其实可以间断进行读取和写入,先写一点,再读一点,再写一点,再读一点。
就好像你在写一本小说,你朋友很好奇在旁边盯着你看。你一边写着,他就一边读。类比过来,一般情况下(单线程的模式),命令行用户输入的时候程序是不能继续运行的,也不能读取输入——就好像因为地方太窄,你朋友想要看你写好的部分的时候,你就不得不停笔,把稿子给他看完以后再继续写。
而程序进行计算,产生输出的过程,就像你的朋友对你写好的部分提意见。命令行输入的模式当中,每当你调用一次 scanf ,程序会先读取你写的还没有读取的部分,全部读取完这个 scanf 还没结束的话,它再让你输入。就相像你的朋友想要读小说,你如果之前有写好他还没看的部分,他就先接着看,不急着要你写;如果他看完了还不足以发表一次意见,或者他想读的时候你根本都还没写,他就会喊你过来接着写,直到你写完之前都等着你。
而你却不知道你的朋友需要读多少才够发表一次意见,就只有写一段请他过来读一下。在命令行输入当中对应的,就是你输入的时候换了一次行。之前因为输入不够在等待你的 scanf ,听到你输入了 '\n' 以后就暂时过来接手稿子,继续阅读看看能不能读完这一次 scanf 的目的。然后 scanf 一直重复上述的过程——不够就让你接着写,写到 '\n' 就看看有没有写够,写够了再结束这个 scanf 继续执行下面的代码,好好就着你写的小说计算、思考,把计算的结果 printf 出来,直到又遇到程序里的下一个 scanf。
scanf 字面意思 scan+f(ormat) 按格式扫描
用户的输入在某种奇怪的设计哲学里是当作文件处理的,或者对应 C++ 里的流——就是一个可以单方向读入的(字符)串,读入以后再也没法回头读(有例外)。
在入门教材、学校/培训机构课程中的练习、示例当中涉及到交互的时候,经常会说 scanf 是用来“输入一个整数,输入一个字符串”的东西,仿佛能直接输入对应类型的对象,仿佛两个对象的输入之间是完全分开的。
其实实际的机制不那么直观,很多入门教材、课程以入门介绍为目的不会好好讲解 scanf 的原理和用法(也没有那个必要)。但是很多入门学习的人就是会在这些地方踩到陷阱,或者就是喜欢在这种地方钻牛角尖。
顺带也普及一个观念,很多时候教材要介绍的是一个非线性的,前后相关,非常复杂的系统,只是为了给你介绍入门概念就要涉及到后面才讲的内容,这时候就请按照教材作者的思路,他告诉你不用弄懂,你就别着急弄懂,后面会告诉你的。
scanf的第一个参数是格式参数,入门教材里会说 "%d" 就会读入一个整数,然后赋值给对应位置参数指针指向的对象。scanf("%d",ptr); 效果就行是 *ptr=getInt(); (如果真是这样该多好,省得解释这么多了)
事实上,scanf 把你的输入当成一个字符串处理。你输入的时候大致会这样操作——输入几位数字,然后按下回车确认,这时候命令行换了一行,程序从 scanf 的地方继续向下执行。
假设你想输入的数是42,你的输入实际上是"42\n"。scanf从第一个参数(格式字符串参数)里读到了%d,就知道应该从你的输入字符串里最开头读取一个整数(然后以int形式保存)。具体过程是这样的——读入'4',是一个数字,继续读下一个字符'2',还是数字,继续读'\n',不是数字,于是数的输入就结束了,'\n'被留在输入的字符串里,'4'和'2'已经被读过,后面再也不可能回来读了。然后读出来的数字'4'和'2'经过计算表示十进制数42,写入对应位置参数指针指向的对象。
所有 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 阅读字符串”就说不通——输入都还没有完成(没有整个字符串都输入完),输出就先蹦出来了
如果按照先前的解释,输入输出应该像这个样子:
12
450
^z
a=12
b=450
其中^z就是我说的结束输入的特殊字符,输入方式是ctrl+z;
回到文件的说法,用户的输入是一个单向读取的文件,与之相对,输入过程中用户就相当于在写入这个文件。如果用户一次性写完再让程序读取,就不可能进行“交互”,在输入以后立马看到结果,再根据看到的结果决定下一步输入什么。
而你就可以根据程序对你写的小说的反馈,决定接下来写什么内容。例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(...){//其他操作
}
...
程序告诉你他想知道你进行什么操作,在命令行输出
please input the operation:
你输入了
add
然后回车('\n')告知程序,你写完了,请他阅读,于是他知道你要加入一个学生,他就告诉你,接下来的段落请告诉他学生的学号、姓名、年龄——
please input id, name, age:
你看到了这个说明就知道该写什么了,于是输入:
12
然后立马就回车,等不及先想知道程序对你写的小说有什么看法;但是程序读下了“12\n”,对照自己的想要看到的东西("%d %s %d"),现在进行到了" %s %d",觉得还没读完(一个scanf还没结束),于是还是等你写,你写道——
bakasunchy 19
然后忘了回车,虽然你输入完了,但是程序不知道,还在傻等。知道你反应过来按下了回车,程序才读完这些,知道你小说里有一个19岁叫bakasunchy的人物并且记住了他(insert(&sr)),并且告诉你他能接受得了这个人物——
insert succeeded
拓展阅读:如果你输入 add 12 bakasunchy 19, 然后回车,程序就会一股脑儿输出“please input id, name, age:insert succeeded”(连换行都没有,原因是正常情况下的换行其实是你输入的),其实就相当于你在叫你朋友来看之前一次性写了两个段落,他反应慢了半拍
接下来讲解格式字符串的格式
scanf 读你写的小说的时候,自己会先写下期待看到的东西,然后从你的小说里看到一件就勾掉一件,而且按顺序勾。
格式字符串的构成主要有 字符 空白 格式说明符 三种(写的时候没有参照标准,说法未必正确)
在格式字符串里如果出现了非空白字符,就说明必须原模原样地输入一样的字符,比如 scanf("long_long_ago"); 你就必须输入 "long_long_ago" ,就好像你的读者很挑剔,必须用“很久很久以前”来开头。scanf 会先发现第一个字符是l,对应格式字符串里的字符,对,于是接着读下一个,直到刚好 scanf 想看到的东西都看到了。
而出现空白的时候对应的是任意数目的空白,比如
scanf(" ")就可以读入" " 或者 ”\t\n\n“(\t是制表符,也是空白的),甚至什么都没有,就好像读惯了盗版网络小说的读者无论怎么排版都能读得下去。
现在有必要插入说明,scanf按照格式字符串读取的时候,是以最长匹配为原则的——
scanf("super soda sea"); 你输入"super \n soda sea"的时候,scanf 对比到 super 后面的那个空格,不会因为" "可以对应格式里的空格,就跳过了想要看's',然后在看到'\n'的时候抱怨——他会一直读到不是空白字符为止,也就是's',然后发现对得上。
格式说明符以'%'开始,紧接着的说明要输入的、可以对应的字符,和解释方式
只讲几个简单的用于举例:
d 会一直读取十进制数字(0-9),并且把读到的数字当成一个数来解释,保存到对应位置的参数(指针指向的对象)。
s 会读取除了空格符以外的任意字符(除非遇到文件结尾),把读到的结果保存到对应参数指针对应的字符数组里(之所以说是对应,原因是并不是指向,这里不细说)
c 会读取除了空格符以外的任意字符,但只读一个
还有些有趣的部分就不讲了,只告诉读者有这样的东西存在(请查阅 cppref):在%后加入数字可以读入固定长度,在%后数字前加上*可以不保存这个格式说明符得到的输入到对应参数(参数顺序顺延),%后加中括号括起字符串可以匹配特定字符组成的字符串
在大部分格式说明符之前,都会相当于加了一个空白字符
例3.
scanf("%d",&a);
deal_with(a);
scanf("%d",&b);
输入:
12
24
在第一个 %d 处 12 被匹配掉了,直到遇到了'\n',scanf 知道第一个 %d 结束了,于是第一个 scanf 调用返回,程序继续执行。处理完 a 的事以后继续 scanf("%d",&b); 。此时输入中还剩下"\n24\n",如果直接按 %d 本身的规则(不包括空白字符处理),第一个遇到的字符就不是十进制数字,输入就会出错。但是 %d 相当于之前有一个空白符。%d 会先处理掉所有空白符号,然后继续。这也是这种代码可以正常运行的原因。
而
例4.
scanf("%d",&a);
deal_with(a);
scanf("%s",str);
输入:
9
baka
你会发现str其实是"\nbaka",baka 前多了个换行,%s 认为空格很有可能也是要输入的一部分,因此不会先省略掉\n。
%c也按相同的逻辑设计,这也是
错例1.
scanf("%s",&command);
//do something and output something
scanf("%c",&operation);
输入“cal\n+”不能正确按照使用者想法进行的原因——要能经过上一次输入还残留了一个\n,然后就赋值给operation,后面真正输入的+就这样错过了(甚至造成后面一系列的输入都出错)。
要时刻记着,scanf 只能从你输入的字符串里推测你输入的哪一段对应他的哪一个格式说明符,如果有两种以上的对应方法,就会按前面所说的“最长匹配”的原则来匹配。abcde如果可以切分成abc/de ab/cde两种切分方法都符合,scanf 一定会选择abc/de。有了以上的知识以后再讲讲怎么处理输入错误——
scanf 是有返回值的。在输入没有正确进行的时候,scanf 会返回比格式说明符数量少的整数,也即成功输入的格式说明符的数量,比如scanf("%d%d%d"),如果在读到第三个%d时出错了,scanf 会中止,并返回2。并且,错误输入的部分就留着不会继续读下去。
下一次 scanf ,还会从上一个 scanf 放弃的地方开始。
这次你扮演读者,如果是读取真正一次性写好一次性读取的文件,出了错误是不可能纠正的,但是交互程序就可以——你可以要求用户重新输入之前输入错误的部分。
问题是,你不知道从哪里开始是用户重新输入的部分。怎么跳过错误的部分从重新输入的部分开始读起呢?
有些书会教你 fflush(STDIN); 然而这是 UB(请自行搜索)。你要做的是,一直读取,直到发现上一次失败 scanf 应该结束的位置。
其实可以简单做个推理,scanf 能继续执行,一定是输入了回车。在用户看到“输入错误”的提示之前他一定刚刚输入了回车,在继续输入之前一定来得及看到“输入错误”的提示。
对于 scanf 来说,他是没有时间概念的
错例2.
while(scanf("%d",&a)<1){
scanf(magic);//假设magic可以匹配任意字符而且丢弃(实际上并不存在)
printf("input error, please input an integer:");
}
对于 scanf 来说,他是没有时间概念的,丢弃错误输入的scanf(magic);不知道到底该什么时候停,他不知道自己被调用之前用户实际上输入了多少——他丢完了已有的输入以后,还会继续要求输入,然后把你的输入再丢掉,读完又问你要,周而复始。
正确的实现是
例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 的解释。
补充两个错误案例:
scanf("%d\n",&a);
scanf("%d,%d");//当输入是 5 1 的时候
第一个 scanf 输入完数字以后无论按下多少次回车键都不行——只要读者看懂了前面的说明就知道,他们都和 '\n' 匹配。只有再输入空白符以外的东西,并且回车才能唤醒 scanf。
第二个 scanf 也许本身并不算错,只是你必须按 [ \n\t][0-9]+,[ \n\t][0-9]+ (任意数目空白符,一个以上的数字,逗号,任意数目空白符,一个以上的数字)的形式输入。强行用空格分段就会导致 ',' 匹配失败,读取不到第二个数。
喜欢的话关注收藏评论转发一波 比心么么哒!加入我们C语言C++学习交流 壹 496926338群内有大量的项目开发和新手教学视频千人大群等着你来加入。
C语言C++学习交流群496926338