☰
C语言经典练手题:水仙花数拆解循环与取余的巧妙应用
2026/10/15 6:56:50 网站建设 项目流程

这两年我一直在坚持整理一套C语言入门练手题,专栏名字就叫“菜鸟编程C语言100例”。前面三道题分别过了一遍变量与数据类型、格式化输出、if-else分支判断,到了第四题,我想着该让循环结构登场了。但纯写一个“for循环讲解”又太枯燥,于是把题目设计成了“输出100到999之间所有的水仙花数”。别急着觉得这道题烂大街,恰恰因为它经典,才适合放在第四题这个位置——它能把循环、取余取整、数学思维和调试习惯串联起来,是让人真正“会写代码”而不是“会抄答案”的关键一题。

这道题适合刚学完if和for语法、想找个练手项目的初学者,也适合带新人的老手直接拿来做两周的小练习。它的核心价值不在“会做”,而在“能不能讲清楚为什么这么做”。下面我把这道题完整的思考过程、初版代码、踩坑记录和优化路径拉出来聊一聊。

1. 题目分析与整体设计思路

这一题的核心描述很简短:编写一个程序,输出所有三位水仙花数。所谓水仙花数,指一个n位数,其各位数字的n次方之和恰好等于该数本身。以三位数为例,若某个数abc满足 a³ + b³ + c³ = abc,那它就是水仙花数。经典的三位水仙花数有四个:153、370、371、407。

拿到题目后,正常的第一反应是“怎么取到百位、十位、个位”。很多新手会卡在这里,不是因为循环不会写,而是不知道如何用整数除法和取模运算把一个数拆开。我个人的习惯是先在稿纸上干跑一遍,比如拿153举例:153除以100取整得到1,153对100取模得到53;53再除以10取整得到5,53对10取模得到3。这个过程其实就是“从高位到低位逐层剥离”。

把拆位的逻辑搞定之后,剩下的结构就清晰了:一个for循环从100走到999,循环体内部做拆位、计算立方和、条件判断,满足条件就输出。宏观上,这题的教学重点有两个层面。第一层是语法层面,让学习者熟悉for循环的基本骨架;第二层是思维层面,让学习者明白“枚举 + 筛选”是计算机解题的基本范式。

我见过不少人第一次做这道题时,上来就直接查百度“水仙花数C语言代码”,然后复制粘贴运行出了结果,觉得自己会了。但换个题目,比如“输出所有四叶玫瑰数”,或者“输出100以内所有满足各位平方和等于自身的数”,立刻就不会了。这就是因为枚举思维没建立起来。所以整个题目的设计,都围绕“让初学者真正理解拆位和循环的关系”来展开。

2. 核心知识点拆解与原理补充

这一题虽然短小,但牵扯出来的知识点很密集,尤其是拆位的数学原理。如果这部分地基打不牢,后面学数组、学字符串处理时可能还会踩坑。

2.1 拆位的两种思路

先明确一个前提:在我们讨论的三位水仙花数场景里,拆位本身是纯数学操作,不需要把整数转成字符串。最常用的方法是整除与取模组合。

以一个三位数n为例:

  • 百位 = n / 100,这个结果是一个0到9的整数。C语言中整数除以整数,结果直接截断小数部分,所以153/100得到1。
  • 十位 = n / 10 % 10,也可以写成 n % 100 / 10。区别在于运算顺序。n/10先把153变成15,然后15%10得5;n%100先把153变成53,再除以10取整得5,结果一样,中间经过的中间量不同。
  • 个位 = n % 10,153对10取模直接得到3。

第二种写法 n % 100 / 10 是我个人更推荐新手练习的。因为它的计算过程和人们口算拆位的过程一模一样:“先扔掉百位以上的部分,再把剩下的十位个位里的十位分离出来。”无论先用除法还是先用取模,最后拿到的结果都是稳定的,不存在谁对谁错。但你需要保证自己写完后能用具体数字走一遍,避免把运算符优先级搞混。

我见过有初学者把十位写成 n / 10 % 100,得到的是15而不是5,这就是典型的“顺序没理清”。遇到这种问题,不要空想,直接在编译器里加上printf打印中间值,逐行看结果,比对着屏幕发呆有效一百倍。

2.2 运算符的坑与优先级提醒

C语言里,/和%的优先级相同,结合方向是自左向右。这句话看起来简单,但实际运用时很容易出问题。比如表达式 n / 10 % 10,实际执行顺序是 (n / 10) % 10,因为同一优先级从左往右算。如果你期望的是 n / (10 % 10),那就大错特错了,因为10%10等于0,除以0直接运行时错误。

还有一种经典错误:使用浮点数pow函数做立方计算。标准库math.h里提供了pow函数,比如 pow(a, 3) 表示a的三次方。按理说可以用,但这里有个隐藏的精度问题。pow的返回值是double,浮点数在计算机里本质上是有精度损失的。当你拿一个整型变量接收pow的返回值时,编译器会做隐式转换,一旦计算结果出现类似27.000000000000004或26.999999999999996这样的值时,赋给int后就会被截断成27或26,极可能导致判断失误。

所以在这个阶段,我更建议直接使用 a * a * a。这不仅绕开了浮点精度隐患,还让代码更加直白,一看就懂。后面学到函数封装时,再考虑把立方运算包成一个自定义函数,而不是依赖pow。

2.3 循环结构的边界思考

for循环的三段式结构——初始化、条件判断、循环后操作,很多人只会照着写for(i=100; i<=999; i++),但未必想过为什么写成<=999而不是<1000。两者运行次数完全一样,对结果也没有影响。但从可读性角度,我推荐写成 i<=999,因为题目明确定义的是“三位数”,最大就是999,让代码的表达范围和题目描述保持一致,别人读起来不需要额外换算。

同样值得思考的是,能不能不从100开始,而是从100开始逐个枚举,这个问题的答案显然是肯定的,三位数就是从100开始的,包括100本身。也就是说,循环起点可以写成i=100,不需要从0或1开始去白跑几百次。很多人写循环喜欢“从0开始走遍所有数”,在这道题里那是在浪费CPU。

3. 实操过程与代码实现细节

到了这一节,我们把整个代码分步骤搭起来。先说调试环境:我平时练习用的是Visual Studio Code加MinGW的gcc编译器,调试时打断点比IDE要麻烦一点,所以会用printf打印中间状态来辅助。如果你用的是Dev-C++或某高校OJ系统自带的在线编译器,操作方式完全一致,代码可以直接跑。

3.1 确定整体框架

第一步,写标准头文件和主函数框架。包含stdio.h即可,不需要额外引入math.h,因为我们要刻意避开pow。

#include <stdio.h> int main() { // 循环遍历100到999 // 拆位,计算立方和,判断输出 return 0; }

很多初学者会把int main写成void main。我的建议是坚持用int main,并返回0。这不是什么考试强迫症,而是在C标准里,int main是唯一被标准定义的形式。某些古老编译器兼容void main,但换到新环境或者参加在线评测时,可能出现警告甚至编译失败。细节决定习惯。

3.2 拆位与判断的初版实现

接下来写循环体。这里我给出的第一个版本,也是大多数教程里会写的版本:

#include <stdio.h> int main() { int i; int a, b, c; int sum; for (i = 100; i <= 999; i++) { a = i / 100; // 百位 b = i / 10 % 10; // 十位 c = i % 10; // 个位 sum = a * a * a + b * b * b + c * c * c; if (sum == i) { printf("%d\n", i); } } return 0; }

逐行说明一下每一步的意图。

循环变量i用来承载当前被检查的三位数。a、b、c三个变量分别存放百位、十位、个位的数字。拆位之后,直接对这三个数字做立方求和。这里如果用a= i/100,那么对于153来说a=1,对于370来说a=3,完全符合预期。再用b = i / 10 % 10,拿153举例,i/10得到15,15%10得到5。最后个位直接i%10。

sum变量存储三个数字立方后的总和。紧接着用if做一个等价判断。如果判断通过,就把这个数通过printf打印出来。输出格式我用的是%d\n,每个结果占一行,简单清晰,方便观察输出列表。

运行这个程序,正确的输出应该是:

153 370 371 407

如果你拿到的结果只有前三个,或者只有第一个,说明判断条件或者拆位逻辑有问题。此时不要直接盯着代码硬看,把sum值打印出来比对一部。

3.3 加入打印调试信息的过程

我在实际带人过程中发现,初学者很难一次写对。与其给一个完美答案,不如让学习者自己把调试信息加上去,观察拆位的结果对不对。比如在sum赋值之前插入:

printf("当前数字: %d, 百位: %d, 十位: %d, 个位: %d\n", i, a, b, c);

对153而言,会输出“当前数字: 153, 百位: 1, 十位: 5, 个位: 3”。如果输出变成“百位: 1, 十位: 15, 个位: 3”,说明你的b取错了,问题就出在“取模和除法的顺序”上。这种加调试打印的方法,比看代码要直观得多,也很建议保留这个习惯。使用完调试后,把它注释掉,保持最终输出的清爽。

这个过程也体现了真实的编程日常:不是一次写对,而是通过观察中间变量不断修正。这也是为什么我一直提倡,练编程题时一定不能只看别人的完整代码,必须自己动手敲一遍,甚至故意写错几版,再通过调试改正,记忆才会牢固。

4. 常见问题与排查技巧实录

写这道题的过程中,我整理了三个出现的频率最高的问题,每一个我都亲自在课堂上帮人排查过。

4.1 输出的结果是空的,一个数都没有

这个现象的原因通常是判断条件写反或者写错。比如有人把 if(sum == i) 写成了 if(sum = i),注意这里是赋值而不是比较。sum = i 这个表达式会把i的值赋给sum,然后判断赋值后的sum是否为真,整体恒为真,导致程序把每个数都打印出来——不对,这个现象的表现应该是打印全部三位数,而不是一个都没有。

如果真的是一个都没有,多半是sum计算成了别的值。常见原因有两个:一是对i做了浮点数相关的错误操作,二是把a、b、c的取值搞错了,比如十位算成了15以上。这时候用上面的调试打印最有效。

4.2 结果只输出了153,没有370、371、407

这个情况比较隐蔽。我排查过的一个案例是,有人把个位写成了 i % 10,这没问题,但他把十位写成了 i % 10 % 10,看起来好像也是“先取余再取余”,实际执行时确实能算出一个数,但对像370这样的数,i%10先得到0,再%10还是0,十位就变成0了,这导致判断失败,最终结果只剩153之类的数。

另一个隐蔽的坑是使用pow函数。有同学写pow(a,3),打印pow的结果时看到是27.000000,感觉没问题,但赋值给int的瞬间发生截断。特殊情况如pow(5,3)在某些编译器环境下计算结果可能是124.999999,转成int后变成124,丢掉一个水仙花数。所以,再次强调:不要用pow做整数小次方运算。

4.3 编译时出现“undefined reference to `pow'”

用了pow函数的人会遇到链接错误。因为pow函数在数学库里,gcc编译时需要在末尾加-lm参数。加上之后能编译通过,但前面说的精度问题仍然存在。我的建议是,从初学阶段就养成用乘法直接计算的习惯,等以后理解了浮点存储原理再考虑pow也不迟。

下面把常见排查要点整理成一张速查表,方便你对照自查。

现象可能原因排查方法
一个结果都没有拆位错误 或 赋值符号误写调试打印a、b、c的值
结果只有部分水仙花数十位拆位错误 或 pow精度问题打印中间sum值,换用连续乘法
编译报pow相关错误未链接数学库 或 用错了pow优先避免pow,用aaa
输出的数字范围不对循环边界写错检查for是否从100到999

5. 从这题延伸出来的几个改进方向

这道题本身写对只是及格,更值得玩味的是后面不断改造它的过程。几个方向我都在自己的练习题集里试过,拿来分享。

第一个方向是把代码封装成可复用的函数。比如写一个int isNarcissistic(int num),专门判断一个数是不是水仙花数,然后main里只做循环调用。这个小改动对后续学习模块化编程很有帮助,把判断逻辑和遍历逻辑分开,代码结构会更干净。

第二个方向是让程序支持任意位数。比如输入一个n位数,判断它是否满足“各位数字的n次方之和等于自身”。这就涉及循环动态计算指数,不再是固定三次方。此时你可以实现一个自定义整数次幂函数,比如用for循环累乘返回整型结果,既绕开pow精度问题,又不限制次数。

第三个方向是扩展成水仙花数的“强化版”练习:找1000以内的所有“阿姆斯特朗数”,或者找100以内的所有“各位数字平方和为原数”的怪数。本质上还是一样的枚举加筛选,但数字位数不同、指数不同,学生必须真正理解拆位思想才能举一反三。

我在实际练习中还发现,把这道题改为“输出水仙花数的个数”也很有价值。你只需要用count变量累计符合条件的数字数量,最后输出count。这个改动虽然简单,但对for循环中计数器的使用是很好的训练,后面处理统计类题目时会频繁用到。

写在最后的做题建议

这道题我做过很多遍,每次带新人重做一遍都能看到新的坑。有一点我特别想强调:写编程题不是背答案,也不是比谁第一次就写对。真正重要的是,你拿到题之后有没有在纸上演算,有没有故意制造几个错误再把它调通。我见过不少学习者,看别人的代码觉得很简单,自己合上编辑器却写不出来,原因就是“观看时代入了别人的逻辑,书写时没有建立自己的逻辑”。

如果你刚做完这道题,可以试着挑战一下:不看我上面给的代码,自己从零写一遍完整可运行的程序。写完再对照一下拆位的写法,看看是不是用了自己能够解释清楚的方式。如果讲解给别人听也能讲明白,那这道题才算过关。

后面第五题我打算把这个水仙花数问题改成用函数封装的形式再写一遍,顺便引出数组的概念。到那个时候回头再看今天的代码,你会发现当初觉得抽象的东西,其实只是成长路上的一个台阶。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询