简介:《C语言经典例题100例.pdf》是一部适合C语言入门学习者与编程练习者的典型题集,旨在通过100个经典实例帮助读者夯实语法基础并提升编程实践能力。压缩包内含1个PDF文档,大小约2.77MB,便于下载后离线阅读与反复对照练习。目前已有1201人学习下载。内容覆盖基本数据类型、控制结构、函数、数组、指针、结构体、文件操作等C语言核心知识点;每道题均按“题目—程序分析—程序源代码”三段式编排,并提供完整可运行代码。例如排列组合计数、奖金提成计算、完全平方数判断、日期天数推算等经典问题,既展示循环、分支、数学函数与运算的综合运用,也帮助读者在动手练习中掌握编程思路与调试方法。这份题集既可用于课堂同步训练,也可作为期末复习或备赛刷题的自学资料。
1. 这份 C 语言 100 例,练的不是语法是思路
C语言经典例题100例这份 PDF 在自学圈流传很广,网上能搜到的版本大多没有目录、排版混乱,正文里还夹着大量重复噪声字符。刷完一遍的结论是:题源比排版靠谱,它几乎是照着《C程序设计》课后题和二级 C 语言上机题库整理的,覆盖循环、分支、递归、数论四大块,数学题居多,工程题偏少。适合三种人:刚看完翁恺 C 语言课还不会独立写代码的初学者、备考计算机二级 c 语言的考生、想找现成代码练手但不想从零搭环境的人。拿到后不用按顺序做,先按下面的分类跳着刷,效率高很多。这份 PDF 里的题带完整题面和源码,但错漏也不少,尤其是金额系数、case 分支这类细节,后面会专门指出。
2. 循环嵌套与枚举法:从三位数排列到图案打印
2.1 三重循环与去重条件
程序 1 的题面是:用 1、2、3、4 四个数字能组成多少个互不相同且无重复数字的三位数。这个题考察的核心是循环枚举加条件剪枝。最自然的写法就是三层 for 循环,百位、十位、个位各一层,最后用 if 把三位重复的组合筛掉。
#include <stdio.h> int main(void) { int n = 0, i, j, k; for (i = 1; i <= 4; i++) for (j = 1; j <= 4; j++) for (k = 1; k <= 4; k++) if (i != j && i != k && j != k) { printf("%d%d%d ", i, j, k); n++; } printf("\ntotal = %d\n", n); return 0; }三个循环变量分别对应百位、十位、个位,嵌套后一共执行 4×4×4=64 次。if 里的三个不等式是连续条件判断,i 不等于 j、i 不等于 k、j 不等于 k 同时成立才输出,这样剩下 4×3×2=24 个有效排列。n 的作用是计数,跑完直接显示 total=24,不用人工数输出行。这里的常见错误是把i != j写成i == j,条件反了程序不会报错,但输出结果会剩下全重复的组合,初学者经常在这个地方浪费很长时间。枚举类题目的通用套路就是:先定枚举范围,再写循环,最后用条件剪掉无效组合。程序 22 的两队乒乓球对阵也是同一个思路,只不过循环变量从整数变成了字符 'x' 到 'z',剪枝条件换成i != 'x' && k != 'x' && k != 'z'。
2.2 图案输出里的行列坐标控制
程序 8 输出 9*9 口诀表,程序 9 画国际象棋棋盘,程序 23 打印菱形,这三个题放在一起刷,能建立“用循环控制输出形状”的感觉。九九表的核心是行列配合:
#include <stdio.h> int main(void) { int i, j; for (i = 1; i <= 9; i++) { for (j = 1; j <= 9; j++) printf("%d*%d=%-3d", i, j, i * j); putchar('\n'); } return 0; }外层 i 控制行,内层 j 控制列,printf 里的%-3d表示左对齐、最少占 3 列,这样每个算式之间不会挤在一起。把内层循环条件改成j <= i,输出就从完整的方形表变成下三角表,这说明内层循环的上限决定了图形的宽窄。程序 23 的菱形更典型,把图形拆成上下两半:上半部分第 i 行输出 2-i 个空格和 2i+1 个星号,下半部分第 i 行输出 i 个空格和 5-2i 个星号,行号 i 同时驱动空格数和星号数,这就是字符图形的行列坐标控制。
表格里几个题的对照如下:
| 程序 | 外层循环 | 内层循环 | 关键技巧 |
|---|---|---|---|
| 程序 1 | i 枚举百位 | j、k 枚举十位个位 | 三重循环+互异条件剪枝 |
| 程序 8 | i 控制行 | j 控制列 | %-3d 控制对齐 |
| 程序 9 | i 控制行 | j 控制列 | (i+j)%2 奇偶判断 |
| 程序 23 | 上下两段分开 | 空格星号各自计数 | 拆分图形规律 |
程序 9 的原版用 ASCII 码 219 输出实心方块,在 Windows 控制台正常,在 Linux 终端经常显示成乱码,跨平台调试时换成普通的#或■更稳妥。这类题目不涉及复杂算法,但很练循环变量的边界感,写命令行表格、进度条、终端菜单时都用得上。
2.3 水仙花数与位数分解
程序 13 是枚举法里最值得动手写一遍的题:找出所有三位水仙花数,即各位数字的立方和等于该数本身。核心是分解百位、十位、个位。
#include <stdio.h> int main(void) { int n, a, b, c; for (n = 100; n <= 999; n++) { a = n / 100; /* 百位 */ b = n / 10 % 10; /* 十位 */ c = n % 10; /* 个位 */ if (a*a*a + b*b*b + c*c*c == n) printf("%d\n", n); } return 0; }n/100取百位,n/10%10去掉个位后再取模得到十位,n%10直接取个位,这是分解多位数最常用的三个算式。改写成通用形式:取整数 x 从右往左第 p 位,用x / 10^p % 10。水仙花数全解只有 153、370、371、407 四个,程序跑完就能对照验证。这个题的价值在于把“数字”和“位”分开处理,后面学进制转换、位运算时,本质上还是在做类似的分段拆解。
循环负责重复尝试,分支负责条件筛选,两者几乎总是一起出现。下一章的奖金提成和日期推算,焦点就从“怎么循环”转移到“怎么把条件边界写对”。
3. 分支结构里的区间边界:奖金提成与日期推算
3.1 分段提成:if-else 与边界常数
程序 2 是企业奖金提成,利润分六个档,每档提成率不同。这类题看起来只是套公式,真正的考点在区间端点:利润恰好等于 10 万时按第一档全额 10% 算;等于 20 万时,第二档和第三档计算结果一样,但如果代码里少写一个<=或把else if写成独立if,边界行为就会乱。流传的 PDF 版本里这题系数大面积写错:10%~7.5% 被写成 0.1 和 0.75,20 万到 40 万区间的 5% 被写成 0.5,60 万到 100 万区间的 1.5% 被写成 0.15。直接抄原版代码,奖金会放大十倍甚至百倍,需要按正确比例重写。
#include <stdio.h> int main(void) { long i; double bonus; double b1, b2, b4, b6, b10; if (scanf("%ld", &i) != 1) { printf("input error\n"); return 1; } b1 = 100000 * 0.1; b2 = b1 + 100000 * 0.075; b4 = b2 + 200000 * 0.05; b6 = b4 + 200000 * 0.03; b10 = b6 + 400000 * 0.015; if (i <= 100000) bonus = i * 0.1; else if (i <= 200000) bonus = b1 + (i - 100000) * 0.075; else if (i <= 400000) bonus = b2 + (i - 200000) * 0.05; else if (i <= 600000) bonus = b4 + (i - 400000) * 0.03; else if (i <= 1000000) bonus = b6 + (i - 600000) * 0.015; else bonus = b10 + (i - 1000000) * 0.01; printf("bonus=%.2f\n", bonus); return 0; }b1 到 b10 是预先算好的各档起点累计奖金,判断时当前区间以上的部分单独乘对应比例。用double存奖金避免整数截断,scanf 的返回值检查可以拦掉非数字输入。测试时至少覆盖 0、100000、100001、200000、400000、600000、1000000、1000001 这组边界,提成率变化的位置最容易写错。%.2f控制输出两位小数,金额场景比原版的%d更合适。
3.2 switch 与查表:日期天数的两种写法
程序 4 求某天是当年的第几天。原版用 switch 按月份累加天数,流传源码里有一个隐蔽错误:case 4 的sum=90经常被漏掉,编译不会报错,但 4 月的所有日期都会少算一天。把散落的 switch 改成查表,这类问题从结构上消失:
#include <stdio.h> int is_leap(int y) { return (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0); } int main(void) { int y, m, d, i, sum = 0; int mdays[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; if (scanf("%d%d%d", &y, &m, &d) != 3) return 1; if (is_leap(y)) mdays[2] = 29; if (m < 1 || m > 12 || d < 1 || d > mdays[m]) { printf("data error\n"); return 1; } for (i = 1; i < m; i++) sum += mdays[i]; sum += d; if (is_leap(y) && m > 2) sum++; printf("It is the %dth day.\n", sum); return 0; }mdays 数组下标从 1 开始,先累加前 m-1 个月的天数再加上日。注意闰年时要先把 2 月改成 29 天再做合法性检查,否则闰年 2 月 29 日会被d > mdays[m]误杀掉。闰年且月份大于 2 时补一天,这个逻辑放在累加之后单独判断;如果输入就是 2 月 29 日,闰年的那 1 天已经包含在 29 里面,不需要再补。查表方案比 switch 更直观,也更容易扩展成“计算两个日期间隔”这类后续题目。
3.3 条件运算符与三数排序
程序 15 的成绩分级用score>=90?'A':(score>=60?'B':'C'),三目嵌套写法简洁,但超过两层可读性明显下降,工程里更推荐拆成 if-else。程序 5 的输入三个整数由小到大输出,用三个连续 if 完成交换:
if (x > y) { t = x; x = y; y = t; } if (x > z) { t = x; x = z; z = t; } if (y > z) { t = y; y = z; z = t; }第一轮把最小的数换到 x,第二轮把第二小的放到 y,z 留下最大的。交换必须借助中间变量 t,直接x=y;y=x会丢掉原来的 x。这段代码是冒泡排序的前置练习,理解三数交换后,再看数组里的冒泡排序 c 语言实现就没有障碍。分支解决顺序问题,下一章的递归和递推,解决的是另一类“重复计算”的问题。
4. 递归与递推:兔子、猴子、阶乘背后的两种思维
4.1 斐波那契数列的递推实现
程序 11 的兔子问题描述的就是斐波那契数列,网传版用一个循环两条赋值语句推进两个数:
f1 = f1 + f2; f2 = f1 + f2;第一句执行后 f1 变成第 3 项,第二句再算时 f1 已经是新值,f2 得到第 4 项。这个写法太紧凑,初学者容易绕晕,不如用三个变量的滚动版本:
#include <stdio.h> int main(void) { long f1 = 1, f2 = 1; int i; printf("%ld %ld ", f1, f2); for (i = 3; i <= 40; i++) { long f3 = f1 + f2; printf("%ld ", f3); f1 = f2; f2 = f3; } putchar('\n'); return 0; }f3 保存新项,然后整体右移。这个版本的循环不变量是 f1、f2 始终代表相邻两项,每一步都能在纸上验证。long 在 32 位平台上到第 47 项左右会越界变成负数,刷题时看到斐波那契输出突然出现负值,第一反应先查类型宽度,而不是怀疑算法错了。
4.2 逆向递推:猴子吃桃
程序 21 是正向推导很难、反向递推一步到位的典型题目。猴子每天吃掉前一天剩下的一半再多一个,知道第 10 天早上还剩 1 个,问第一天摘了多少。反向关系是:前一天剩余 =(当天剩余 + 1)× 2。
#include <stdio.h> int main(void) { int day = 9; int x2 = 1, x1; while (day > 0) { x1 = (x2 + 1) * 2; x2 = x1; day--; } printf("the total is %d\n", x1); return 0; }day 是循环次数,x2 存当天早上剩余数,x1 存前一天早上剩余数。从第 9 天往前推:第 9 天 4、第 8 天 10、第 7 天 22、第 6 天 46、第 5 天 94、第 4 天 190、第 3 天 382、第 2 天 766、第 1 天 1534。手动把这条链写出来,代码里的每一步就都能对上。这类题的关键是先写出相邻两天的关系公式,再决定从哪一头开始循环。
4.3 递归:阶乘、逆序输出与年龄递推
程序 26 求 5!,是最简单的递归函数:
#include <stdio.h> int fact(int n) { if (n <= 1) return 1; return n * fact(n - 1); } int main(void) { int i; for (i = 0; i < 5; i++) printf("%d!=%d\n", i, fact(i)); return 0; }递归写起来短,但要清楚两层含义:边界条件 n<=1 返回 1,否则把问题缩小成n * (n-1)!。int 能表示的阶乘只到 12!,13! 已经超出 32 位 int 上限,所以这套题只让算到 5!,不是巧合。程序 27 反向打印字符的原理更值得琢磨:递归展开时不断调用 getchar 读字符,等递归到最深一层才开始 putchar,输出顺序正好反过来,字符串逆序 c 语言 pta 这类题目的标准做法就是这个思路。程序 28 的年龄问题 age(n)=age(n-1)+2 同理,递推到第一个人返回 10,再逐层加回去得到 18。
递推和递归的适用边界差别很大:
| 维度 | 递推(迭代) | 递归 |
|---|---|---|
| 实现方式 | 循环 + 变量更新 | 函数调用自身 |
| 数据流动 | 从初值向前推进 | 先递后归,出栈时计算结果 |
| 主要风险 | 变量更新顺序写错 | 缺边界条件导致栈溢出 |
| 典型题目 | 斐波那契、猴子吃桃 | 阶乘、年龄、逆序输出 |
同一道斐波那契用两种方式都能写,但递归版会重复计算大量子问题,n 稍大性能就明显下降,所以例题里斐波那契选择递推而不是递归,是有原因的。递推适合“已知前一项推后一项”的线性场景,递归适合“问题能拆成同构子问题”的树形场景。
5. 数论算法:素数、质因数、最大公约数的 C 表达
5.1 素数判定:用上界压缩循环
程序 12 判断 101 到 200 之间有多少个素数。原始代码里用k=sqrt(m+1)再试除,sqrt 依赖浮点运算,边界上存在截断隐患。工程写法直接比较平方:
#include <stdio.h> int is_prime(int n) { int i; if (n < 2) return 0; for (i = 2; i * i <= n; i++) if (n % i == 0) return 0; return 1; } int main(void) { int m, count = 0; for (m = 101; m <= 200; m++) { if (is_prime(m)) { printf("%-4d", m); count++; if (count % 10 == 0) putchar('\n'); } } printf("\nThe total is %d\n", count); return 0; }is_prime 里用i*i <= n代替i <= sqrt(n),省一次浮点转换,也避免sqrt截断带来的误判。n 小于 2 的情况先排除。101 到 200 之间一共 21 个素数,跑完用总数核对结果。判断逻辑独立成函数后,主流程只剩枚举和输出,后续要扩大范围、改成多线程分段筛,都不用动主结构。
5.2 质因数分解:while 除尽同一个因数
程序 14 把 90 分解成 233*5。原版 for 循环配合 while 能跑,但边角处理比较碎。改成下面的写法,利用循环不变量:
#include <stdio.h> int main(void) { int n, i; if (scanf("%d", &n) != 1) return 1; printf("%d=", n); for (i = 2; i * i <= n; i++) { while (n % i == 0) { printf("%d", i); n /= i; if (n > 1) putchar('*'); } } if (n > 1) printf("%d", n); putchar('\n'); return 0; }内层 while 反复用同一个 i 整除 n,直到不能整除为止,保证像 3*3 这样重复的质因数全部输出;外层 i 每次加 1,i*i <= n作为结束条件,因为 n 在循环中不断缩小,循环结束时若 n 还大于 1,剩下的 n 必然是一个质数。输入 90 输出90=2*3*3*5,输入 17 输出17=17。输入 1 时这个写法会只输出1=,所以工程版本会把整个分解过程包在if (n > 1)里,这个边界题面没有提,但测试时很容易撞到。
5.3 最大公约数、完全平方数与完数
程序 16 用的是辗转相除法,核心循环可以压到最短形式:
while (b != 0) { temp = a % b; a = b; b = temp; } printf("gcd=%d\n", a); printf("lcm=%d\n", num1 / a * num2);每次把上一轮的 b 变成新的 a,a%b 变成新的 b,直到 b 为零时 a 就是最大公约数。最小公倍数用num1 / a * num2而不是num1 * num2 / a,先除后乘避免两个 int 相乘溢出,这个顺序在很多取模场景里也是通用技巧。程序 3 判断完全平方数的做法是开方后回代验证:
x = (int)sqrt(i + 100); y = (int)sqrt(i + 268); if (x * x == i + 100 && y * y == i + 268) printf("%ld\n", i);sqrt 返回 double,赋值给 int 时截断取整,再通过 x*x 回代确认没有因为开方精度丢解。题目限定 10 万以内,结果有 21、261、1581 三个。程序 19 找 1000 以内完数,本质是枚举因子再求和比较,和质因数分解共享同一个“试除”核心,只是把输出换成累加比较,因子个数需要预留数组空间,原版固定开 10 个,实际 1000 以内的完数因子数不会超过这个值。
数论这块的题型对照:
| 题目 | 数论考点 | 容易踩的坑 |
|---|---|---|
| 程序 12 素数 | 试除上界取 sqrt | sqrt 浮点截断、leap 标志忘记重置 |
| 程序 14 质因数 | 除尽同一因数 | 循环结束剩一个质数没输出 |
| 程序 16 gcd/lcm | 辗转相除法 | 先乘后除导致溢出 |
| 程序 3 完全平方 | 开方回代验证 | 浮点精度漏判整数解 |
| 程序 19 完数 | 因子枚举累加比较 | 因子数组越界 |
6. 把例题改造成可验证的工程代码
6.1 用断言锁住关键结果
这 100 例里有不少题有确定输出,非常适合写成测试用例。猴子吃桃的 1534、水仙花数的四个结果、奖金提成在 20 万时是 17500,都能用 assert 直接锁住。以猴子吃桃为例:
#include <assert.h> int main(void) { int day = 9, x1 = 0, x2 = 1; while (day > 0) { x1 = (x2 + 1) * 2; x2 = x1; day--; } assert(x1 == 1534); return 0; }assert 在 Debug 模式下生效,release 编译定义 NDEBUG 宏后会被整体移除。把例题的正确答案固化成断言,以后改成函数版、优化算法,跑一遍测试就知道有没有改坏。
6.2 边界用例比正常用例更值钱
程序 2 的奖金题重点测每个档位的端点和端点加一:100000 应输出 10000.00,100001 输出 10000.08,200000 输出 17500.00,1000001 输出 39500.01。程序 4 的日期题要测 4 月 30 日和 4 月 31 日、闰年 2 月 29 日和非闰年 2 月 29 日、12 月 31 日。程序 16 的辗转相除,输入 0 会让除法直接崩溃,读入后要先判断非零。程序 14 输入 1,输出不能只留一个等号。这些边界在题面里都没写,但实际跑代码时最先暴露问题的地方就是它们。
6.3 编译选项与后续方向的缺口
推荐用gcc -std=c11 -Wall -Wextra -g test.c -o test编译后运行。在 VSCode 配置 c 语言环境时,把 -Wall 和 -Wextra 加进 tasks.json 的 args 数组,警告会直接显示在终端里。这套 100 例的优点是把基础语法和经典算法覆盖得很全,缺口也很明显:几乎不涉及指针传参和内存管理,没有链表操作,也没有文件读写操作代码,而这些恰恰是 c 语言指针、c 语言链表、c 语言文件读写操作代码方向的地基。刷完这 100 例后,建议先把程序 16 的 gcd 改写成指针参数版本,再把程序 17 的字符统计从getchar()换成fgets()逐行读取文件内容,才算真正把例题的思维接进文件场景。
本文还有配套的精品资源,点击获取