先说个真实感受:这套“C语言经典100道”,我从学生时代做到工作以后带新人,1-10题一直都是我推荐的起点。别小看这几道题,它们看着基础,却把顺序、分支、循环、数组、字符串这些C语言最核心的骨架全过了一遍。很多人刷题坚持不下去,往往就是死在“第一题太简单不想做、第五题开始看不懂”的中间地带。这篇把1-10题完整拆开,每题给出可直接运行的代码、涉及的知识点、以及我实践里踩过的坑。适合刚学完C语言语法、想找题练手的初学者,也适合笔试面试前想快速过一遍基础语法的人参考。
1. 为什么这套经典100道值得一刷再刷
1.1 题目设计的前后逻辑
这套题流传了很多年,网上能找到的版本少说也有七八个,但前10题的排列逻辑基本一致:从最简单的输出开始,逐步引入变量交换、条件判断、数学计算、循环嵌套、数组和字符串操作。这种顺序不是随手排的,它遵循了学习语言的天然路径——先学会让程序开口说话,再学会让程序做选择,最后让程序处理批量数据。
很多人问过我:“现在网上刷题平台那么多,为什么还要刷这种老古董题?”我的看法是:刷题平台的题目更偏算法竞赛,上来就是动态规划、图论,对初学者并不友好。而这套100题更像是“语法关卡”,每道题对应的语法点非常明确,比如第3题考分支、第5题考循环边界、第8题考嵌套循环输出格式。刷完能建立一套“看到题目就能定位到语法点”的直觉,这对后续学数据结构、刷算法题帮助很大。
1.2 前10题是整个系列的基础底座
前10题覆盖的知识点,恰恰是后面90题反复使用的基本功。以我的经验,很多人做到第30题以后开始吃力,回头一排查,发现是第5题的循环边界、第6题的取模运算没真正搞懂。前10题里每个知识点都值得反复打磨:
| 题号 | 核心知识点 | 关键语法点 |
|---|---|---|
| 1 | 标准输入输出 | printf、转义字符、return 0 |
| 2 | 变量与赋值运算 | 中间变量交换、printf格式控制 |
| 3 | 分支结构 | if/else、逻辑与或、取模运算 |
| 4 | 循环与经典算法 | while循环、辗转相除法 |
| 5 | 循环边界优化 | for循环、sqrt函数、break |
| 6 | 循环与取位运算 | 数位分解、while循环 |
| 7 | 迭代与递归思想 | 斐波那契数列、递归写法 |
| 8 | 嵌套循环与格式化 | 双重for循环、字段宽度控制 |
| 9 | 一维数组操作 | 数组逆序、双指针 |
| 10 | 字符串处理 | 字符串反转、字符数组 |
这张表建议保存下来,刷完前10题对照自查一遍,哪个格子是空的,就回去补哪块。
2. 1-10题逐题拆解与可运行代码
2.1 第1题:Hello World,不是只写一行就完事
题目:在屏幕上输出“Hello World”。
这题几乎是所有人C语言的第一行代码。但我想说的是:如果你只是把代码敲进去、看到输出就觉得自己会了,可能错过了不少细节。我在面试实习生时会问一个很刁钻的问题:“printf("Hello World\n")里的\n到底干了什么?不写行不行?”有人答不上来,有人会说“换行”,但很少有人能说清楚——它是转义字符,代表回车换行,如果不写,下一次输出内容会和Hello World挤在同一行。
#include <stdio.h> int main() { printf("Hello World\n"); return 0; }这段代码里值得留意的点有三个:
#include <stdio.h>是预处理指令,告诉编译器我们要用到标准输入输出函数,printf就在这个头文件里声明。漏了它,编译直接报“隐式声明”警告。main函数是程序入口,标准写法必须是int main(),有些人图省事写void main(),这在某些编译器里能过,但不是标准C,不建议养成这种习惯。return 0;表示程序正常结束,返回0给操作系统。不写的话编译器隐式补上,但强烈建议显式写出来。
我见过不少初学者在环境配置阶段就被劝退了,其实第一题不用急着折腾复杂的IDE,随便找个在线编译平台,把这段代码贴进去跑通,先建立“我能写出可运行程序”的信心,再慢慢研究本地环境。
2.2 第2题:交换两个变量的值,面试也爱考这道变体
题目:定义两个变量,交换它们的值。
这道题最简单直观的办法是引入第三个临时变量。有些资料会教你不用临时变量也能交换,比如用加减法或异或运算。我建议初学者先把中间变量法写熟,因为它的思路最通用——以后学数组、学链表时,交换两个元素这件事会反复出现,临时变量的方案永远是最稳妥、最不容易出错的。
#include <stdio.h> int main() { int a = 3, b = 5; int temp; printf("交换前: a=%d, b=%d\n", a, b); temp = a; a = b; b = temp; printf("交换后: a=%d, b=%d\n", a, b); return 0; }这里有个新手常犯的误区:写成a = b; b = a;。你想啊,第一句执行完,a已经变成5了,第二句再把5赋给b,两个变量都变成5,值没交换成。所以我带新人时总是让他们在草稿纸上画三个盒子,把变量名写在盒子上,把值写在盒子里,然后手动走一遍三行赋值,印象一下就深了。
至于异或法a = a ^ b; b = a ^ b; a = a ^ b;,我个人的态度是:知道有这么回事就行,不要在初学阶段用它炫技,可读性太差,而且面试官不一定认同这种写法。
2.3 第3题:判断闰年,逻辑表达式组合的经典练习
题目:输入一个年份,判断是否为闰年。
闰年的规则是:能被4整除但不能被100整除,或者能被400整除。这道题考察的是把自然语言描述翻译成逻辑表达式的能力,对应C语言里的if/else和逻辑运算符&&、||、!。
#include <stdio.h> int main() { int year; printf("请输入年份: "); scanf("%d", &year); if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) { printf("%d 是闰年\n", year); } else { printf("%d 不是闰年\n", year); } return 0; }很多初学者第一次写会漏掉“不能被100整除”这个条件,输出结果里像1900年这种能被100整除但不能被400整除的年份就被错误地判成了闰年。我建议写完代码后用几个特殊年份验证:2000年(闰年)、1900年(不是闰年)、2024年(闰年)、2023年(不是闰年)。把边界值测一遍,比蒙头跑十次普通年份有效得多。
另外提醒一句:scanf("%d", &year)这里的&不能忘。它表示取地址,把输入的值存到year这个变量的内存地址上。漏掉&是C语言初学阶段出现频率最高、最容易让人崩溃的编译或运行错误之一,后面我还会再提。
2.4 第4题:求最大公约数,辗转相除法为什么优雅
题目:输入两个正整数,求它们的最大公约数。
最笨的办法是从两个数里较小的那一个开始,往下一个个试,直到找到能同时整除两个数的最大数。这个思路没问题,但效率太低。真正经典的解法是欧几里得的辗转相除法,核心逻辑只有一句话:两个数的最大公约数,等于较小数和两数相除余数的最大公约数。用while循环不断把“被除数”和“余数”替换,直到余数为0,剩下的就是最大公约数。
#include <stdio.h> int main() { int m, n, r; printf("请输入两个正整数: "); scanf("%d %d", &m, &n); while (n != 0) { r = m % n; m = n; n = r; } printf("最大公约数是 %d\n", m); return 0; }第一次看到这个算法的人,多少会有点“还能这样操作?”的惊讶——两行替换,就把一个暴力遍历问题变成了O(log n)的快速问题。这个“用余数缩小问题规模”的思路,在后面学递归时还会再次遇到。
如果你已经掌握了函数的概念,可以把它写成递归版本,会更精简:
int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); }我个人建议:先把while版本写熟,理解循环跳出的条件是什么,再去看递归版本。不要一上来就递归,容易把自己的脑子绕进去。两个版本都要会手写,面试里这道题出现的频率真的不低。
2.5 第5题:判断素数,循环边界决定你的算法水平
题目:输入一个正整数,判断它是否为素数(质数)。
素数是指除了1和它本身以外,不能被其他正整数整除的数。最直接的想法是让n从2除到n-1,只要有一次能整除,就不是素数。不过这个写法有冗余——如果n能被某个数整除,那这个数最多到根号n就够了,因为因子总是成对出现的。比如判断49,试到7就够了,因为7×7=49,如果7除不尽,再往后的数也不可能除尽。
#include <stdio.h> #include <math.h> int main() { int n, i; int isPrime = 1; printf("请输入一个正整数: "); scanf("%d", &n); if (n < 2) { isPrime = 0; } else { for (i = 2; i <= sqrt(n); i++) { if (n % i == 0) { isPrime = 0; break; } } } if (isPrime) { printf("%d 是素数\n", n); } else { printf("%d 不是素数\n", n); } return 0; }关键点有两个:
- 循环上界是
sqrt(n)而不是n。这需要引入math.h头文件,用sqrt函数计算平方根。有些编译器环境对sqrt要求链接数学库,如果报错,可以在编译命令里加-lm参数。 break的使用很讲究——一旦确认某个数能整除,立刻跳出循环,没必要继续试下去。这不仅是效率问题,更是一种“及时止损”的编程思维。
我见过不少人忘记处理“n小于2”的情况。1既不是素数也不是合数,2是最小的素数。边界条件不处理,程序逻辑就会出现漏洞。这也是我在解题时反复强调“先处理特殊情况,再写主逻辑”的原因。
2.6 第6题:水仙花数,数位分解的入门必修课
题目:输出所有水仙花数。水仙花数是指一个三位数,其各位数字的立方和等于该数本身,例如153 = 1³ + 5³ + 3³。
这道题的重点在于掌握如何从一个整数中拆出各个数位。对于一个三位数abc,百位是n / 100,十位是n / 10 % 10,个位是n % 10。这里用到了整数除法和取模运算的配合。
#include <stdio.h> int main() { int number, a, b, c; number = 100; while (number < 1000) { a = number / 100; b = number / 10 % 10; c = number % 10; if (a * a * a + b * b * b + c * c * c == number) { printf("%d\n", number); } number++; } return 0; }运行结果就是153、370、371、407这四个数。
这道题真正想让你练的,是把“数字”和“数位”互相转换的能力。后面做题会遇到“回文数判断”“数字反转”“整数转字符串”等题目,全部建立在数位分解的基础上。所以不要只是把这段代码抄一遍,要自己多试几个变体,比如改成输出所有的四叶玫瑰数(四位数各数位四次方之和等于本身),你会发现思路只要稍微迁移一下就能跑通。
2.7 第7题:斐波那契数列,递归与迭代的分水岭
题目:输出斐波那契数列的前n项。斐波那契数列从第1项开始,前两项是1、1,从第3项起每一项都等于前两项之和。
这道题有两种思路:递归和迭代。递归写法非常直白,几乎就是把数学定义翻译成代码:
int fib(int n) { if (n <= 2) { return 1; } return fib(n - 1) + fib(n - 2); }但如果你让这个函数计算fib(50),程序会慢到怀疑人生。原因很简单:递归会大量重复计算。fib(5)要算fib(4)和fib(3),fib(4)又要算fib(3)和fib(2),同一个fib(3)被算了两遍。n越大,重复计算越夸张。这就是为什么我建议用迭代版本:
#include <stdio.h> int main() { int n, i; unsigned long long f1 = 1, f2 = 1, next; printf("请输入项数: "); scanf("%d", &n); if (n == 1) { printf("1\n"); } else if (n == 2) { printf("1 1\n"); } else { printf("1 1 "); for (i = 3; i <= n; i++) { next = f1 + f2; printf("%llu ", next); f1 = f2; f2 = next; } printf("\n"); } return 0; }这里用unsigned long long是因为斐波那契数列增长极快,到第50项左右就会超过int的范围。很多初学者不管数据类型,统一用int,跑到第40项就发现输出了负数,还以为是自己逻辑写错了。这个“用合适类型承接运算结果”的意识,越早建立越好。
迭代版本的思路是滚动更新:每次计算完next,把f2赋值给f1,把next赋值给f2,相当于两个滑块依次前进。理解了这一点,你就掌握了动态规划里“状态转移”的雏形。
2.8 第8题:九九乘法表,嵌套循环与输出格式的磨刀石
题目:打印九九乘法表,即从1×1到9×9的三角形乘法表。
这道题一看就是双重for循环。外层循环控制行数,内层循环控制每一行打印多少项。但真正折磨人的是“格式对齐”——不同乘积位数不同,不控制宽度输出就会歪歪扭扭。
#include <stdio.h> int main() { int i, j; for (i = 1; i <= 9; i++) { for (j = 1; j <= i; j++) { printf("%d*%d=%-2d ", j, i, i * j); } printf("\n"); } return 0; }关键在%-2d这个格式控制:-表示左对齐,2表示占两个字符宽度。这样1×1=1和2×3=6这种单位数结果,输出时也能整齐对齐。你可以试着把-2d改成2d,看看输出变成什么样,就知道左对齐和右对齐的区别了。
输出应该是:
1*1=1 1*2=2 2*2=4 1*3=3 2*3=6 3*3=9 ...我遇到的常见错误是内层循环的条件写成j <= 9,结果每行都打出9项,变成了一个矩形,而不是三角形。这里要时刻记住:内层循环的结束条件依赖外层循环变量的值,这是嵌套循环的重要考察点。
2.9 第9题:数组逆序,双指针思想的第一次实战
题目:定义一个整数数组,将数组元素顺序颠倒。
数组逆序的经典做法是双指针:一个指针指向第一个元素,一个指针指向最后一个元素,交换它们,然后左指针右移、右指针左移,直到两个指针相遇。
#include <stdio.h> #define N 6 int main() { int arr[N] = {1, 2, 3, 4, 5, 6}; int left = 0, right = N - 1; int temp, i; while (left < right) { temp = arr[left]; arr[left] = arr[right]; arr[right] = temp; left++; right--; } for (i = 0; i < N; i++) { printf("%d ", arr[i]); } printf("\n"); return 0; }这里用到了第2题学的交换逻辑,所以我说前10题是一环扣一环的。循环条件是left < right,如果数组长度是奇数,中间那个元素不用动;如果是偶数,左右指针会在中间相遇后自然退出循环。
我见过有人用“定义一个新数组,把原数组从后往前拷进去”的写法,功能上没错,但多占了一倍的存储空间。双指针法是在原数组上直接操作,空间复杂度O(1)。养成“优先考虑原地操作”的习惯,对后续算法学习很重要。
2.10 第10题:字符串反转,从数组到指针的一道坎
题目:输入一个字符串,将其反转后输出。
字符串在C语言里其实就是一个以\0结尾的字符数组。所以反转字符串的思路和反转整型数组很相似,只不过你要注意结束标志\0不能被挪到开头去。
这里我先提醒一个很多教材还在用、但我强烈不建议的新手写法:gets(str)。gets函数不检查缓冲区容量,用户输入稍微长一点就会越界写坏内存,属于公认的不安全函数,新版C标准里已经被移除了。我用fgets代替:
#include <stdio.h> #include <string.h> int main() { char str[100]; int left, right; char temp; printf("请输入字符串: "); fgets(str, sizeof(str), stdin); str[strcspn(str, "\n")] = '\0'; left = 0; right = strlen(str) - 1; while (left < right) { temp = str[left]; str[left] = str[right]; str[right] = temp; left++; right--; } printf("反转后: %s\n", str); return 0; }这里有个非常隐蔽的坑:fgets会把输入时的回车换行符也读进数组里。如果不处理,strlen得到的长度就多算了1,反转结果里会带一个换行符。所以str[strcspn(str, "\n")] = '\0';这行的作用就是把换行符替换成字符串结束标志。strcspn是“返回字符串中第一次出现指定字符的位置”的函数,正好用来找换行符。
计算right时用strlen(str) - 1,减1是为了跳过字符串结束标志\0。这个细节一旦漏了,反转后的字符串会缺字符或者出现乱码。做题时多想想“字符数组的末尾发生了什么”,比死记代码有用得多。
3. 实操环境与排查方法
3.1 用什么环境跑这10道题最省心
这一节是给读者实操时参考的,我不只推荐一个方案,按上手难度排三个层次:
- 在线判题平台:完全不需要本地配置,打开浏览器注册即用,写完提交直接看运行结果和报错信息。对于1-10题这种基础题目,在线平台完全够用,还能看到其他人提交的写法,方便对比。缺点是网络依赖强,而且有些平台对scanf的输入输出有严格格式要求,需要适应。
- Dev-C++:老牌Windows平台免费IDE,界面朴素但功能齐全,自带编译器,适合刚接触编程、还不太会用命令行的人。缺点是停止维护多年,在高分屏下显示比较模糊。
- VS Code + GCC编译器:写代码体验好,配置也不算复杂。我现在的日常环境就是这个组合。如果你之前没配过,照着官方文档一步步来,大概二十分钟能搞定。对于后面90题来说,这个环境能支撑更复杂的调试需求。
我的建议是:如果只是刷完这套题,选在线平台或Dev-C++就够了,不要花太多时间折腾环境。敲代码的工具有很多,真正重要的是你敲进去的每一行代码。
3.2 我实际使用最多的三个调试技巧
前10题虽然简单,但不代表不会出错。我总结三个调试技巧,不花哨但很顶用:
- printf大法:在关键步骤输出中间变量的值。比如交换两个数后先把a和b打印出来,判断循环边界时先打印循环变量。这个方法看着原始,却能在五分钟内定位大部分问题。调试完记得把临时打印删掉,不然输出会变得很乱。
- 边界值测试法:不要只测试正常输入。第3题用1900和2000测闰年判断,第5题用1、2、4测素数判断,第7题用n=1和n=50测边界项。把边界情况测一遍,你的程序才算真正能交出去。
- 人肉模拟运行:拿一张纸,写下所有变量的初始值,逐行执行代码,每次赋值就划掉旧值写新值。这个方法在排查循环逻辑错误时非常好用,尤其是第4题gcd那种变量不断互换的算法,走一遍纸面运行,整个流程瞬间就清楚了。
3.3 编译报错后先看什么
初学者遇到报错最容易慌。我总结一个原则:看编译器给你的第一个错误、最上面的那条信息。很多时候编译器只报了一个错,后面的错误都是由它连带引发的。前10题里最常见的编译问题有这么几类:
- 未声明函数:多半是忘了
#include对应的头文件。printf、scanf要stdio.h,sqrt要math.h,strlen要string.h。 - main函数返回值类型不对:标准C要求
int main(),写成void main()或干脆漏掉返回类型,有的编译器会警告,有的直接error。 - 中英文标点混用:中文输入法打个全角分号或括号,编译器看得一脸懵。一旦报错内容是“expected ‘;’”,先检查标点是不是全角。
- 找不到头文件:通常是文件名写错,比如
<studio.h>这种手滑。
4. 常见错误与避坑清单
4.1 前10题最典型的五个错误
把这几天我在问题排查中反复见到的错误整理成一张速查表:
| 错误现象 | 根本原因 | 解决办法 |
|---|---|---|
| scanf后变量值一直是0 | 忘记写&取地址符 | scanf("%d", &n)而非scanf("%d", n) |
| 输出全是乱码或负数 | 数据类型范围不够 | 斐波那契等大数用long long或unsigned long long |
| 循环死循环不停 | 循环变量没更新或更新条件写错 | 检查循环体内是否有i++或等价操作 |
| 字符串反转结果不对 | 忘记处理换行符或\0 | fgets后去掉换行符,strlen后减1 |
| 判断结果和预期相反 | 逻辑表达式优先级或边界写错 | 用括号明确优先级,特殊值单独测试 |
第2个错误最隐蔽,因为有时候int够用,表象不明显,但一旦n到47左右斐波那契就溢出,数据变成负数,很让人费解。我建议养成习惯:凡是数值可能比较大的变量,优先用long long,不要等到输出错了才回头改类型。
4.2 从“能跑通”到“写规范”的三点建议
会写出正确结果,不等于代码质量合格。我面试新人时不会只看答案对不对,还会看代码长什么样。前10题是你养成好习惯的最佳时期,这三点建议可以直接套用:
- 变量命名要见名知意。写
n、a、b没有问题,但更好的是year、number、left、right。单字母变量在简单题里能看懂,到了复杂题里只会让你自己怀疑人生。 - 统一缩进与大括号风格。条件语句和循环体即使只有一行,也建议写大括号。省了那两对大括号,后面加逻辑时很容易漏加而改变执行结构。
- 写完代码先做一次“人肉编译”。也就是逐行读一遍自己的代码,检查有没有漏了分号、
&、#include。这个习惯能让你在编译器报错之前就消灭一半的问题,别人还在改bug的时候,你已经可以把运行结果截图发出去了。
4.3 如何高效复刷而不是机械抄答案
刷题最忌的是“对着网上的答案复制粘贴”。我推荐一个做法:先把答案盖住,只看题目的原始描述,自己在纸上写出思路,哪怕是伪代码,然后动手实现,跑通了再对照参考代码看差异。如果没思路,那就先看答案,弄懂以后再合上答案自己完整写一遍。第二次能独立写出来,这道题才算真正掌握。
做完1-10题,别急着往下冲。花一点时间把每道题的变体想想,比如第5题改成“输出100以内的所有素数”,第7题改成“输入n,输出第n项”,第8题改成“用while循环实现一遍”。这些变体一旦能独立完成,说明你不是背会了代码,而是理解了思路,这才是刷题的核心目的。
最后聊几句
这套1-10题我前前后后带过不少人刷过。有人一天就全部跑通,有人卡在第7题斐波那契整整一周,都很正常。我个人最深的体会是:第4题gcd和第5题素数判断这两道题值得多花时间,它们第一次把一个“笨办法”优化成“巧办法”的过程展示在你面前,那种恍然大悟的感觉,是很多人对算法感兴趣的开端。
做题期间如果遇到运行结果和预期不符,别急着翻答案,先按上面说的边界测试法多测几组输入。实在搞不定,把代码丢到一边休息半小时再回来,思路往往会突然打开。这是我自己踩过无数坑之后总结出来的最实在的一条经验。