
1. 从翁恺习题入手为什么这套题值得反复刷很多人学C语言卡在同一个地方课听懂了书翻完了一打开编译器就不知道从哪下手。翁恺老师的C语言课程在MOOC平台上口碑一直很稳配套的编程习题设计得相当扎实——它不是那种输入两个数求和的机械练习而是每一道题都卡在一个具体的知识拐点上逼着你把当周学的东西真正用出来。这套习题最值得刷的原因有三个。第一题目和课程进度严格对齐每周的知识点对应一组题不会出现还没学数组就要你写排序的情况。第二题目描述里埋了很多边界条件的暗示比如输入可能包含空格数字可能为负数这些细节在真实开发中就是bug的高发区。第三翁恺的题量控制得很好一周五六道认真做下来两三个小时不会让人产生刷不完的焦虑。我前后把这套题刷了两遍。第一遍是跟着课程走做完就扔第二遍是隔了半年回头重做发现自己当初很多题其实是蒙对的——循环边界靠试、数组越界靠运气。第二遍才真正把每道题为什么这么写想清楚了。所以这篇内容不是简单贴答案而是把每类题背后的思路、容易踩的坑、以及我自己的解法整理出来适合正在跟翁恺课程的朋友也适合任何想用这套题检验C语言基础的人。2. 分支与循环看似简单边界才是真正的考点2.1 判断语句里最容易被忽略的等于陷阱翁恺习题里有一类题专门考条件判断比如判断闰年、判断成绩等级、比较三个数大小。这些题逻辑本身不难但新手写出来的代码经常在边界值上翻车。最典型的是闰年判断。规则是能被4整除但不能被100整除或者能被400整除。很多人第一版写成这样if (year % 4 0 year % 100 ! 0 || year % 400 0)这行代码逻辑上是对的因为优先级高于||。但我见过太多人写成if (year % 4 0 year % 100 ! 0 || year % 400 0)然后自己心里没底不知道到底先算哪个。我的建议是永远加括号不是为了编译器是为了三个月后的自己if ((year % 4 0 year % 100 ! 0) || (year % 400 0))另一个高频错误是把写成。if (a 5)在C语言里是合法的它把5赋给a然后判断5是否为真永远成立。编译器一般会给警告但很多人不看警告直接跑结果逻辑全错还找不到原因。我的习惯是把常量写在左边if (5 a)这样万一漏写一个等号编译器直接报错因为5不能被赋值。2.2 循环的三种写法与选择逻辑翁恺的循环题覆盖了for、while、do-while三种结构。很多人习惯所有循环都用for但其实这三种各有适用场景。for适合循环次数已知的情况比如求1到100的和输出前n个斐波那契数。它的三个表达式把初始化、条件、更新集中在一行读起来清晰。while适合循环次数未知、依赖某个条件退出的情况。比如不断读入数字直到输入-1为止这时候用while (scanf(%d, n) n ! -1)比for自然得多。do-while最少用但有一类场景非它不可循环体至少执行一次。比如输入一个正整数如果输入不合法就重新输入这时候必须先读一次才能判断用do-while最合适int n; do { printf(请输入正整数); scanf(%d, n); } while (n 0);我当初做这类题时最大的收获不是学会了三种循环而是理解了选择哪种循环本身就是设计的一部分。代码可读性很大程度上取决于你选的工具对不对。2.3 循环嵌套里的变量作用域问题有一道题是输出乘法口诀表还有一道是输出指定行数的金字塔。这两道题都涉及循环嵌套而新手最容易犯的错误是内层循环和外层循环用了同一个变量int i, j; for (i 1; i 9; i) { for (i 1; i i; i) { // 错误内层也用了i printf(%d*%d%d , i, j, i*j); } }这段代码的问题在于内层循环把i重置了外层循环直接失控。正确写法是内层用jfor (int i 1; i 9; i) { for (int j 1; j i; j) { printf(%d*%d%-2d , j, i, i*j); } printf(\n); }这里顺便说一个实用技巧%-2d里的-表示左对齐2表示至少占两个字符宽度。乘法口诀表里结果有一位数也有两位数不加宽度控制输出会参差不齐。这种格式化细节在翁恺的题里经常出现值得专门记一下。3. 数组与字符串从能跑到跑对的分水岭3.1 数组越界为什么不一定报错C语言最坑的地方之一就是数组越界不一定会崩溃。你定义一个int a[10]访问a[10]甚至a[100]程序可能照常运行也可能突然输出一堆乱码。这不是编译器的问题是C语言的设计哲学——它信任程序员不做运行时检查。翁恺有一道题是输入n个数逆序输出。很多人写成int n, a[n]; scanf(%d, n);这在C99之后是合法的变长数组但很多在线评测系统用的是老标准直接编译报错。更稳妥的写法是定义一个足够大的数组#define MAXN 1000 int a[MAXN]; int n; scanf(%d, n); for (int i 0; i n; i) { scanf(%d, a[i]); } for (int i n - 1; i 0; i--) { printf(%d , a[i]); }这里的关键是循环条件用i n而不是i n。差一个等号就是越界和不越界的区别。我自己的习惯是写完数组循环后专门检查一遍所有下标的上界这个动作花不了十秒钟但能省掉大量调试时间。3.2 字符串处理fgets和scanf的分工翁恺的字符串题里有一类专门考输入方式。scanf(%s, str)遇到空格就停所以如果输入是hello world它只会读到hello。要读整行得用fgetschar str[100]; fgets(str, sizeof(str), stdin);但fgets会把换行符也读进去所以经常需要手动去掉str[strcspn(str, \n)] \0;strcspn返回的是字符串中第一个匹配字符的位置这里找的是换行符。这行代码比手动循环找\n简洁得多建议记住。还有一道经典题是统计字符串中单词个数。思路是遍历字符串遇到非空格且前一个字符是空格或字符串开头时计数加一。这道题考的是状态判断代码不长但逻辑要清晰int count 0; int in_word 0; for (int i 0; str[i] ! \0; i) { if (str[i] ! !in_word) { count; in_word 1; } else if (str[i] ) { in_word 0; } }这种状态标记的写法在字符串处理里非常常见值得当成一个模式记下来。3.3 二维数组的行列陷阱二维数组的题里矩阵转置和矩阵乘法是常客。转置的逻辑是b[j][i] a[i][j]看起来简单但循环边界容易写错for (int i 0; i row; i) { for (int j 0; j col; j) { b[j][i] a[i][j]; } }注意转置后b的行数是原来的列数列数是原来的行数。如果后面还要遍历b循环边界得跟着变。我见过有人转置完还用原来的row和col去遍历结果要么少输出要么越界。矩阵乘法更要注意c[i][j] sum(a[i][k] * b[k][j])其中k的范围是第一个矩阵的列数也是第二个矩阵的行数。这三个循环的顺序可以优化但在初学阶段先把逻辑写对最重要。4. 函数与指针C语言真正的门槛4.1 函数参数传递值传递的本质翁恺有一道题是写一个函数交换两个变量的值这道题是理解C语言参数传递的经典案例。很多人第一版写成void swap(int a, int b) { int t a; a b; b t; }调用swap(x, y)之后发现x和y根本没变。原因是C语言只有值传递函数里的a和b是x和y的副本改副本不影响原件。正确写法是传指针void swap(int *a, int *b) { int t *a; *a *b; *b t; } // 调用swap(x, y);这道题的价值不在于学会交换而在于理解函数内部改不了外部变量这个事实。很多新手在这里卡很久就是因为没想通副本这个概念。4.2 指针与数组的关系为什么a[i]等于*(ai)指针和数组的关系是C语言里最容易被讲复杂的地方。其实核心就一句话数组名在大多数情况下会退化成指向首元素的指针。所以a[i]本质上就是*(a i)——从a指向的地址开始往后移动i个元素然后取值。理解这一点之后很多写法就通了int a[5] {1, 2, 3, 4, 5}; int *p a; // p指向a[0] printf(%d, *p); // 输出1 printf(%d, *(p1)); // 输出2 printf(%d, p[1]); // 输出2和上面等价翁恺的题里有一道用指针遍历数组并求和就是考这个。写的时候注意p和(*p)的区别前者移动指针后者增加指针指向的值。这两个操作符优先级不同*p等价于*(p)先取值再移动指针。4.3 指针作为函数参数的常见模式除了交换指针作为参数还有几种常见模式。一种是用指针返回多个值void min_max(int a[], int n, int *min, int *max) { *min *max a[0]; for (int i 1; i n; i) { if (a[i] *min) *min a[i]; if (a[i] *max) *max a[i]; } }另一种是传递数组时同时传长度因为函数内部拿不到数组的实际大小int sum(int a[], int n) { int s 0; for (int i 0; i n; i) s a[i]; return s; }这里int a[]和int *a是完全等价的写哪个都行。我一般写int a[]因为更直观地表明这个参数是个数组。5. 结构体与文件从练习题到真实项目的过渡5.1 结构体的定义与初始化翁恺的结构体题通常围绕学生成绩管理展开比如定义学生结构体、按成绩排序、查找最高分。结构体的定义要注意分号struct Student { char name[20]; int score; }; // 这个分号不能少初始化有几种写法struct Student s1 {张三, 90}; struct Student s2 {.name 李四, .score 85}; struct Student s3; s3.score 88; strcpy(s3.name, 王五);第二种指定初始化器是C99的特性可读性最好推荐使用。第三种要注意字符串不能直接赋值必须用strcpy。5.2 结构体数组的排序按成绩排序结构体数组本质上是排序算法的应用只是交换的元素变成了结构体for (int i 0; i n - 1; i) { for (int j 0; j n - 1 - i; j) { if (students[j].score students[j1].score) { struct Student t students[j]; students[j] students[j1]; students[j1] t; } } }结构体可以直接赋值因为C语言允许结构体整体拷贝。这一点比数组方便——数组不能直接赋值必须用memcpy或循环。5.3 文件读写的基本流程文件操作是翁恺课程后期的内容题目通常是从文件读数据处理后写回文件。基本流程是三步打开、操作、关闭。FILE *fp fopen(data.txt, r); if (fp NULL) { printf(打开文件失败\n); return 1; } int n; fscanf(fp, %d, n); // ... 处理 fclose(fp);写文件用w模式追加用a模式。注意fopen之后一定要检查返回值文件不存在或没有权限时fp是NULL直接用它会导致程序崩溃。还有一个容易忽略的点fscanf和scanf一样遇到格式不匹配会停止读取。如果文件格式不规整最好用fgets逐行读再用sscanf解析。6. 刷题之外几个让我少走弯路的习惯6.1 先写伪代码再写C我刚开始刷题时习惯直接敲代码边写边想结果经常写到一半发现思路不对删掉重来。后来改成先在纸上或注释里写伪代码// 1. 读入n // 2. 读入n个数到数组 // 3. 遍历数组找最大值 // 4. 输出最大值伪代码写清楚了翻译成C就是几分钟的事。这个习惯看起来慢实际上快得多尤其是逻辑复杂的题。6.2 用printf调试而不是靠猜新手遇到结果不对第一反应是盯着代码看。但人眼很容易忽略细节尤其是循环边界和条件判断。我的做法是在关键位置加printfprintf(i%d, j%d, a[i]%d\n, i, j, a[i]);把中间状态打出来一眼就能看出哪里不对。调试完之后把这些printf删掉或注释掉。这个习惯在刷题阶段特别有用因为在线评测系统不会告诉你哪里错了只能自己定位。6.3 每道题做完后想三个问题这道题考的是什么知识点我的解法有没有更简洁的写法如果输入规模变大十倍我的代码还能跑吗第三个问题尤其重要。很多题目的测试数据很小用冒泡排序和快速排序都能过但真实场景下数据量一大O(n²)的算法就撑不住了。刷题阶段就要有意识地区分能过和好。6.4 整理自己的错题本我建了一个Markdown文件每道做错的题记录三样东西题目编号、错误原因、正确思路。比如数组越界循环条件写成in字符串输入忘了fgets会读换行符。隔一段时间翻一遍比重新刷一遍题效率高得多。7. 关于答案的使用建议网上流传的翁恺习题答案质量参差不齐有些甚至是错的。我的建议是答案只用来对照思路不要直接抄。具体做法是自己先写一版能过评测就过过不了再看答案重点看它为什么这么写而不是它写了什么。如果一道题想了半小时还没思路可以看答案但看完之后要合上答案自己重写一遍。这个重写的动作很关键它把别人的思路转化成你自己的肌肉记忆。另外翁恺的题很多都有多种解法。比如字符串逆序可以用数组、可以用指针、可以原地交换、可以新建数组。每种解法都值得试一遍这样你才能真正理解C语言不同特性之间的取舍。最后说一个我自己的体会这套题的价值不在于做完而在于做透。我第一遍刷完花了大概三周第二遍只花了十天但第二遍的收获比第一遍大得多。因为第一遍是在完成任务第二遍是在理解设计。如果你时间有限宁可少做几道也要把每道题背后的知识点吃透。