☰
翁恺C语言习题深度解析:从分支循环到指针结构体的刷题指南
2026/9/25 8:15:29 网站建设 项目流程

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重置了,外层循环直接失控。正确写法是内层用j:

for (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"。要读整行,得用fgets:

char 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]等于*(a+i)

指针和数组的关系是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", *(p+1)); // 输出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[j+1].score) { struct Student t = students[j]; students[j] = students[j+1]; students[j+1] = 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调试而不是靠猜

新手遇到结果不对,第一反应是盯着代码看。但人眼很容易忽略细节,尤其是循环边界和条件判断。我的做法是在关键位置加printf:

printf("i=%d, j=%d, a[i]=%d\n", i, j, a[i]);

把中间状态打出来,一眼就能看出哪里不对。调试完之后把这些printf删掉或注释掉。这个习惯在刷题阶段特别有用,因为在线评测系统不会告诉你哪里错了,只能自己定位。

6.3 每道题做完后想三个问题

这道题考的是什么知识点?我的解法有没有更简洁的写法?如果输入规模变大十倍,我的代码还能跑吗?

第三个问题尤其重要。很多题目的测试数据很小,用冒泡排序和快速排序都能过,但真实场景下数据量一大,O(n²)的算法就撑不住了。刷题阶段就要有意识地区分"能过"和"好"。

6.4 整理自己的错题本

我建了一个Markdown文件,每道做错的题记录三样东西:题目编号、错误原因、正确思路。比如"数组越界:循环条件写成i<=n""字符串输入:忘了fgets会读换行符"。隔一段时间翻一遍,比重新刷一遍题效率高得多。

7. 关于答案的使用建议

网上流传的翁恺习题答案质量参差不齐,有些甚至是错的。我的建议是:答案只用来对照思路,不要直接抄。具体做法是,自己先写一版,能过评测就过;过不了再看答案,重点看"它为什么这么写"而不是"它写了什么"。

如果一道题想了半小时还没思路,可以看答案,但看完之后要合上答案自己重写一遍。这个"重写"的动作很关键,它把别人的思路转化成你自己的肌肉记忆。

另外,翁恺的题很多都有多种解法。比如字符串逆序,可以用数组、可以用指针、可以原地交换、可以新建数组。每种解法都值得试一遍,这样你才能真正理解C语言不同特性之间的取舍。

最后说一个我自己的体会:这套题的价值不在于做完,而在于做透。我第一遍刷完花了大概三周,第二遍只花了十天,但第二遍的收获比第一遍大得多。因为第一遍是在"完成任务",第二遍是在"理解设计"。如果你时间有限,宁可少做几道,也要把每道题背后的知识点吃透。

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

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

立即咨询