牛客网OJ循环输入输出:scanf与printf用法详解与避坑指南
作为一个在各类在线评测平台上刷过几千道题的老选手我太清楚循环输入输出这道坎了。很多人第一次在牛客网做C语言练习题本地编译通过、运行结果也对一提交就是“答案错误”或者“时间超限”有时候连“运行错误”都蹦出来。排查半天发现根本不是算法问题而是输入输出没处理好。牛客网的循环输入输出测试本质就是考察你对scanf和printf这两个最基础函数的掌握深度——不是会写printf(hello)那种会而是对格式化控制、返回值处理、缓冲区残留、多组数据循环读取这些细节了如指掌。这篇文章我就以牛客网典型的循环输入输出题目为例把scanf和printf从用法到原理到实战坑点完整拆一遍。不管你是刚学C语言的大一新生还是准备秋招笔试正在刷OJ的应届生甚至是想把基础打牢的爱好者这篇都能帮你在面对“多组输入”“直到文件结尾”“按格式输出”这类描述时不再犯怵。1. 牛客网循环输入输出到底在考什么1.1 为什么OJ题总有多组输入这个坑很多初学者习惯的编程方式是“一段代码处理一份数据”比如声明一个变量、读入它、输出结果程序就结束了。但在线评测平台的题目为了批量验证程序正确性通常会准备多个测试点每个测试点里可能包含多组测试数据。你的程序必须有能力把一组数据读进来处理完输出结果然后继续读下一组直到所有数据处理完毕。这里有个很容易忽略的点评测系统不会像人一样“看”你的程序输出它是把程序输出和标准输出做逐字节比对。所以多组输入意味着多组输出每组输出之间用什么分隔、换行符放在哪都得跟标准输出完全一致。多数题目采用“每组输出占一行”的约定也就是每组结果后面跟一个\n。如果少输出一个换行或者多输出一个空格都会被判定为错误。理解了这一点你再看牛客网上scanf和printf的循环测试题就会发现它们考的其实是三个能力能否正确解析输入格式、能否正确产生输出格式、能否让循环在正确时机终止。三个能力缺一个程序就跑不对。1.2 读懂题目描述里的输入格式要求牛客网的题目描述一般分几行题目大意、输入描述、输出描述、样例输入、样例输出。大多数人急着看样例输入就开写这个习惯在简单的AB题上没问题但做到循环输入输出的综合题就会翻车。输入描述里常见的几类说法“输入包含多组测试数据每组占一行包含两个整数a和b。”——说明你不知道有多少组要找EOF结束。“输入第一行包含一个整数T代表测试数据的组数。”——说明先用计数器模式。“输入包含若干行每行两个整数以0 0结束。”——说明要用哨兵值做终止条件。“输入一个字符串以#号结束。”——说明处理的是字符型数据留意getchar吃掉换行。输出描述则是反向的约定“对于每组测试数据输出一行结果。”这句话直接决定了你的换行符位置。我的建议是做题前先在草稿纸上把输入输出格式翻译成伪代码比直接上手写代码更稳。比如“多组输入每组两个数”心理自动映射成while (scanf(%d %d, a, b) 2)“先给组数T再来T行数据”映射成scanf(%d, T)加for循环。这种“格式描述到代码模式”的反射能力就是刷牛客网循环输入输出题的核心收获。2. scanf用法拆解不只是读数据2.1 格式字符串与控制符很多人把scanf当成“读键盘输入的函数”这个理解太模糊了。更准确地说scanf是“按照格式字符串对输入流进行解析的函数”。它的核心是格式字符串里的%控制符每个控制符对应一种数据类型的读取方式。最常用的几类控制符控制符含义对应变量%d读取十进制整数int*%lld读取长长整数long long*%f读取单精度浮点float*%lf读取双精度浮点double*%c读取单个字符char*%s读取字符串遇空白停止char*数组名%[^\n]读取整行直到换行char*数组名注意一个高频误区scanf读int时符号位、前导空白空格、换行、制表符都会被自动跳过但%c不会跳空白。也就是说如果你连续用scanf(%c)读字符读到的很可能是上一行末尾的换行符。这就是后面要专门讲缓冲区残留问题的原因。另外格式字符串里的普通字符必须原样匹配。比如scanf((%d,%d), a, b)要求输入必须是(1,2)这种带括号和逗号的格式只要有一个字符对不上解析就失败。在牛客网题目里输入格式一般都很规整很少要求这么严格匹配但你要具备“格式字符串不是摆设”的意识。2.2 scanf的返回值到底有什么用这是循环输入输出的分水岭知识点。scanf返回的是“成功读取并赋值的变量个数”也就是匹配赋值的次数。比如scanf(%d %d, a, b)成功读入两个整数返回2。成功读入一个另一个失败返回1。一个都没读入返回0。读到文件结尾或流错误返回EOF也就是-1。正因为有返回值我们才能写出经典的循环模式while (scanf(%d %d, a, b) 2)。这个写法表达的意思是只要我能成功读到两个整数就继续处理一旦读不到数据读完或者格式不对循环结束。很多人喜欢写while (!feof(stdin))这其实是个坑。feof()在scanf真正读到文件结尾之后才会置位容易导致循环体多执行一次。我见过无数人在这上面栽跟头最后结果是本地跑通、提交出错。所以如果是判读是否读完直接用返回值判断最可靠。再延伸一点“读入失败”未必等于“文件结尾”。比如题目要求数字却混入字符scanf就返回0甚至EOF具体取决于输入流状态。不过牛客网的评测数据是整理好的合法数据你更需要注意的是“正常读完”这个情况。2.3 缓冲区残留问题getchar、空格、换行的恩怨scanf读取时键盘输入首先进入一个叫“缓冲区”的内存区域scanf再从这个缓冲区里取数据。这意味着一次输入可能会残留若干个空白字符影响后续的读取。典型场景是这样的先读一个整数再读一个字符或字符串直接写scanf(%d, n); scanf(%c, c);第一行用户输入5然后按回车缓冲区里其实存了5\n。第一个scanf读走5留下了\n。第二个scanf(%c)读到的不是用户真正的字符而是这个残留的换行符。解决办法有两种在%c或%[^\n]前面加一个空格scanf( %c, c)。格式字符串里的空格表示“跳过任意空白字符”。在中间用getchar()把换行符吞掉。类似的问题在读取完整一行时也很常见。用scanf(%[^\n], str)能读取到换行前的整行内容但它不会消费掉结尾的换行符。如果你后面紧接着还要读行内容就会出问题。稳妥的方案是用getchar配合判断或者直接改用fgets读行——不过这正是牛客网题目喜欢设计的小关卡。我还遇到过一种情况连续多行字符串处理每一行结尾都有换行循环里getchar放错位置导致每读一行就吃掉一个有效字符。排查的时候很费劲因为单独跑一组数据是对的多组一循环就错位。后来我总结出“先想清楚谁负责消费换行”这个原则就好办多了。3. printf用法拆解输出格式全解析3.1 格式控制符与修饰符输出方面printf的格式控制符看起来好懂但细节非常多。先记住基本盘控制符输出类型%dint%lldlong long%ffloatC语言中默认转double%lfdouble%cchar%s字符串%x十六进制整数%.2f保留两位小数修饰符是很多人忽略的部分。在%和类型字母之间的数字、负号、点号分别控制宽度、对齐、精度。比如%5d输出宽度至少5位不够用空格在左边补右对齐。%-5d左对齐前面是数字本身后面补空格。%05d不足5位用0在前面补。%.2f保留两位小数执行四舍五入。%8.3f总宽度8位其中小数占3位。%s配合%.5s可以截断字符串只输出前5个字符。牛客网的很多输出题不会要求你补空格对齐但会要求“保留两位小数”“输出整数不带小数点”。比如printf(%.2f, 3.0)输出3.00有些人用%f直接输出3.000000白白丢分。3.2 常见输出格式要求应对我刷题时总结过几种常见的输出描述及对应写法“结果保留两位小数”printf(%.2f\n, ans);“每个整数占一行”printf(%d\n, a);“每个整数之间用一个空格隔开行末无多余空格”这个实现起来最麻烦需要在循环里判断是不是最后一个数。“输出Yes或No”注意大小写和拼写必须与样例完全一致。“输出没有多余的空格和空行”很多人会犯多输出空行的错误尤其是从“每组输出一行”误理解成“每组后面多打一个空行”。“行末无多余空格”这个需求值得单独讨论。最笨的办法是把结果存到数组里先输出前n-1个并带空格再单独输出最后一个不带空格。也可以用标记变量int first 1; while (scanf(%d, x) ! EOF) { if (!first) printf( ); printf(%d, x); first 0; } printf(\n);这个技巧在处理不确定个数的输入且要求空格分隔时非常常用。输出格式在评测系统里是硬指标想办法满足它比争论“多一个空格怎么了”有意义得多。4. 循环输入输出的四大经典模式4.1 未知组数EOF模式这是牛客网上最普遍的输入模式。题目描述只说“输入包含多组测试数据”不告诉你有多少组你的任务是把文件内容全部读完。标准写法#include stdio.h int main() { int a, b; while (scanf(%d %d, a, b) ! EOF) { printf(%d\n, a b); } return 0; }这里的! EOF也可以写成 2因为只要题目保证输入格式合法每组都能读满两个数。两种写法表达的意思略有不同! EOF侧重“没到文件结尾就继续” 2侧重“成功读入两个变量才处理”。在数据合法前提下两者效果一样。我通常更喜欢 2它等于给数据合法性加了一道保险万一输入里有脏数据循环会提前终止而不是把错数据也消化掉。初学阶段很多人不理解为什么不写循环终止条件。其实文件本身就是一个有限流读到最后自然就EOF了。就像看一篇文章每一段是一组数据你不断读下一段直到翻到最后一页。4.2 已知组数计数器模式如果输入开头给出一个T后面跟T行数据就用计数器模式int T, i, a, b; scanf(%d, T); for (i 0; i T; i) { scanf(%d %d, a, b); printf(%d\n, a b); }这里有个隐蔽的坑scanf(%d, T)读完整数后缓冲区里还留着一个换行。如果下面紧跟scanf(%c)或者scanf(%[^\n])会出问题。如果是继续读数值类型scanf会忽略前导空白所以问题不大。但你要是读字符串行就得考虑把这个换行处理掉。计数器模式的好处是你可以用for循环直接控制次数结构清晰。对于需要先读组数再处理多组数据的场景这是最不容易写出死循环的写法。4.3 哨兵值模式有时候题目描述是“每行两个数以0 0结束”此时0 0就是一个哨兵值它代表“结束标记”不属于真实数据。写法while (scanf(%d %d, a, b) (a ! 0 || b ! 0)) { printf(%d\n, a b); }也可以先读后判断while (1) { scanf(%d %d, a, b); if (a 0 b 0) break; printf(%d\n, a b); }第二种写法更直观理解难度低循环体内第一件事就是读数据读进来判断是否结束。牛客网有不少题采用类似的哨兵方式数据本身会有结束标志你只要判断标志即可。有些人容易把哨兵组数据也当成正常数据处理一遍结果多输出一行答案。排查时先确认结束标记是否被正确拦截。4.4 字符串与字符输入的特殊处理循环输入输出不止有整数还有字符字符串。字符串题比整数题更爱出缓冲区问题。一种常见情况是先输入一个整数T接着输入T行字符串。如果不处理T后面的换行第一行字符串会读到一个空串。处理办法是scanf(%d, T); getchar(); // 吃掉T后的换行 for (i 0; i T; i) { fgets(s, 100, stdin); // 或 scanf(%[^\n], s); ... }用scanf(%s, s)读字符串时它遇到空格、换行都会停止因此只能读取不含空格的单词。题目要求读取含空格的整行时%[^\n]或fgets更合适。之后还要注意字符串末尾的换行符是否要去掉特别是用fgets时它会连同换行符一起读入。我自己做字符串题时都会先问自己三句话这题读单词还是读整行行尾换行谁负责清掉字符串结尾有没有补\0这三个问题想清楚字符串循环题就成功了一半。5. 完整实战从读题到AC5.1 例题1多组AB这是最基础的入门题。题目描述我模拟一下输入包含多组数据每组一行两个整数a和b以文件结尾结束。对每组数据输出ab。完整解法#include stdio.h int main() { int a, b; while (scanf(%d %d, a, b) 2) { printf(%d\n, a b); } return 0; }这个程序短但把EOF循环模式体现得最清楚。提交后评测系统会把多个测试点依次喂给程序每个测试点内部有若干组数据程序读到EOF后自然结束。注意这里用 2是因为每组正好两个数读不够就说明数据异常或读完了。5.2 例题2先给组数的求和题目描述输入第一行是整数T接下来T行每行两个整数a和b输出每组ab。#include stdio.h int main() { int T, a, b, i; scanf(%d, T); for (i 0; i T; i) { scanf(%d %d, a, b); printf(%d\n, a b); } return 0; }这个解法考的就是计数器模式。代码本身没有新知识但很多人会忘记for循环里每次都重新读入a和b。这里有个细节scanf在循环内每次执行都会从输入流取两个整数所以a和b的值会被覆盖更新这是预期的行为。不要画蛇添足在循环里重复读入T行以外的数据。5.3 例题3哨兵值与格式输出题目描述输入若干行每行两个整数a和b以0 0结束对每组输出a b result这个格式。先想清楚格式是什么是a,b,result要在同一行还是每行一段完整句子。假设每行输出abresult的求值表达式格式#include stdio.h int main() { int a, b; while (scanf(%d %d, a, b) 2 !(a 0 b 0)) { printf(%d%d%d\n, a, b, a b); } return 0; }把哨兵判断放到while条件里是为了避免处理0 0这一组。题目样例常见格式里可能有空格比如“a b 3”的形式那你就得严格按照样例把空格位置都摆对。printf的普通字符会原样输出所以格式串里写空格就是真的空格写加号就是真的加号。实战中最大的收获是把输入模式判断正确代码主体往往是几行而已。循环读入、判断结束、格式化输出这套组合拳打熟练之后遇到这类题基本就是肌肉记忆。6. 常见错误与排查技巧6.1 死循环与运行超时死循环是最让人头疼的问题。程序在本地跑输入固定数据能正常退出但在评测系统里显示超时。原因往往是循环条件判断错误比如while (scanf(%d, n))只在遇到0时退出但输入数据最后一个数是0就会提前退出或者本应判断EOF却用了非零即真的条件。排查死循环最好的办法是在自己的代码里加printf调试信息看每次循环读到的值是什么。比如int ret scanf(%d, n); printf(ret%d n%d\n, ret, n);不过提交时一定要把调试输出删掉否则格式全乱。如果是在牛客网页面提交没有本地调试环境我建议先在本地编译器里构造一个测试输入文件用输入重定向跑通再复制代码去提交。6.2 输出格式与样例不一致评测系统比对的是完整输出字符串所以任何一点偏差都会导致失败。常见问题多打一个空格。行尾多空格非常隐蔽肉眼很难看出来。少打一个换行。测试点里多个测试项连续输出时少一个换行会导致两行结果拼在一起。大小写错误。要求输出“Yes”写成“YES”就是错误。中英文符号混用。题目要求英文冒号输出中文冒号在界面上看起来差不多但ASCII码完全不同。建议提交前把样例输出复制到文本编辑器显示所有空白字符对照检查每个空格和换行位置。或者干脆把程序输出存到文件用diff命令与期望文件对比这是最严格的办法。6.3 本地跑对提交却错出现这种情况先怀疑三件事第一是否有未初始化的变量第二是否有数组越界第三是否依赖了本地特有的输入环境。未初始化变量在本地可能恰好是0在评测机器上是随机值结果就不稳定。解决方案是定义变量时直接赋初值int a 0;或者在读入后再使用。数组越界最容易出现在字符串处理中。char s[10]; scanf(%s, s);输入超长字符串时越界写入本地可能没崩溃评测系统却可能因此段错误。把数组开大到题目上限加上余量是最直接的防御。牛客网题目一般会给数据范围你比最大范围再多开几个字节比如char s[1005];应对长度1000以内的字符串。依赖本地输入环境的情况比如用了getch()、system(pause)这类交互函数OJ系统里没有对应交互程序会卡住或行为异常。提交代码前检查一下有没有这类“人类用户友好”的函数。6.4 常见问题速查表现象可能原因解决思路提交显示答案错误样例能过多组输入没循环处理改用while(scanf(...)!EOF)输出结果最后多一行哨兵值没被拦截判断a0 b0后break读字符串第一行为空T后的换行没被消耗在循环前加getchar()程序运行超时但本地正常循环条件写错导致死循环检查返回值判断和终止条件输出一串0或随机数变量未初始化声明时赋初值或读入后再用段错误随机出现数组越界按题目上限扩大数组浮点输出位数不对格式串没用%.2f加精度修饰符7. 写在最后的经验打这些循环输入输出的题我个人最大的体会是算法可以慢慢想但输入输出的模板必须形成肌肉记忆。我见过太多人卡在AB这道题上不是不会加而是不知道循环怎么退、格式怎么控制。反过来只要把scanf返回值和printf格式串这两个点吃透牛客网上大部分入门级题目对你来说都只是换个壳子而已。最后分享一个实用小技巧准备一套自己的代码模板。比如遇到“多组整数输入”下意识打出while (scanf(%d %d, a, b) 2)遇到“先给组数”下意识写出scanf(%d, T); for(...)遇到“读整行字符串”下意识想fgets和getchar的位置。把这些模板练成条件反射你真正思考算法的时间会多出不少做题心态也会稳很多。刷题这件事熟练度带来的信心提升往往比灵感更可靠。