尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

PTA简单计算器题解:从scanf陷阱到switch边界处理实战

PTA简单计算器题解:从scanf陷阱到switch边界处理实战 我先说个真实经历。前阵子带一个学弟过PTA上的C语言习题他卡在“习题6-7 简单计算器”这道题上本地跑得好好的一提交就给我来个“部分正确”。他当时特别憋屈说这题不就是一个加减乘除嘛怎么还能翻车。我让他把代码发我扫了两眼就发现问题了——符号读取用的是scanf(%d%c%d)数字和运算符中间隔个空格%c读到的根本不是运算符本身而是一个空格字符switch判断自然一路落到default输出就变成了“ERROR”。这种坑不踩一次真的很难发现。这题名字叫“简单计算器”难度看着也不高但它在PTA上却是出了名的“细节题”。表面只考分支结构实际把输入读取、运算符处理、除零边界、非法运算符、输出格式全部串在一起。能一遍提交通过的人往往不是代码写得有多炫而是把边界情况和评测机制都摸透了。今天就从这道题出发把完整的拆解思路、实现代码、踩坑记录和升级玩法一起捋一遍给正在做PTA题目的朋友一点可直接参考的东西。1. 先把题意吃透习题6-7“简单计算器”在考什么1.1 题目要求与真正的考察点“简单计算器”这题的基本要求很直白读入两个整数和一个运算符运算符通常是 - * /这类四则运算符号有些题型还会要求支持取模%。程序根据运算符完成计算输出结果。如果运算符不是合法运算符或者除法时分母为0都要输出明确的错误提示。很多同学看到“简单”两个字就容易放松警惕潜意识里觉得只要把if判断写清楚就行。但我要提醒一句PTA这类自动评测系统在读代码时是不看人情的。它只看你的程序输出和标准输出是否逐字节一致。多打一个空格少一个换行整型除法该截断却没有截断都可能造成提交失败。具体到这道题我认为真正的考察点有三个会不会用scanf读取混合类型数据尤其注意%c前面要不要加空格会不会用switch-case或if-else处理多分支逻辑并在合适的位置做条件判断有没有边界意识能在写代码前就想到除零、非法输入、格式输出这些场景这三件事任何一件没做到位题目就不可能拿满分。而这三件事恰恰是后续很多PTA题目都会反复用到的底层能力。1.2 边界条件决定了这道题的区分度如果只是“读两个数做一次运算”那这道题就是送分题不值得单开一篇博客讨论。它之所以能被收录进习题库就是因为边界条件能让不同水平的代码高下立判。我把常见边界情况列出来大家可以对照自己的思路除数是0执行/或%运算时应该输出错误信息而不是让程序崩溃运算符不在合法集合中比如输入了或者^应该统一落入错误分支除法和取模结果在C语言中遇到负数时的表现比如-7 % 2的结果和数学期望是否一致虽然这题一般不会这么考但了解总比踩坑后了解要好如果题目要求输出整数那么7 / 2应该输出3而不是3.5这是C语言整型除法截断的特性我在帮学生和同事看这类代码时发现大家最容易丢分的不是主流程而是这些看似微不足道的特例。很多代码在主流程上逻辑完整但要么漏了default要么除零判断位置放得不对要么输出格式和题目要求不一致。写代码之前先想清楚边界情况比你多写十行功能代码更有价值。2. 分支结构选型为什么我推荐用switch而不是if-else2.1 switch在可读性和错误兜底上更占优势实现计算器的分支逻辑C语言里最常见的两种写法是switch-case和if-else链。我的建议是优先使用switch理由和“性能”关系不大主要是可读性和结构上的天然优势。运算符本身就是离散值、-、*、/、%对应五种不同行为。switch把“每个值对应一段计算逻辑”这件事表达得非常自然。你写完代码哪怕是三天后再回来看一眼也能知道哪个分支是干嘛的。如果写成一串else if (op )判断条件一多代码行数会膨胀阅读时还得来回扫看久了真的头晕。另外switch有一个自带的分支叫default。所有没有匹配到case的值都会落进default这正好可以用来处理非法运算符。if-else链也能通过最后的else实现同样的效果但逻辑上没有switch这么“顺理成章”。对我来说这种零成本的结构优势已经足够成为选它的理由。2.2 scanf读取运算符时的空白字符陷阱这个点我几乎每次和人讨论这题都会提到因为它实在太容易被忽略了。scanf处理%d的时候会自动跳过输入流里的空白字符比如空格、Tab、换行。但%c不会它会直接从缓冲区当前位置读取一个字符包括空格。这就产生了一个非常经典的问题当我们用scanf(%d%c%d, a, op, b)去读输入“5 3”时整型变量a读到了5随后op读到的却不是加号而是5和加号之间的那个空格字符。程序后面拿op和比较时自然就匹配不上最终输出ERROR。解决方式极其简单在%c前面加一个空格写成scanf(%d %c %d, ...)。这个空格会告诉scanf先跳过所有空白字符再读运算符。类似的坑还会在读取多个字符时出现所以这次记住了后面很多字符串交互题里都会很受益。2.3 除零检查应该放到case内部而不是放switch外面除零这个边界条件很多人的第一反应是读完数据后立刻判断b 0是0就直接输出ERROR。这个思路不能说错但不严谨因为它没有区分运算符。举个例子输入是8 0这个表达式完全合法结果应该是8。但如果程序在读入数据后、进入switch前就统一判断b 0那它会直接输出ERROR把合法输入误判成非法输入。这在评测系统里就是正确答案错误。正确的做法是把除零判断放进case /和case %内部。因为只有除法或取模操作才关心除数是否为0加法和减法完全没有这个限制。代码执行到对应case时先检查b再做运算逻辑才真正对得上。这类“判断条件放错位置”的错误我看到过太多回。它其实不是不会写某个语法而是没有把业务规则还原成代码流程。题目里说“除法时除数为0要报错”那就应该在“做除法”的时候才检查而不是在“准备做运算”的时候就检查。3. 可以直接提交的参考实现与关键细节3.1 先搭一个能跑通的主框架写代码不用一步到位先搭主框架再补细节这是我一直以来的习惯。对于这道题主框架只需要四件事声明变量、读取输入、按运算符分支、输出结果。第一步可以参考下面这个结构#include stdio.h int main(void) { int a, b; char op; if (scanf(%d %c %d, a, op, b) ! 3) { printf(ERROR\n); return 0; } switch (op) { case : printf(%d\n, a b); break; case -: printf(%d\n, a - b); break; case *: printf(%d\n, a * b); break; default: printf(ERROR\n); break; } return 0; }这个版本已经能处理加减乘三种运算也能对非法运算符输出ERROR。但它还缺除法、取模和除零检查所以只算一个中间版本。写代码时我建议像这样从小到大地搭积木先让结构成形再往里填充细节不要一开始就追求写完所有分支。3.2 补全除法和取模分支在switch中加入/和%两个case并在每个case内部判断除数是否为0这样边界逻辑才完整switch (op) { case : printf(%d\n, a b); break; case -: printf(%d\n, a - b); break; case *: printf(%d\n, a * b); break; case /: if (b 0) printf(ERROR\n); else printf(%d\n, a / b); break; case %: if (b 0) printf(ERROR\n); else printf(%d\n, a % b); break; default: printf(ERROR\n); break; }有几个输出细节需要特别指出。C语言的整型除法对负数采用截断而非向下取整例如-7 / 2的结果是-3而不是-4。如果题目没有特别说明直接按C语言默认行为输出即可。取模运算同理结果符号跟随被除数的符号。题目如果用极端数据来考知道这些特性才能不慌。还有一点这道题通常输入输出都是整数所以变量类型用int就够了。但如果你在练习版里看到题目要求支持小数运算那就要把类型改成float或double同时printf里的格式符也要换成%f。做题前先看清楚题目描述里写的是“整数”还是“实数”别想当然。3.3 scanf返回值检查的意义我在代码里特意写了if (scanf(...) ! 3)这行判断可能有人觉得多余但我不这么认为。scanf的返回值是成功匹配并赋值的输入项个数。对于scanf(%d %c %d, ...)三个数据都正常读入时返回3。如果用户在输入时漏了某个字段或者输入了非数字内容返回值就会小于3。程序如果不检查这个返回值直接拿变量去参与运算结果完全不可控。例如输入是5 后面少了第二个操作数b的值是未初始化的参与运算后输出什么全凭运气。在评测系统里这种未定义行为很容易导致答案错误或运行时错误。加上返回值检查至少可以保证程序在输入不完整时走一个明确的错误输出而不是陷入不可预测的状态。做工程的人常说“不改的代码比改过的代码更容易出问题”在这里其实要改一种说法不检查的输入比检查过的输入更容易出问题。一句话scanf一定要检查返回值这个习惯能救很多次。3.4 完整可提交版本把上面所有内容合并得出一份我认为比较稳的完整版本#include stdio.h int main(void) { int a, b; char op; if (scanf(%d %c %d, a, op, b) ! 3) { printf(ERROR\n); return 0; } switch (op) { case : printf(%d\n, a b); break; case -: printf(%d\n, a - b); break; case *: printf(%d\n, a * b); break; case /: if (b 0) printf(ERROR\n); else printf(%d\n, a / b); break; case %: if (b 0) printf(ERROR\n); else printf(%d\n, a % b); break; default: printf(ERROR\n); break; } return 0; }这份代码没有用什么高级技巧但它把这个题目里能想到的分支和边界都覆盖了。如果题目后续变化成浮点数版本或者要求连续处理多组表达式框架也能很容易地扩展。对初学者来说能稳定写出这种代码比背那些花哨的写法更重要。4. 提交途中踩过的坑排查思路与解决记录4.1 空格让运算符读错这是最高频的翻车点前面已经提过%c不跳过空格的问题这里我把当时的完整排查过程写出来。有次一位读者发给我的代码逻辑看起来完全没有问题我本人在本地跑了几个样例输出也都正常。但对方反馈说提交到PTA只有“格式错误”。我特意用了几组不同格式的输入测试才发现对方本地测试时输入的是53没有空格而PTA的样例输入是5 3数字和运算符之间带着空格。代码里写的是scanf(%d%c%d, ...)输入53时一切正常输入5 3时op就变成了空格字符。不同输入格式导致程序行为发生改变这个问题非常隐蔽。修正方法是把格式串改成%d %c %d。我也建议大家在本地准备测试数据时尽量模拟PTA题面给出的确切输入格式不要自己怎么顺手怎么来。4.2 除零判断放错位置反而拦截了合法运算有一个同样高频的错误就是把所有除零检查提前到进入switch之前统一判断b是否为0。例如if (b 0) { printf(ERROR\n); return 0; }这行代码如果在switch前执行那么输入8 * 0或者5 0时也会被拦截而它们其实是完全合法的运算。我前面分析过这个问题的根源没有把“除数为0”的约束和具体运算符绑定。这里再补充一个理解角度判断条件的粒度要对应业务规则的粒度。题目说“做除法时如果除数为0则报错”那么这个判断天然就应该放在做除法的代码分支里。看代码时条件判断的位置往往比条件本身的写法更重要。4.3 缺少default分支程序会“安静地失败”漏写default分支到底会造成什么后果如果输入3 4switch会遍历所有case找不到匹配的分支。这时如果没有defaultswitch的作用域内没有任何代码被执行程序就好像“什么都没发生”一样继续往下走。结果就是程序没有输出直接结束。PTA看到你的程序一个字符都没输出自然判断为错误。让人头疼的是这种方式失败得非常安静你不打断点或者特意加上默认输出根本发现不了问题。所以无论题目是否要求处理非法运算符我建议都写上default分支。哪怕只是防御性地输出一个ERROR也比什么都不做强。4.4 输出格式对不上主要是题目没读细有一次我在检查同学的代码时发现他使用printf(result %d\n, a b);输出结果。他本意是想让输出更友好但这种输出在评测系统里绝对不行。PTA的自动比对是逐字符的多出的result 前缀会导致“格式错误”或“答案错误”。正确的做法是严格照抄题目给出的输出样例。如果题目只说“输出运算结果”那结果后面紧跟一个换行即可。还有一类小细节是空格有的题目要求结果前有一个空格有的没有。做题的时候我会把样例输入样例输出单独复制到一个文本里写完代码后逐个对齐。4.5 本地自查用例表提交之前我在本地做了一系列测试用例。以下是我为这道题整理的常规表输入期望输出覆盖场景5 38正常加法9 - 45正常减法6 * 742正常乘法8 / 24正常除法7 / 23整数截断7 % 21取模运算5 / 0ERROR除数为零5 % 0ERROR求余时除数为零3 4ERROR非法运算符这九组用例基本覆盖了正常路径和主要异常路径。测完之后再提交虽然不能保证100%通过所有隐藏数据但至少把最常见的问题都拦在门外了。5. 从单步运算走向表达式求值一个值得去做的小升级5.1 为什么做完这道题我建议你再走一步做完“简单计算器”很多人的第一反应是“终于结束了”。但如果你想继续往深走我建议再做一个升级版一个能计算完整中缀表达式的程序比如输入3 5 * 2 - 8 / 4直接输出最终结果。这个升级版不再只有一个运算符还涉及运算符优先级。3 5 * 2不能简单地从左到右算成16而是要先算5 * 2再算3 10。用if-else或switch解决不了因为程序需要一种“延迟计算”的机制先读取后面的数据再决定前面保存的数据什么时候参与运算。这个问题背后是经典的数据结构栈。“简单计算器”里我们处理的是一个二元运算一步就能算完。表达式求值里我们要用一个操作数栈和一个运算符栈把暂时用不到的数据先存在栈里等时机成熟再取出计算。5.2 双栈法计算表达式的基本套路我拿3 5 * 2来演示双栈法的大致流程。先准备两个空栈一个存操作数一个存运算符。从左到右扫描表达式读入数字3压入操作数栈读入运算符运算符栈为空直接入运算符栈读入数字5压入操作数栈读入运算符*此时运算符栈栈顶是而*的优先级高于所以*入栈读入数字2压入操作数栈表达式扫描完毕开始从运算符栈弹出运算符。先弹出*从操作数栈弹出两个数注意先弹出的2是右操作数后弹出的5是左操作数计算5 * 2 10把10压回操作数栈再弹出从操作数栈弹出10和3计算3 10 13得到最终结果这个流程里最核心的一个细节在步骤4新运算符的优先级低于或等于栈顶运算符时必须先弹出栈顶运算符并完成一次运算直到栈为空或者栈顶优先级更低新运算符才能入栈。只有熟练掌握了这一层才能真正让程序理解“先乘除后加减”的规则。括号的处理也是套同样的思路遇到左括号直接进运算符栈遇到右括号一直弹出运算符进行计算直到弹出左括号为止。这些逻辑写起来比“简单计算器”复杂不少但每一步都能在原来的基础上找到影子读数据、做分支、处理边界、关注输出。5.3 这个能力还能用到哪些PTA题型上如果你在PTA做题比较多会发现双栈表达式求值的思路并不孤立。天梯赛里的模拟题字符串处理题的“模式匹配”以及二分查找之外那些需要维护单调性的题目都有一个共同点它们不只是把数据算出来而是在数据流变化的过程中保持某种状态。比如“字符串逆序”系列题表面考的是字符数组操作但如果你能想到利用栈后进先出的特性代码思路会清爽很多。再比如二分查找的边界条件分析和计算器里的除零判断有异曲同工之处都是要找清楚“什么情况下程序该走哪条路”。我把这个过程总结成一句话做一道题不是为了把它AC就结束而是要把这道题背后抽象出来的思维模型带走。“简单计算器”教会我们的分支判断、边界检查、格式关注在后面的二分查找、字符串处理、模拟题里全部都会回来找你。我个人做这类题时的习惯是每完成一个基础版本就追问自己一句“如果题目复杂十倍我现在的代码还撑得住吗”撑不住的地方就是下一阶段该练的方向。“简单计算器”只是起点顺着它往栈、状态机、解析器方向走你会发现编程里最有趣的部分才刚刚开始。
返回列表