C经典100例几乎是每个学C语言的人都绕不开的一套题。我在带新人、自己也回头复习的时候经常把这100道题翻出来重做。练习31属于典型的“看着简单、做起来有门道”的类型题目要求根据星期几英文名称的首字母来判断对应日期如果首字母相同比如Tuesday和Thursday都是TSaturday和Sunday都是S就继续读第二个字母来确定。这题不涉及指针、不涉及结构体就是纯粹的字符输入、分支判断但它把C语言里几个特别容易翻车的点全串起来了getchar()的读取机制、输入缓冲区的残留、多分支逻辑的组织方式。这篇就围绕这道题把背后的知识点完整拆开。新手可以直接照着写写过几年的老手也能回头补一补基础。1. 题目解读与核心考点1.1 题目到底在说什么先明确题目内容用户输入一个英文字母程序根据这个字母判断对应的星期几。英文的星期几分别是中文英文首字母星期一MondayM星期二TuesdayT星期三WednesdayW星期四ThursdayT星期五FridayF星期六SaturdayS星期日SundayS注意这张表里的关键信息首字母只有M、T、W、F、S五种情况其中T对应星期二和星期四S对应星期六和星期日。当用户输入T或S的时候单独靠一个字母没法确定必须继续读第二个字母。T后面跟u就是Tuesday跟h就是ThursdayS后面跟a就是Saturday跟u就是Sunday。这道题在C经典100例里难度不高但它是练习“分支嵌套”和“字符输入”的极好素材。很多初学者拿到题以后第一反应是用字符串整体比较比如读一个字符串然后和“Monday”“Tuesday”这些全称去比对。这种做法不是不行但它违背了题目本身的设计意图。题目故意让你用“逐步缩小范围”的思路来做这就涉及到对字符输入和分支结构的理解。1.2 这道题真正想考你什么很多初学者以为这题考的是“会不会写if-else”其实没那么简单。我觉得它真正想考察的至少有三个点第一点是字符输入函数的选择。C语言里读入一个字符至少有getchar()、getc()、scanf(%c)这几种办法它们的行为有细微差别。getchar()从标准输入流中读取下一个字符返回int类型这是很多教材推荐的方式。但getchar()不会跳过空白字符也就是说你输入一个字母后敲的回车键\n也会被当成一个字符读走。这时候你要么再调一次getchar()把回车消耗掉要么用别的方式处理缓冲区。这个问题如果没想明白运行程序的时候就会出现“第一次输入正常第二次输入直接跳过”的诡异现象。第二点是分支结构的组织能力。判断星期几需要先判断首字母再在特定情况下判断第二个字母。用if-else能写用switch-case也能写但哪种结构更清晰、更容易扩展这就是代码功底的问题。C经典100例本意是训练基础语法但如果你能在这种小题里养成“结构优先”的意识后面写大项目会受益很多。第三点是边界情况的处理。用户不按套路输入怎么办输入了小写字母怎么办输入了多个字符怎么办虽然题目本身没要求但一个合格的C程序员应该在写这类交互程序时就想到这些。练习31的“标准答案”通常很短但你要往深了挖能挖出不少东西。2. 解题思路与方案选型2.1 为什么用字符比较而不是字符串比较先想一个硬核问题为什么这道题适合逐字符判断而不是读整个字符串来比较英文星期几的全称最短的是Monday6个字符最长的是Wednesday9个字符。如果你想用字符串比较你得先让用户输入完整的星期名称然后再用strcmp()或者strncmp()去匹配。这在功能上完全可行但作者设计这道题的意图显然不是这样——题目特意写了“输入第一个字母如果一样再输入第二个字母”这是在引导你使用“前缀匹配”的思路。前缀匹配在很多场景下都有用比如命令行的命令补全、电话号码的区号识别、词典的索引构建。练习31就是前缀匹配的一个极简模型用最少的输入量在有限的候选集合中唯一确定结果。这种方案的优点是输入成本低、判断效率高缺点是要求候选集合满足“首字母区分度足够”的条件。在星期几这个场景里七天有五个不同的首字母其中两组需要二次区分完全符合要求。2.2 分支结构怎么组织最清晰明确了逐字符判断的思路之后第二个问题就是代码结构。有两种主流写法第一种是用if-else链。先判断letter是不是M不是的话判断是不是W依次往下。缺点是嵌套一深代码会变得很啰嗦尤其处理T和S的时候还要再嵌套一层if-else。第二种是用switch-case。switch (letter)对首字母分派遇到T和S的情况再在分支内部用if-else判断第二个字母。这种写法的好处是结构扁平每个case各自独立逻辑一目了然而且后续如果想增加新的分支规则改动成本很低。我个人推荐第二种。switch-case本来就是为这种“多选一”的场景设计的用在这里语义清晰。有的教材会说“switch只能判断整型或字符型”这恰好满足我们的需求因为char类型在C语言里本质上就是一个小整数。选择switch-case还有一个隐含好处default分支可以统一处理非法输入。如果你用一串if-else最后得靠else来兜底而switch-case天然支持default代码意图更明确。2.3 数据流分析从输入到判断这一步我们按数据流来梳理一下程序的行为用户敲下的内容会先进入标准输入缓冲区。程序调用getchar()时从缓冲区中取出第一个字符。注意如果缓冲区里什么都没有getchar()会阻塞等待用户输入。用户输入“T”并按下回车后缓冲区里实际存了两个字符T和\n。第一次getchar()取走T第二次getchar()取走\n。如果你在取走T之后不额外处理这个\n它就会留在缓冲区里。紧接着程序再次调用getchar()时读到的就会是回车而不是用户输入的第二个字母程序行为就会错乱。这也是为什么我在代码里用了一个“多余”的getchar()或清空循环来吸收回车。初学者经常不理解这一行的作用把它删掉结果程序就出现莫名其妙的问题。这就是缓冲区机制在实际开发中的一个典型体现。3. 完整代码实现与逐层拆解3.1 标准参考实现下面给出我整理过的参考代码这个版本既保留了答案的简洁性又在关键位置加了必要的容错处理#include stdio.h int main() { char letter1, letter2; printf(请输入星期几的第一个字母大写\n); letter1 getchar(); while (getchar() ! \n); // 清空缓冲区确保残留字符不会影响后续输入 switch (letter1) { case M: printf(星期一 Monday\n); break; case W: printf(星期三 Wednesday\n); break; case F: printf(星期五 Friday\n); break; case T: printf(请输入第二个字母\n); letter2 getchar(); while (getchar() ! \n); // 清空缓冲区 if (letter2 u || letter2 U) printf(星期二 Tuesday\n); else if (letter2 h || letter2 H) printf(星期四 Thursday\n); else printf(输入有误无法识别\n); break; case S: printf(请输入第二个字母\n); letter2 getchar(); while (getchar() ! \n); // 清空缓冲区 if (letter2 a || letter2 A) printf(星期六 Saturday\n); else if (letter2 u || letter2 U) printf(星期日 Sunday\n); else printf(输入有误无法识别\n); break; default: printf(输入有误无法识别\n); break; } return 0; }这个版本里我用while (getchar() ! \n);来清空缓冲区比单纯多写一个getchar()要稳得多。它会把当前输入行剩余的所有字符全部读走直到遇到回车为止。这样即使用户多敲了几个字符也不会影响下一次读取。3.2 逐段运行流程演示假设用户严格按照提示第一次输入T第二次输入u程序的执行流程是这样的提示用户输入第一个字母getchar()取走T。while循环把缓冲区里剩下的\n清掉。switch判断letter1等于T进入case T分支。提示用户输入第二个字母getchar()取走u。while循环把回车清掉。if判断letter2等于u输出“星期二 Tuesday”break跳出switch。如果用户第一次输入W流程更短getchar()取走W清掉回车switch直接命中case W输出“星期三 Wednesday”。整个过程不需要第二个字母的参与因为首字母已经能唯一确定。你再观察一下这个程序把“清空缓冲区”这个动作放在每次getchar()之后是有讲究的。如果你只清一次第二次读取时缓冲区里可能还残留着上次输入的换行符导致letter2读到\n判断直接失败。这种问题在交互式程序里特别典型。3.3 容易翻车的输入细节这里重点说三个细节每个都是新手翻车高发区。第一个细节getchar()的返回类型是int不是char。很多教材会强调这一点因为EOF文件结束标志在标准C里被定义为-1不能用char来完整表示。虽然在这个题目里我们只是读取普通字母不会遇到EOF但你要养成好习惯。如果某天你写的程序需要从文件里循环读字符就一定要用int来接收getchar()的返回值否则遇到EOF会出问题。第二个细节缓冲区清空记不住要写。有些初学者觉得while (getchar() ! \n);这行代码很怪看不懂就删了。其实它的作用就是“把这一行剩下的字符全部吃掉直到换行符为止”类似于给输入流做了一个简单的清理。删掉它程序在单次输入时不会立刻报错但在需要连续读取字符的时候就会暴露问题。比如你先输入了“Tu”两个字母再回车那么letter1是Tletter2直接就是u根本不会给你第二次输入的机会。第三个细节输出格式。题目要求判断星期几但没有明确要求输出格式。你可以输出“Monday”也可以输出“星期一 Monday”甚至可以只输出“Tuesday”。我建议输出完整信息因为程序的可读性和用户体验也是一部分。你总不能让别人猜你输出的是什么东西这不符合实际开发习惯。4. 常见问题排查与避坑实录4.1 缓冲区残留导致第二次读取失败这是练习31里遇到最多的一个问题。症状很典型程序第一次getchar()正常第二次getchar()不等用户输入就直接跳过了letter2读到了回车或者一个奇怪的字符。排查思路其实不复杂。第一步在letter2 getchar();这行后面加一个printf(%d\n, letter2);看看读到的到底是什么ASCII码值。如果输出的是10那对应的就是\n基本可以断定是回车残留。如果输出的是其他值说明用户输入里混入了多余字符。第二步检查你的清空缓冲区代码是否在每次读取后都执行了。我见过有些初学者把清空缓冲区的代码只写在main函数开头后面再也不管这当然不行。正确的做法是每次getchar()读取一个“逻辑输入项”之后都要清一次缓冲区。所谓的“逻辑输入项”在这道题里就是“一个字母加一个回车”字母是有效数据回车是输入确认符。4.2 大小写不统一怎么办题目里输入的是大写字母因为英文星期几的首字母本来就是大写。但实际运行时用户很可能输入小写。最简单的做法是在判断之前统一转换把letter1和letter2都转成大写或者都转成小写。C语言标准库里的tolower()和toupper()函数可以干这个事它们声明在ctype.h头文件里。比如#include ctype.h letter1 tolower(letter1);这样无论用户输入T还是t后面统一按小写t来判断。如果你不想依赖头文件也可以自己写一个转换逻辑如果字符在a到z之间就减去32变成大写。这个技巧在很多题目里都能用值得记住。我做的参考代码里直接把大小写两种情况并列在if条件里了比如if (letter2 u || letter2 U)这也是一种处理办法。好处是不依赖库函数坏处是条件稍微啰嗦一点。两种做法都行看你的风格。4.3 输入了多余字符怎么办还有一种情况是用户不按套路出牌第一次输入就直接敲了“Tu”两个字母。缓冲区里会依次排列T、u、\n。程序第一次getchar()取走T然后while循环把u和\n全部吃掉。注意这里的u被清掉了用户之后看到的提示是“请输入第二个字母”但他以为已经输入过了就会再敲一个字母最终导致判断失败或者报错。怎么处理这个问题其实有两种思路。思路一不让用户有机会多输。把两次输入合并成一次提示用户“请输入前两个字母”。但这改变了题目逻辑不推荐。思路二提示用户输入完一个字母后按回车并且在上一次清空缓冲区时把多余的字符也读走。就在while循环里把u和\n都消费掉逻辑上没问题行为上也没问题。用户虽然多敲了字符但不会影响后续输入最多是后续提示有点突兀。对于这种练习程序做到这个程度已经足够了。5. 扩展思路把练习31做出花来5.1 用字符串表重写这道题虽然原意是逐字符判断但我们可以换一种思路先把所有星期名称存在一个字符串数组里然后让用户输入任意长度的前缀用strncmp()去匹配所有候选把匹配数量缩减到1时输出结果。#include stdio.h #include string.h int main() { char *weeks[] {Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday}; char input[20]; int i, match 0, len; printf(请输入星期几的英文前缀例如 T、Tu、S); scanf(%s, input); len strlen(input); for (i 0; i 7; i) { if (strncmp(weeks[i], input, len) 0) { printf(匹配结果%s\n, weeks[i]); match; } } if (match 0) printf(没有匹配项\n); return 0; }这个版本的价值在于它把“前缀匹配”的思想更完整地展现出来了。用户可以输入一个字母、两个字母甚至完整拼写程序都能正确处理。你甚至可以把用户输入的前缀逐步增长每次都重新匹配这就是一个简单的实时联想输入模型。从这个角度说练习31背后的思想比它表面的代码重要得多。5.2 封装成独立函数我们还可以把判断逻辑封装成一个函数比如int get_week_from_prefix(char first, char second)函数返回一个枚举值或者星期编号。这样main函数会变得非常干净判断逻辑可以复用。这也是从“写练习题”走向“写工程代码”的重要一步。封装之后的调用方式大致是先用getchar()读首字母如果需要就再读第二个字母然后把两个字母传给函数函数内部做判断并返回一个整数比如0表示输入无效1到7分别表示星期一到星期日。这样无论以后是想输出中文还是英文都只需要根据这个返回值做映射。如果你愿意还可以把输出也映射成数组下标char *week_names[] {, Monday, Tuesday, ...}然后直接week_names[返回值]就能拿到对应的英文名称。这种“数据驱动”的思路在练习题里提前接触一下对后续学结构体、链表甚至数据库操作都有帮助。5.3 配合文件读取实现批量判断如果你想进一步折腾可以把输入和输出重定向到文件。在命令行里执行program.exe input.txt output.txt程序就会自动从input.txt读入数据并把所有输出写入output.txt。这样你只需要在input.txt里准备多组首字母数据就能批量测试程序的判断逻辑。这种重定向的方式在本地调试C程序时非常有用。尤其当你写了一堆测试用例不想一遍遍手动输入的时候把所有用例写进文本文件再统一跑效率高得多。配合diff命令对比输出结果你甚至能做简单的自动化测试。建议有条件的读者试一试这个习惯对以后写更复杂的程序很有帮助。5.4 结合调试工具观察缓冲区行为练习31这种题目特别适合用调试器观察程序运行时的变量变化。很多人在VSCode里配好了C语言环境却不知道怎么利用断点。就拿这道题来说你可以在letter1 getchar();这行打一个断点然后单步执行观察当你输入T并回车后letter1变量的值是85也就是T的ASCII码。再往下走你会发现while (getchar() ! \n);这个循环确实把缓冲区清空了。用调试器亲眼看一次缓冲区是怎么工作的比单纯背“getchar会残留回车”这个结论要有效得多。如果你用的是gdb可以用print letter1来查看变量值用VSCode的话直接在变量面板里就能看到。我强烈建议初学者在刷题的时候顺手把调试工具用起来省得以后再花时间专门学。最后再分享一个小技巧练习31这种题目别只看答案就过了。拿到题目先自己动手写一版再想想有没有第二种写法再想想如果需求变更了代码要不要大改。能把一道小题玩出这个深度C语言基础的扎实程度一定不会差。我自己后来带人也经常用这题当面试摸底效果很不错。