mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
9084 字
25 分钟
C 语言学习笔记
2026-10-17
2026-10-21
无标签

按 OJ 题目顺序学习 · 边做边更新 配套人:DSH 助手 | 学习方式:做一题 → 讲透 → 记录


一、学习路线总览#

C 语言的主干就这几块,OJ 的题目顺序基本就是照这个来的:

阶段核心内容对应题型状态
① 顺序结构变量、类型、scanf/printf、算术运算绝对值、复利、位数✅ 基本掌握
② 选择结构if/else if/else、&&/||、switch闰年、分段函数、四则运算✅ 基本掌握
③ 循环结构while、for、累加/计数/打擂台求和、求最大值🔄 正在学
④ 嵌套循环循环里套循环图形打印、九九表、质数⬜ 未开始
⑤ 数组一维/二维数组、排序、查找查表、统计、矩阵⬜ 未开始
⑥ 字符与字符串char、字符数组、string.h字符统计、回文、加密🔄 部分(char 学过)
⑦ 函数定义、调用、参数、递归阶乘、斐波那契⬜ 未开始
⑧ 指针地址、指针运算、数组与指针——⬜ 未开始
⑨ 结构体struct、结构数组学生成绩管理⬜ 未开始
⑩ 文件fopen/fclose/读写文件统计⬜ 未开始

当前重点:③ 循环结构 → 开始接触「经典算法」(第 38 题辗转相除法是第一个)。


二、核心代码模式速查#

这五个骨架覆盖了 80% 的入门题。看到题目先想”往哪个骨架里套”。

模式 1:分段判断(选择结构)#

if (条件1) {
...
}
else if (条件2) { /* ← 必须是 else if,不能是独立 if */
...
}
else { /* 兜底 */
...
}

要点:分支互斥时一定用 else if 链。


模式 2:累加器(求和/求平均)#

long long sum = 0; /* ① 先清零 */
for (i = 0; i < n; i++) {
scanf("%d", &x);
sum += x; /* ② 累加 */
}
printf("%lld\n", sum); /* ③ 输出 */

要点:sum 必须清零;防溢出用 long long。


模式 3:计数器(数个数)#

int cnt = 0; /* 先清零 */
for (...) {
if (满足条件) cnt++; /* 命中才 +1 */
}

模式 4:打擂台(求最值)#

int max = 第一个数; /* ① 立擂主,不能用 0! */
for (...) {
scanf("%d", &x);
if (x > max) max = x; /* ② 严格 > ,平手不换 */
}

要点:

  • 初值不能给 0(除非确定有非负数)
  • 用严格 > → 平手时保留先出现的

模式 5:判断某性质(试除/逐位处理)#

/* 例:判断质数 */
int isPrime = 1;
for (i = 2; i * i <= n; i++) { /* 试到开方就够 */
if (n % i == 0) { isPrime = 0; break; }
}
/* 例:逐位处理(水仙花数、求位数) */
while (n > 0) {
int d = n % 10; /* 取最后一位 */
...
n /= 10; /* 砍掉最后一位 */
}

三、⭐ 个人易错点清单(基于实际踩过的坑)#

这份清单比上面的模式更值钱 —— 每一条都是你真犯过的错。

A. 标点符号类(最高频!)#

错误例子后遗症
漏分号printf(...) 后没写 ;编译错误
多分号if (x>0);if 被空语句吃掉,else 找不到 if
case 后写分号case '+';必须是冒号 case '+':
{ } 不配对少一个 },后面的 if 嵌进去了编译错误或逻辑全乱
break 位置default: 写到 switch 的 } 外面default 和 break 双报错

自查方法:提交前数一遍 { 和 };检查每个 if (...)、while (...) 右括号后有没有多余分号。


B. 分支结构类#

错误正确做法
该用 else if 却写了独立 if分支互斥 → 必须 else if
条件写成 'a'<=c<='z'必须写 c>='a' && c<='z'
if 和 else 之间插了语句else 必须紧跟 if 的语句
用了 a > b > c必须写 a > b && b > c
&& / || 用混都要满足 → &&;满足其一 → ||

C. 循环结构类#

错误正确做法
忘写 i++死循环(TLE)
差一错误起点 0 配 <,起点 1 配 <=
累加器没清零sum = 0 必须先写
打擂台初值给 0全负数时输出 0;应先读第一个数当擂主
while (...); 多分号同 if 的坑

D. 变量与类型类#

错误说明
变量未初始化int m; 直接用 → 输出垃圾值(实测见过 32759)
累加用 int和可能超 21 亿 → 用 long long + %lld
a*a 溢出大数相乘会变负数 → 用 long long
格式符不匹配int→%d,long long→%lld,double→%lf(scanf)/%f(printf)
abs vs fabs整数用 abs(stdlib.h),小数用 fabs(math.h)

E. 输出格式类(吃过亏!)#

坑例子
大小写题目要 Yes/No,写了 yes/no → WA
末尾标点题目要 yes.(带点),漏了点 → WA
保留位数%.2f 不是 %.6f
单独占一行别忘 \n

铁律:题目要求输出什么,就一个字符不差地照抄。


F. 数学/业务理解类#

坑例子
分段累进漏档位工资题:第三档要加上前两档拿满的钱
浮点数不能用 == 比 0用 |s2| < 1e-10(或两边夹 -1e-10 < s2 && s2 < 1e-10)
先检查再计算除法前查除数;三角形先查三边关系

四、提交前 30 秒自查清单#

  1. 编译一遍,看到零警告才交
  2. 每个 printf/scanf/break 后有 ; 吗
  3. if (...)、while (...)、case xxx: 后面没有多余分号吗
  4. { 和 } 数量相等吗
  5. 变量都赋初值了吗(尤其是累加器/计数器/擂主)
  6. 输出格式照抄题面(大小写、标点、换行、小数位)
  7. 在分界点各测一个值(如 x=-2、x=3、50000/50001)
  8. 会不会溢出(int 加起来够大吗)

五、已完成的题目记录#

#题目考点当时的问题
1求绝对值顺序结构 + ifscanf 没检查返回值
2存款复利pow vs 循环用了单利公式
3求位数/ % 拆位讨论了 log10 的精度陷阱
4字符大小写char、ASCII 差 32分界线写成 90、加减方向反了
5字母序号字符相减多了个分号
6三字符最大打擂台> 方向写反(变成求最小)
7字符分类else if 链漏 3 个分号 + scanf 多了空格
8水仙花数拆位 + 立方✅ 一次通过
9闰年&&/|| 组合输出大小写 No 写成 no
10门票计费简单分支变量名 piao/n 混用
11判断三角形三边关系多一个条件少一个条件
12直角三角形勾股定理 + ||讨论了 int 溢出
13象限判断互斥分支前两个 if 没写 else if
14销售工资分段累进比率小 10 倍 + 漏档位
15分段函数绝对值printf 插在 if/else 之间
16月份天数查表 vs switch把”闰年”当成”输出 29”
17四则运算switch + 浮点判零漏除零检查;case 后写分号;default 在 } 外
18绝对值最大打擂台 + 平手规则m 未初始化 + 比错对象
19n 个数求和循环入门✅ 一次通过
20n 个数最大值打擂台 + 循环初值给 0(全负数时输出 0)
21不及格率计数器 + 浮点除法分数线写成 90;整数除法 cnt/n
22统计正/负/零多个计数器i 未声明;for 分隔符写成逗号;忘读 n
23奇数乘积累乘器(初值 1)%n 应为 %d;%llf 应为 %lld
24自然对数表格式宽度 + log✅ 一次通过
25阶乘表递推(fact *= i)内外层同用 i → 死循环;公式写成 (j-1)*j
26奇偶平方/立方和分支累加行尾写逗号;ji*ji*ji+ji;&m &n 少逗号
27阶乘累加(1!+…+n!)递推 + 累加忘读 n;内层乘到 n 而非 i;jie=0
28数列平方根递推 + 累加x=item 写在 scanf 前(item 还是 0)
29a+aa+aaa…递推 + 累加两行顺序反了:先 sum+=x 后 x=x*10+a
30猴子吃桃(倒推)逆推(倒着算)循环 n 次应为 n-1 次 + 初值 0 应为 1(两个错叠在一起)
31斐波那契兔子二阶递推 / 滚动变量fib=sum 抹掉前一项 → 变成 ×2 输出 2 的幂
32幸运数字(7的倍数非4的倍数)不枚举 + 从上界往下退for 条件方向反了(n<=m 永不成立);break 插在 if/else 之间;printf("%d ",n) 空格代替换行
33判断素数试除法 + 标志变量 flag①break 写在 if 外面(循环只跑一轮 → 退化成判奇偶,1000 个里错 333 个)②改完漏了 n < 2(1 被判成素数)
34阶乘不等式 1!+…+m! < nwhile + 三条更新 + 类型溢出✅ 一次通过!(前四题连踩坑后,第一次完全自己写对)
35学生成绩最高分(读到负数为止)哨兵值 + 不定项输入 + 打擂台✅ 又一次性通过!(只有一处小瑕疵:printf 末尾没加 \n)
36逆序输出正整数各位% / 拆位 + 次数未知的循环✅ 逻辑与输出全对(逐字节比对样例通过);唯一隐患:第 10 行 int yu=0; 写在 scanf 之后(声明与语句混写),老判题机按 C90 会报编译错误
37正序输出正整数各位位权 h + 两次循环(高位优先)✅ 通过(穷举 200 万个数、严格 C89 编译全部通过)。但连错三轮:①wei 当成”位数”用(wei++)而不是”位权”(wei*=10)②int m=n; 写在 scanf 之前(备份到的是 0)③漏掉 n = n % wei;(吐出的是前缀 1 12 123 1234 12345)
38最大公约数(辗转相除法)第一个”有名字的算法”✅ 一次通过! 三行核心 y = m % n; m = n; n = y; 顺序完全正确,零警告,严格 C89 编译通过。实测 400 万对穷举 + 200 万对随机大数 + 9 万对枚举对照 + 42 对相邻斐波那契 + 20 万组倍数性质 → 不一致 0 处
39最大公约数 + 最小公倍数先除后乘防”中间过程溢出”⚠️ 差一行。第一版 c = a*b/m; → 1000000 1000000 输出 1000000 -727(a*b 在 int 里先溢出)。改 c = a/m*b; 后全对。备份 a=m; b=n; 这一步他自己想到了,做对了
40字符加密(凯撒挪 1 位 + 统一小写)环形取模 %26 + 两步变形讲完待交。核心 c = 'a' + (c - 'a' + 1) % 26;必须先把大写转小写再移位(否则 ‘Z’ 变 ‘z’ 没绕回 ‘a’);scanf("%c") 不能加前导空格(空格本身要保留)

规律:逻辑几乎没错过,错误集中在标点、边界、格式。

近期趋势(2026-10-21 晚,热身题 7-4 / 7-5):出现了一类新错误 —— “指错对象”。7-5 四个空全错,但没有一个是”不会 C”:该用循环变量 h 用了人数 w、该加人数 w 加了 1、该跟最大值比跟了前一个桶(还越界)、该写 17 写了 18。语法全对,指称全错。 对策见「六点五」的抓手二(拼音变量名 + 变量角色表)。

近期趋势(第 19~24 题):逻辑零失误,错误只剩语法/格式符 —— 说明循环骨架已掌握。

近期趋势(第 25~29 题):五题里有四题错在”递推”上 —— 不是不懂,是”递推语句的位置/顺序”出错。

近期趋势(第 30~32 题):从”写错”升级到”想错” —— 连续三题出现新骨架(倒推、二阶递推、不枚举)。

近期趋势(第 33~34 题):第 33 题把老大难”if 不加 {}”又犯了一次(break 跑到 if 外面);第 34 题一次通过 —— 说明”边界 + 类型”的意识开始建立。

近期趋势(第 34~35 题):连续两题一次通过。第 35 题两个致命坑(scanf 忘在循环里、printf 写在循环内)全部躲过 —— while 的骨架已经稳了。

近期趋势(第 36 题):连续三题一次通过。% 与 / 配对拆位、“printf 在循环里会累积输出”都是自己想通的。

近期趋势(第 37 题):前三题的”一次通过”停下了 —— 一题连错三轮。但这三轮的错法很有信息量:都不是”不懂解法”,而是「变量的身份搞错」(位数当成位权)、「变量的时机搞错」(备份写在 scanf 前)、「少写一句」(漏 n = n % wei)。这三类正是拆位题的全部雷区,一轮踩完。位权 h 的算法本身(while (n/wei>=10)) 他第一版就写对了。

近期趋势(第 38 题):第一次接触”有名字的算法”(辗转相除法),一次通过。而且这题是三行滚动变量赋值顺序最容易翻车的地方(写反就变 18),他没踩。从”拼循环”跨到”用算法”的第一步走得很稳。

★★ 两题的”最大值”对比:次数知不知道,决定用 for 还是 while#

第 20 题第 35 题
有几个数?题目先给 n不知道,读到负数为止
用哪个循环forwhile
循环条件i < n(数到第 n 个)x >= 0(读到哨兵)
读数据在哪循环体开头循环体末尾(读下一个)
擂主初值先读第一个数先读第一个数(一样)
输出在哪循环外循环外(一样)

哨兵值(结束标志)#

哨兵值 = 一个特殊的数,它不是数据本身,只负责站在队尾喊”到此为止”。

7 6 5 9 2 -1
↑ ↑
真成绩 ────────────── 真成绩 哨兵(不是成绩!)

循环条件问的是”手上这个数还是有效数据吗”(x >= 0),不是”是不是哨兵”(x != -1)。 后者只在哨兵恰好是 -1 时才对 —— 万一哨兵是 -5 就漏了。

为什么不需要专门排除哨兵: 读到 -1 时,while (x >= 0) 立刻不成立,循环体一次都不执行,哨兵根本没机会参加打擂台。

不定项输入的标准摆法(推荐背下来):

scanf("%d", &x); /* 起点:先读一个 */
max = x; /* 让它先当擂主 */
while (x >= 0) { /* 条件:手上这个还是有效数据吗 */
if (x > max) max = x;
scanf("%d", &x); /* 更新:读下一个 ← ★ 最容易忘的一句 */
}
printf("%d\n", max); /* 输出:循环【外面】 */

两种”输入结束”的区别:

结束方式判断写法例子
哨兵值(题面给的一个特殊数)x >= 0 / x != 0第 35 题(负数表示结束)
读到文件结尾(EOF)while (scanf("%d", &x) == 1)题面说”多组数据直到文件结束”

紧凑写法(把 scanf 塞进条件):

int x, max = -1; /* 成绩非负,-1 比任何成绩都小 */
while (scanf("%d", &x) == 1 && x >= 0)
if (x > max) max = x;
printf("%d\n", max);

顺序不能换:scanf(...) == 1 必须在 && 前面(先确认读到了,再看值)。

★ 正序拆位:位权 h 三件套(第 37 题)#

第 36 题是个位优先(逆序),第 37 题是最高位优先(正序)。方向反了,做法就变了。

第 36 题(逆序)第 37 题(正序)
先拿哪一位个位最高位
拿一位n % 10n / h
削一位n = n / 10n = n % h
位权/除数固定 10从最高位开始,每轮 h = h / 10
循环条件n > 0h > 0

位权 = 这一位代表多少个 1。 12345 的位权依次是 1 / 10 / 100 / 1000 / 10000。

第一步:找最高位的位权。

int h = 1;
while (n / h >= 10) /* 问:n/h 还是两位数以上吗?是就再升一级 */
h = h * 10;

⚠️ 必须写 n / h >= 10,不能写 n / h > 0 —— 后者会多乘一次 10,位权多一个 0,输出前面多一个 0。

⚠️ 也别写 h * 10 <= n —— 本题范围(n ≤ 10^9)内能过,但 n 接近 int 上限时 h * 10 会溢出绕圈,最后 h 变成 0 → n / h 除以零,程序直接崩溃(退出码 -1073741676 = 0xC0000094)。能用除法判断就别用乘法。

第二步:从高位到低位逐位吐。

while (h > 0) {
printf("%d ", n / h); /* ① 吐出最高位 */
n = n % h; /* ② ★ 把最高位削掉(漏了就会吐"前缀")*/
h = h / 10; /* ③ 位权降一级 */
}

②和③是一对,缺一不可:

  • 少了 n = n % h → 吐出来的是越来越长的前缀(1 12 123 1234 12345)
  • 少了 h = h / 10 → 死循环,一直吐同一个数字

为什么少了②会变成前缀:n / h 的含义是”把低 h 位砍掉,留下高位部分”。不把刚吐出的那一位削掉,下一轮除数小 10 倍,就多露出前面一位。

记忆法:想「留下」最高位用 /(取商),想「扔掉」最高位用 %(取余)。

⚠️ 拆位会把 n 改掉。 循环结束后 n 已经变成 0,原值丢了。如果后面还要用 n,必须在拆位之前先备份 int m = n; —— 而且这句必须写在 scanf 之后!

实测对照(缺口那一行 vs 补上):

输入漏 n = n % h补上后
123451 12 123 1234 12345 1 2 3 4 5 ✔
9999 99 999 9 9 9 ✔
1001 10 100 1 0 0 ✔
77 7 ✔(一位数碰巧对)

复发错误榜(按次数排序,提交前必查)#

错误次数出现题目
if 不加 {},或 if / else 之间夹了别的语句4第 15 题(printf 插在 if/else if 间)、第 32 题(break 插在 if/else 间)、第 33 题(break 在 if 外,只跑一轮)← 头号大坑
输出格式(大小写、末尾标点、空格 vs 换行、缺 \n)4第 9 题 No/no、第 12 题 yes.、第 32 题 "%d "、第 33 题 printf("Yes") 缺 \n
缺分号 / 多分号 / 分号写成逗号6+第 5、7、21、26、32 题
循环次数差一(n vs n-1,<= vs <)2第 30 题猴子吃桃、第 32 题循环条件方向
漏掉边界情况(题目”注意:“里写的)4第 7 题大小写、第 26 题除零、第 32 题范围、第 33 题 n=1
类型溢出(该用 long long 用了 int)3第 30 题猴子吃桃、第 31 题斐波那契、第 34 题阶乘
“备份/初始化”语句写在了数据到来之前2第 28 题(x = item; 在 scanf 之前)、第 37 题(int m = n; 在 scanf 之前)← 新上榜,专坑拆位/递推题
“位数”与”位权”混淆1第 37 题(wei 当计数器用 wei++,得到 5 而不是 10000)
拆位时漏掉”削位”那一句1第 37 题(漏 n = n % h,吐出前缀 1 12 123 1234 12345)

★ 铁律:题目里出现”注意:…”,就当它是一行必须写的代码#

第 33 题题面写着 “注意:1不是素数”,他代码里没有这一行 → 1000 个测试点错 1 个 → 报错。 第 34 题题面写着 “提示:注意计算过程中的类型溢出问题” → 他按要求用了 long long → 一次通过。

第 34 题题面写着 “提示:注意计算过程中的类型溢出问题” → 他按要求用了 long long → 一次通过。 第 37 题题面写着 “提示:注意整数运算避免使用 double 类型的函数如 pow()“,并且直接把算法给了出来 → 他没写 pow,用整数循环找位权,while (n/wei>=10) 第一版就写对了。

而且第 37 题他是自己主动说出”这次竟然还有提示”的 —— 说明这条铁律已经养成。

题目不会白写废话。凡是”注意""提示""保证""规定”后面的内容,都要落到代码里。

★ 写赋值语句前先问一句:“右边的量,现在有值了吗?”#

同一个错误犯了两次(第 28 题、第 37 题),必须单列出来。

题错的写法后果
第 28 题x = item; 写在 scanf("%d %d", &item, &n); 之前item 还是 0,整题输出 0.00
第 37 题int m = n; 写在 scanf("%d", &n); 之前m 备份到的是 0,0 % wei 恒为 0,输出 0 0 0 0

规则:凡是”把某个变量备份/初始化到另一个变量”,那一行必须写在”数据读进来之后”。

拆位/递推题的三个雷区(第 37 题一轮全踩):

雷区表现
变量的「身份」搞错wei 到底是”位数”(有几个数字)还是”位权”(这一位代表多少)?12345 的位数是 5,位权是 10000
变量的「时机」搞错备份语句写在数据到来之前
「少写一句」n = n % h 漏了 → 吐出前缀;h = h / 10 漏了 → 死循环

递推三连问(每次写 递推 + 累加 都要问)#

  1. 我这一步用的是”上一项”吗?(阶乘漏了 *i、写成 (jiecheng-1)*jiecheng)
  2. 递推语句放在”加”的前面还是后面?配得上变量的初值吗?(第 29 题两行反了)
  3. 变量初值对吗?(fact=1 不是 0;term=0 不是 a;x=item 要等 scanf 之后)

三个必须区分的”递推家族”#

家族式子需要几个变量例题
一阶递推新 = 旧 与 i 运算1 个阶乘、a+aa+aaa、平方根数列
二阶递推新 = 前1项 + 前2项2 个(滚动)斐波那契
逆推由后一项反解前一项,倒着算1 个猴子吃桃

二阶递推的固定写法(滚动变量 / 滑动窗口):

int a = 1, b = 1; /* 窗口最初盖住第1、2项 */
for (i = 1; i < n; i++) { /* 滚 n-1 次 */
int t = a + b; /* 先算新项存到临时变量 */
a = b; /* a 右移 */
b = t; /* b 右移 */
}
printf("%d\n", a);

关键:a = b; b = t; 必须借助临时变量 t,绝不能写成 a = b; b = a + b;(前一项被覆盖)。

逆推题的固定三问:

  1. 起点是什么?(已知的最后一项)
  2. 翻几次?(“已知末项求首项”→ 几乎总是 n-1 次)
  3. 怎么翻?(把题目条件反解成公式)

while 还是 for?—— 看”次数知不知道”#

用哪个什么时候用三零件怎么摆
for次数事先知道(数着数跑:1~n、n 个数、n-1 次)三零件挤在一行:for (i=1; i<=n; i++)
while次数事先不知道(跑到某个条件成立为止)起点写在外面,条件写在括号里,更新写在循环体内

两者完全等价:for (A; B; C) D 就是 A; while (B) { D; C; }。

while 的头号死循环陷阱 —— 忘记写更新语句:

while (k >= m) {
if (k % 4 != 0) { printf("%d\n", k); return; }
/* 忘了 k -= 7; -> k 永远不变,条件永远成立 -> 死循环(OJ 报 TLE)*/
}

规则:更新语句要放在循环体里”每条路径都会经过”的位置,不要藏进 if 里面。

情况二:更新语句不止一条(第 34 题)

while (sum + fact < n) {
sum += fact; /* 少这条 -> sum 不变 -> 死循环 */
m++; /* 少这条 -> m 不变 -> 答案永远是 0 */
fact *= (m + 1); /* 少这条 -> fact 不变 -> 死循环 */
}

三条更新少任何一条,不是死循环就是答案错。 写完对着数一遍:是不是三条都写了?

为什么这种题不用 for:for 的第三格只装得下一个式子,三条得挤成逗号表达式很难读;而且”循环几次”事先不知道,本来就该用 while。

★「先问后加」比「先加后撤」好#

第 34 题的精髓 —— while 的条件只”问”不”改”,是试探:

while (sum + fact < n) { /* 问:加进去还成立吗? 只算,不写回任何变量 */
sum += fact; /* 答"成立"才真的加 */
}

对比”先加后撤”的写法(要多一句 mm-- 回退):

while (1) {
mm++;
f *= mm;
if (s + f >= n) { mm--; break; } /* ★ 得撤销刚才那一步 */
s += f;
}

多一步回退,就多一类可能忘写的地方。能”先问后加”就别”先加后撤”。

类型溢出速查(什么时候必须 long long)#

危险值值说明
int 上限2147483647约 21 亿
12!479001600安全
13!6227020800超 int 上限 → 阶乘表、阶乘不等式必须 long long
S(13)=1!+…+13!6749977113超 int → 阶乘累加必须 long long
F(44)701408733斐波那契安全
F(47)2971215073超 int 上限 → 所以斐波那契题卡在 n≤44
猴子吃桃 n=301610612734安全;但中间过程会达到 3221225470 超界

溢出的表现:结果变成负数,或者”条件永远成立”造成死循环。 判断口诀:题目给了范围,就把范围上限代进去,手算一下最大值。

另一个信号:题目里写”注意计算过程中的类型溢出问题” → 直接上 long long。

★★ 最阴的一种溢出:结果没超,中间过程超了(第 39 题)#

题目说”输入保证最终结果在 int 范围内”—— 这句话只保证结果,不保证过程。

第 39 题:n, m ≤ 1000000,求最大公约数和最小公倍数。

最小公倍数 = n * m / gcd
题目保证【最小公倍数】≤ int 上限 ✔
但没人保证【n * m】不超 —— 它最大能到 1000000 × 1000000 = 10^12
int 上限是 2147483647(约 2.1×10^9)
→ n * m 最大是 int 上限的 466 倍 ●

现场实测(n = m = 1000000):

gcd = 1000000,正确答案 = 1000000 (确实在 int 范围内 ✔)
真实的 n*m = 1000000000000
当成 int 看 = -727379968 ← 已经变成负数了
写法1 n * m / gcd = -727 ★错★
写法2 n / gcd * m = 1000000 ✔

最小的能触发的输入:n = m = 46341 —— 46341 × 46341 = 2147488281,只比 int 上限大 4634,n*m/gcd 就输出 -46340 而不是 46341。

四种写法对照:

写法代码对不对原因
1n * m / gcd★错★int 里先算 n*m → 溢出
2n / gcd * m✔先除后乘,中间值不会超过答案
3(long long)n * m / gcd✔乘法挪到 long long 里做
4(int)((long long)(n * m) / gcd)★错★cast 加晚了 —— n * m 已经在 int 里溢出完了

实测:n,m = 1~2000 全部 400 万组合法输入 —— 写法2 错 0 处、写法3 错 0 处;而在 999000~1000000 区间里挑出的 1487 组合法输入,写法1 和写法4 全错。

★ 通用规律:把”危险的运算”放到最后一步。

场景危险写法安全写法
最小公倍数n * m / gcdn / gcd * m
平均数(a + b) / 2(a+b 可能溢出)a + (b - a) / 2
阶乘比值 n!/(n-k)!先算两个阶乘先约掉,从 n-k+1 连乘到 n
组合数先算分子再算分母边乘边约

“能先约分就先约分” —— 数学上等价的式子,计算顺序不同,安全性完全不同。

⚠️ 还有一种”半途而废”的 long long(写法4):cast 写在最外面,等于什么都没救。要保证乘法本身发生在 long long 里。

“能不能不枚举”—— 范围一大就要先想这句#

看到 “小于 10 的 9 次方 / 10 的 12 次方” 这种规模,先问一句:真的要一个个试吗?

第 32 题的两个新套路:

  • 求”满足条件的最大值” → 不要从头扫,从上界往下退:k = n/7*7; 然后 k -= 7;
  • 求”某数的倍数” → 用 / 和 * 一步跳过去,不用循环:n/7*7 就是”不超过 n 的最大 7 的倍数”

能算出公式/跳着走的,就不要枚举。

★ 第一个算法:辗转相除法(第 38 题)#

这是学到的第一个”有名字的算法”。 以前是”把循环和分支拼出来”,从这里开始要问:“这题有没有一个聪明的办法?”

暴力做法为什么不行(实测循环次数):

输入枚举法跑几轮辗转相除跑几轮
4 641
1000 9999992
701408733 433494437433494437(4.3 亿)42

题目上限 10^9,判题机一秒约跑 1 亿次 → 枚举必然超时。

核心公式:gcd(m, n) = gcd(n, m % n),反复套用到第二个数变成 0,此时第一个数就是答案。

三行骨架(★ 值得背下来):

while (n != 0) {
r = m % n; /* ① 先算余数并【存起来】 */
m = n; /* ② 小的那个上位 */
n = r; /* ③ 余数接着当除数 */
}
printf("%d\n", m); /* n 变 0 时,m 就是答案 */

⚠️ 顺序绝对不能反。 写成 m = n; n = m % n; 就错了 —— 实测 gcd(48,18) 输出 18(m 已被覆盖成 18,18 % 18 = 0,第一轮就结束)。 这和斐波那契 int t = a + b; a = b; b = t; 是同一个「滚动变量」套路:先存临时变量,再移动。

两个不用操心的地方:

  • 不用特判 m < n:实测 4 6 第一轮 4 % 6 = 4,自动就变成 (6, 4) 了。
  • 不会死循环:每轮 m % n 严格小于 n,第二个数一路严格变小,必然碰到 0。

为什么”一直除余数”能得到公约数(一句话版): m % n 的意思就是”把 m 里的 n 一个个减掉,减到减不动为止”。而减去几个 n 不会改变公约数的集合 —— 实测 gcd(48,18) 四步 (48,18)→(18,12)→(12,6)→(6,0),每一步的”公有约数”全都是 1 2 3 6,一个字没变。

更快的口诀记忆:% 就是”更相减损术的快进版”(48→30→12 要减 2 次,48 % 18 = 12 一步到位)。

为什么停在 (m, 0):0 能被任何正整数整除,所以”m 和 0 的公约数”就是”m 的所有约数”,最大的是 m 自己 → gcd(x, 0) = x。

最坏情况是相邻斐波那契数(因为互质)。10^9 以内最大一对 gcd(701408733, 433494437) 要 42 轮。这叫 Lamé 定理:轮数不超过 5 × 位数。

两个前瞻:

  • 递归版(等学函数):int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); }
  • 最小公倍数:m / gcd * n —— 必须先除后乘,因为 m * n 在 10^9 量级是 10^18,int 装不下
  • 扩展欧几里得(以后数论会遇到):能顺便求出 a*x + b*y = gcd(a,b) 的 x, y,用于求模逆元(RSA 加密)

第 38 题实测记录:400 万对穷举(1~2000)+ 200 万对随机大数(上限 10^9)+ 9 万对与”从大到小枚举”对照 + 42 对相邻斐波那契 + 20 万组倍数性质 → 不一致 0 处;gcc -Wall -Wextra 零警告;gcc -std=c89 -pedantic-errors 通过。


★ 变量声明一律写在 main 开头#

第 36 题第 10 行 int yu=0; 写在 scanf 语句之后 —— 这叫”声明与语句混写”,严格 C90 标准不允许:

gcc -std=c89 -pedantic-errors -Wall
→ error: ISO C90 forbids mixed declarations and code

本地 gcc 默认用新标准,只给个警告就过去了;但很多判题机用很老的编译器(VC++ 6.0 或强制 C90 的 gcc),那里这是硬错误,直接判”编译错误”。

改法:把变量全挪到最上面 —— int n = 0, yu = 0;

同理:for (int i = 0; ...) 里的 i 也是 C99 才允许的,老判题机会报错。养成先写 int i; 放最上面的习惯。

★ 命令行坑(不是 C 的问题,但会白耽误时间)#

现象:在 PowerShell 里想把”编译 + 运行”一次搞定,写了 cmd / Linux 风格的 && 或 &:

所在位置 行:1 字符: 2
+ 2& D:\code\1.exe
+ ~
表达式或语句中包含意外的标记“&”。
+ FullyQualifiedErrorId : UnexpectedToken

这不是判题机的报错,是 PowerShell 在抱怨命令本身 —— C 代码一个字都没被执行。

写法PowerShell 的反应
命令1 && 命令2标记“&&”不是此版本中的有效语句分隔符(5.1 不支持 &&,7.0 才有)
命令1 & 命令2不允许使用与号(&)……是为将来使用而保留的(& 是”调用运算符”,不是分隔符)
2& 程序.exe表达式或语句中包含意外的标记“&” ← 就是这个报错

正确写法(二选一):

Terminal window
# 写法 A:分成两行,最省心
gcc -Wall -Wextra -o D:\code\1.exe D:\code\1.c
D:\code\1.exe
# 写法 B:用分号 ; 连成一行(PowerShell 的语句分隔符是 ;)
gcc -Wall -Wextra -o D:\code\1.exe D:\code\1.c ; D:\code\1.exe

想直接把输入喂进去: '12354' | D:\code\1.exe

记三句话:PowerShell 分隔语句用 ;;&& 只在 7.0 以后才有;& 是”调用一个命令”的运算符,不是分隔符。

怎么区分报错来源:带 ParserError / CategoryInfo / FullyQualifiedErrorId 字样的是 PowerShell 在说话;判题机的提示只有一行 文件名:行号: error: ...,或者”编译错误 / 答案错误 / 格式错误 / 运行超时”这类中文短语。


六、下一步重点#

当前:③ 循环结构

  • while 三个零件(起点 / 条件 / 更新)
  • 计数器、累加器模式
  • for 循环(while 的紧凑版)
  • 差一错误(起点 0 配 <,起点 1 配 <=)
  • 打擂台 + 循环
  • 多个计数器(统计正/负/零)
  • 累乘器(初值 1,不是 0)
  • 递推(后一项由前一项算出来:阶乘、平方根数列、a+aa+aaa)
  • 递推语句的位置(先造后加 vs 先加后造 —— 要和初值配套)
  • break(找到一个就停 + 顺便防超时)
  • 标志变量 flag(把”循环里发现的结论”带出循环)
  • 试除法(判素数:i 从 2 开始,i * i <= n 就够)
  • long long 防溢出
  • 哨兵值 / 不定项输入(读到负数为止)
  • % 与 / 配对逐位拆数字(次数不定,一位 0 都不会漏)
  • 变量声明全部集中在 main 开头(兼容老判题机的 C90)
  • 位权 h(正序拆位:先算最高位位权,再 n/h 取位、n%h 削位、h/=10 降权)
  • 辗转相除法(第一个”有名字的算法”:while (n) { r = m%n; m = n; n = r; })
  • continue
  • 循环里嵌套判断
  • 循环的不变式(每轮结束时变量应满足什么关系)

核心模式已齐(十种):分段判断、累加器、计数器、打擂台、累乘器、递推器、逆推(倒着算)、滚动变量(二阶递推)、标志变量 flag、试探式 while(先问后加)

拆位两件套(第 36 / 37 题 —— 一正一反,考的是同一对 % 和 /):

逆序(第 36 题)正序(第 37 题)
骨架while (n > 0) { printf("%d ", n % 10); n = n / 10; }int h=1; while (n/h >= 10) h *= 10;
while (h > 0) { printf("%d ", n/h); n = n % h; h /= 10; }
一句话用 /10 把个位扔掉,扔之前先 %10 看一眼用 /h 留下最高位,再用 %h 把它扔掉
关键除数固定 10必须先花一趟算出 h

递推的统一骨架:

先把手里有的"变一下"(递推),再把变完的"收起来"(累加)
题目递推式
阶乘fact = fact * i
阶乘累加fact = fact * i; → sum += fact
平方根数列x = sqrt(x); → sum += x
a+aa+aaax = x * 10 + a; → sum += x
斐波那契f = a + b; a = b; b = f;
阶乘不等式(第 34 题)fact *= (m + 1); —— 装的是下一项 (m+1)!

下一个里程碑:④ 嵌套循环

  • 打印三角形/菱形
  • 九九乘法表
  • 求最大公约数(辗转相除) ✅ 第 38 题已完成
  • 求最小公倍数 ✅ 第 39 题已完成 → 口诀 n / gcd * m,先除后乘
  • 数组(2026-10-21 晚已开课):下标从 0、int a[100] = {0}; 清零、大数组放全局、桶 cnt[值]++

六点五、★ 被验证过的两件”抓手”(2026-10-21 晚,选拔赛前三天)#

抓手一:四句填空法 —— 专治”看得懂但写不出来”#

“看懂”不等于”会写”。 每拿到一道输入输出题,只按顺序回答四句话,代码是被这四句话”逼”出来的,不是背出来的:

顺序问自己对应代码
①读什么?(几个数?什么类型?读到什么为止?)声明变量 + scanf
②要算出什么?(最终答案存在哪个变量里?)定义”答案变量”
③怎么算出来?(分几步?每步用哪几个变量?)主循环 / 分支
④输出什么?(几个?什么格式?要不要 \n?)printf

四级台阶(判断自己离”独立写出来”还有多远):

台阶给什么能力
1完整代码能看懂 ← 一直停在这里就是无效学习
2填空架能填
3只给注释能写 ← 以后从这一级开始
4只给题目能独立写 ← 考试要的

抓手二:变量名用足拼音#

英文单字母名(w h best sum)在脑子里是纯噪声,看不出”人数""几点""最多”,于是写代码时”指错对象”。

w + x ← 没有语义差别,只是字母
xiaoshi + shicha ← 差的是一个完整单词,一眼分开

三条规则:

  1. 用完整拼音,不缩写 —— renshu ✔,rs ✘
  2. 一个概念一个名字 —— 分钟用 fenzhong 就不要再叫 shi
  3. i / j 保留字母(下标全世界的 C 代码都这么写)

常用角色固定名:计数器 geshu/cishu|累加器 he/zonghe|最大值 zuiduo/zuida|最小值 zuixiao|标志位 biaozhi|临时变量 linshi|数组(桶) tong[]/shuzu[]|长度 changdu

★ 复盘出的核心诊断(比任何语法点都重要)#

7-5 四个空全错,但四个错里没有一个是”不会 C”:

他的写法应该写错误类型
local = w + xdangdi = xiaoshi + shicha指错对象(该用循环变量却用了人数)
sum[h] += 1tong[xiaoshi] += renshu指错对象(该加”人数”却加了”1 次”)
sum[h] > sum[h-1]tong[xiaoshi] > zuiduo指错对象(且 h=0 时 sum[-1] 越界)
local <= 18<= 17边界差一(会议开 1 小时)

结论:短板不是语法,是”精确指称”。 方法:写代码前先列「变量角色表」(每个变量是什么、最容易跟谁混),每写一行先口头念一句”我要用哪个变量”。

★ C 的负数取模(跨时区/环形题必踩)#

-3 % 24 = -3 ← C:符号跟着【被除数】走
-3 % 24 = 21 ← Python:不一样!

环形下标一律写两层:((x % 24) + 24) % 24

  • 第一层 % 24 → 范围 (-24, 24),可能为负
  • + 24 → 范围 (0, 48),一定非负
  • 再 % 24 → 收回 0..23

漏掉两层的后果:实测某基地 100 人、时差 x = -20,漏写时 h-20 恒为负 → 这个基地一次都不被算进去 → 答案从 100 变 0。不崩溃、不警告,只是悄悄少算。

七、经典算法地图(遇到一题就点亮一个)#

从第 38 题开始,题目会越来越多地考”有名字的算法”。看到题目先问一句:这题有没有一个聪明的办法?

算法干什么关键式子 / 骨架状态
辗转相除法最大公约数while (n) { r = m%n; m = n; n = r; }✅ 第 38 题
埃氏筛快速筛出素数表从 2 开始,把每个素数的倍数全部划掉⬜ 待学
试除法判断单个数是不是素数for (i=2; i*i<=n; i++)✅ 第 33 题
冒泡 / 选择 / 插入排序给一堆数排队两层循环 + 交换⬜ 待学(数组之后)
二分查找在有序数组里找数while (lo <= hi) { mid = (lo+hi)/2; ... }⬜ 待学
快速幂算 a^b mod m(b 很大)把指数按二进制拆开,平方递推⬜ 待学
高精度大整数加减乘除用数组存每一位,模拟竖式⬜ 待学
动态规划有重叠子问题的最优化状态 + 转移方程⬜ 待学
扩展欧几里得求 a*x + b*y = gcd(a,b)在辗转相除上回溯⬜ 待学

背模板 + 理解为什么对,两条腿走路。 但背下来不等于写对 —— 第 37 题的教训:算法第一遍就懂了,细节还是错了三轮。每写一次都要问自己”这几行的顺序为什么不能换”。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

C 语言学习笔记
https://yearend.cn/posts/c-语言学习笔记/
作者
YearEnd
发布于
2026-10-17
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录