翁恺C语言习题刷题指南:从运算符到指针的完整避坑手册
1. 为什么我要把翁恺C语言习题从头到尾刷一遍很多人学C语言的状态是这样的视频看完了感觉都懂一上手写代码就卡壳。指针、数组、函数传参这些概念看老师讲的时候明明白白轮到自己写就不知道从哪下手。我当初也是这个状态后来发现问题的根源特别简单——看懂了不等于会写会写不等于写对。翁恺老师的C语言课程在入门阶段口碑一直很稳但光看视频不做题跟看别人游泳自己不下水是一个道理。这套习题汇总就是我在刷完翁恺老师课程配套练习后把自己做过的题目、踩过的坑、以及每道题背后的知识点重新梳理出来的东西。它适合两类人一类是刚学完C语言基础语法、想找一套成体系的题目来练手的新手另一类是学过一遍但觉得自己基础不牢、想回头把运算符、循环、数组、指针、结构体这些核心模块重新夯实的人。题目本身不偏不怪都是围绕C语言最核心的语法点设计的但恰恰是这些基础题最能暴露你对语言细节的理解程度。我整理的时候刻意保留了每道题的原始题目描述、我的解题思路、完整代码、以及运行结果分析而不是只丢一个答案上去。因为做题这件事答案本身价值有限真正值钱的是“为什么这么写”和“我当时为什么写错了”。下面我会按知识模块拆开讲每个模块挑几道有代表性的题把思路和坑点说透。2. 运算符与表达式最容易翻车的基础模块2.1 自增自减的求值顺序到底怎么算运算符这块翁恺老师的习题里有一类题特别经典给一段包含i、i、i--混合运算的表达式让你写出最终结果。比如这种int i 5; int a i i; printf(%d %d, a, i);很多人第一反应是5 7 12但实际结果依赖编译器的求值顺序。在C语言标准里这种表达式中对i的多次修改之间没有序列点属于未定义行为。也就是说不同编译器可能给出不同结果甚至同一编译器不同优化级别下结果都不一样。我实测过在GCC默认优化下这段代码输出12 7但在-O2优化下可能输出13 7。所以这类题的正确态度不是背答案而是认识到这是未定义行为实际写代码时绝对不要这么写。翁恺老师在课上专门强调过这一点习题里出这类题的目的是让你理解“副作用”和“序列点”的概念而不是让你去猜某个编译器的实现。注意考试里如果出现这种题按你所用编译器的实际行为作答即可但工作中遇到这种代码直接打回去重写。2.2 整数除法与浮点除法的隐式转换陷阱另一道高频错题是这样的int a 5, b 2; float c a / b; printf(%f, c);输出是2.000000而不是2.500000。原因在于a / b是两个整数相除结果先被截断为整数2然后才赋值给float类型的c。正确写法是float c (float)a / b;或者float c a / 2.0;。这个坑我在批改别人作业时见过无数次。很多人明明知道整数除法会截断但一到实际写代码就忘。我的建议是只要涉及除法先问自己一句“这里需不需要小数”需要的话立刻做强制类型转换不要等结果不对了再回头找。2.3 位运算的常见考法位运算在入门阶段考得不多但翁恺老师的习题里会涉及几个典型场景判断奇偶、交换两个数、取某一位的值。比如不用临时变量交换两个整数a a ^ b; b a ^ b; a a ^ b;这个写法的原理是异或运算的自反性x ^ x 0x ^ 0 x。推导过程是第一步后a变成a ^ b第二步b (a ^ b) ^ b a第三步a (a ^ b) ^ a b。逻辑很漂亮但实际工程中我不建议这么写因为可读性差而且如果a和b是同一个变量比如数组里a[i]和a[j]且i j结果会直接变成0。老老实实用临时变量编译器优化后性能没差别。3. 循环与流程控制从会写到写对的距离3.1 循环边界条件的三种典型错误循环是C语言入门的分水岭。翁恺老师的习题里循环相关的题目占比很大从简单的累加求和到打印图形、判断素数、分解质因数都有。我总结下来新手在循环上翻车基本逃不出这三种情况第一种是边界多一次或少一次。比如求1到100的和写成for (i 1; i 100; i)结果少了100。这种错误靠肉眼检查很难发现我的习惯是写完循环后立刻在纸上模拟前两次和最后一次迭代确认边界正确。第二种是循环变量在循环体内被意外修改。比如在for循环里又写了一个内层循环也用了i导致外层循环失控。这种问题编译器不会报错但运行结果完全不对。解决办法是内层循环用j、k并且尽量把循环变量声明在for语句内部C99及以上支持。第三种是死循环。常见于while循环忘记更新条件变量或者for循环的更新表达式写反了方向。比如for (i 0; i 10; i--)这个循环永远不会结束。调试死循环最直接的方法是在循环体里加一句printf打印循环变量跑几秒就能看出问题。3.2 打印图形类题目的通用解法翁恺习题里有一类打印图形的题比如打印金字塔、菱形、九九乘法表。这类题看起来花哨其实套路非常固定外层循环控制行数内层循环控制每行的空格数和星号数。以打印n行金字塔为例for (int i 1; i n; i) { for (int j 1; j n - i; j) printf( ); for (int k 1; k 2 * i - 1; k) printf(*); printf(\n); }关键就三个公式第i行的空格数是n - i星号数是2 * i - 1每行结束后换行。把这三个公式推出来所有类似的图形题都能套。我当初做这类题的时候习惯先在纸上画出n4时的图形标出每行的空格和星号数量然后找规律。这个习惯帮我省了很多试错时间。3.3 素数判断与质因数分解的优化思路判断素数这道题翁恺老师的习题里要求写一个函数判断一个数是否为素数。最朴素的写法是从2循环到n-1看有没有能整除的。但这样效率很低稍微优化一下就能快很多int isPrime(int n) { if (n 2) return 0; if (n 2) return 1; if (n % 2 0) return 0; for (int i 3; i * i n; i 2) { if (n % i 0) return 0; } return 1; }这里做了三层优化排除小于2的数、单独处理2、只检查奇数且只检查到sqrt(n)。为什么是sqrt(n)因为如果n有一个大于sqrt(n)的因子那必然对应一个小于sqrt(n)的因子所以检查到sqrt(n)就够了。这个优化思路在质因数分解题里同样适用。4. 数组与字符串内存布局决定一切4.1 数组越界为什么这么危险数组是C语言里最容易出安全问题的地方。翁恺老师的习题里有一道题是“输入10个数逆序输出”很多人写成int a[10]; for (int i 1; i 10; i) scanf(%d, a[i]);这里a[10]已经越界了因为合法下标是0到9。越界写会覆盖数组后面的内存可能改掉其他变量的值也可能直接导致程序崩溃。更麻烦的是越界读可能不报错但读到垃圾值让你误以为程序是对的。我的习惯是定义数组时把大小写成宏或者常量循环条件用 N而不是 N并且每次访问数组前在脑子里过一遍“这个下标最大能到多少”。4.2 字符串处理fgets与scanf的区别字符串这块翁恺习题里经常要求读入一行包含空格的字符串。用scanf(%s, buf)遇到空格就停了读不全。正确做法是用fgetschar buf[100]; fgets(buf, sizeof(buf), stdin);fgets会把换行符也读进去所以通常需要手动去掉末尾的\nbuf[strcspn(buf, \n)] \0;strcspn返回的是字符串中第一个匹配字符的位置这里找\n的位置并替换成\0。这个写法比用strlen判断末尾再截断更安全因为即使没有换行符也不会越界。注意fgets在读取失败时返回NULL实际代码里应该检查返回值尤其是从文件读取时。4.3 冒泡排序的手写要点冒泡排序是翁恺习题里的常客。虽然实际工作中直接用qsort但手写冒泡是理解循环嵌套和数组操作的好练习。标准写法for (int i 0; i n - 1; i) { for (int j 0; j n - 1 - i; j) { if (a[j] a[j 1]) { int t a[j]; a[j] a[j 1]; a[j 1] t; } } }内层循环的边界是n - 1 - i因为每轮结束后最大的元素已经沉到末尾了不需要再比较。这个- i是优化点不写也能排对但多做了无用比较。我见过有人写成j n - 1结果也能跑但效率低。另外交换两个元素时临时变量的类型要和数组元素类型一致别用int去交换double数组。5. 函数与指针C语言真正的门槛5.1 值传递与地址传递的本质区别函数传参是C语言入门最大的坎。翁恺老师的习题里有一道经典题写一个函数交换两个整数的值。很多人写成void swap(int a, int b) { int t a; a b; b t; }调用后发现没交换。原因是C语言只有值传递函数内的a和b是实参的副本改副本不影响原件。正确写法是传指针void swap(int *a, int *b) { int t *a; *a *b; *b t; }调用时写swap(x, y)。理解这个问题的关键是画内存图实参x和y在调用者的栈帧里形参a和b在被调用者的栈帧里它们地址不同。传指针后形参a和b虽然还是副本但副本里存的是x和y的地址通过*a就能间接修改x的值。5.2 指针与数组的关系到底怎么理解“数组名就是首元素地址”这句话很多人会背但一到用的时候就糊涂。翁恺习题里有一道题用指针遍历数组并求和。写法int sum(int *arr, int n) { int s 0; for (int i 0; i n; i) { s *(arr i); } return s; }arr i是指针算术移动i个元素的位置不是i个字节。如果arr是int*arr 1实际地址增加sizeof(int)个字节。理解这一点就能明白为什么arr[i]等价于*(arr i)也能明白为什么二维数组传参必须指定第二维的长度。5.3 函数返回指针的陷阱有一道题要求写一个函数返回数组中的最大值所在的地址。有人写成int *findMax(int *arr, int n) { int max arr[0]; int *p max; for (int i 1; i n; i) { if (arr[i] max) { max arr[i]; p arr[i]; } } return p; }这里p初始指向局部变量max函数返回后max的内存已经失效返回的指针是悬空指针。正确做法是让p初始指向arr[0]的地址或者返回下标而不是指针。这个坑我在实际项目中见过返回局部变量地址的bug往往在函数返回后一段时间才暴露排查起来很费劲。6. 结构体与文件操作从练习题到实际项目6.1 结构体作为函数参数的三种方式翁恺习题里有一道题定义一个学生结构体包含姓名、学号、成绩写函数按成绩排序。结构体传参有三种方式传值、传指针、传数组。传值会复制整个结构体如果结构体很大开销明显传指针只复制一个地址效率高但函数内可以修改原结构体传数组本质上也是传指针。我一般这样选只读访问用const指针需要修改用普通指针结构体很小比如只有两个int且不需要修改时可以用传值。排序函数需要交换结构体所以传指针数组或者直接操作结构体数组。6.2 文件读写的完整流程文件操作是翁恺课程后期内容习题里通常要求读一个文本文件统计行数或单词数。完整流程是FILE *fp fopen(input.txt, r); if (fp NULL) { perror(fopen); return 1; } char line[256]; int count 0; while (fgets(line, sizeof(line), fp) ! NULL) { count; } fclose(fp); printf(Lines: %d\n, count);几个关键点fopen后必须检查返回值fgets读取时如果一行超过缓冲区大小会分多次读取统计行数时要注意fclose不能忘否则数据可能没写回磁盘。我当初做这道题时忘了检查fopen返回值结果文件不存在时程序直接崩溃调试了半天才发现。6.3 综合项目通讯录管理翁恺课程最后有一个综合练习用结构体数组实现一个简单的通讯录支持添加、删除、查找、显示。这个项目把前面所有知识点串起来了结构体定义、数组操作、字符串处理、循环与分支、函数封装。我建议每个学完C语言基础的人都动手写一遍不用写图形界面命令行版本就够。写的时候注意几个工程习惯把结构体定义和函数声明放在头文件里函数实现放在.c文件里主函数只负责调度。这样代码结构清晰也方便后续扩展。删除联系人时我用的方法是把最后一个元素移到被删除位置然后数组长度减一这样避免大量元素移动。7. 常见编译错误与调试技巧速查7.1 编译错误对照表错误信息常见原因解决方法expected ; before }上一条语句漏分号检查前一行末尾implicit declaration of function函数未声明就使用在文件开头加函数声明或包含头文件assignment makes pointer from integer指针赋值类型不匹配检查是否漏了或*undefined reference to xxx链接时找不到函数定义检查函数名拼写、是否编译了所有源文件segmentation fault空指针解引用或数组越界用gdb定位崩溃行检查指针和下标7.2 调试心得printf大法好新手最容易上手的调试方法就是printf。在关键位置打印变量值看程序执行到哪一步、变量的值是否符合预期。我到现在写复杂逻辑时还会用这招比单步调试快。几个技巧打印时加上标签比如printf(i%d, sum%d\n, i, sum)不然一堆数字分不清谁是谁在循环里打印时限制次数比如if (i 5) printf(...)避免刷屏调试完后记得删掉或注释掉调试输出。7.3 常见运行时错误排查段错误是最常见的运行时错误。排查步骤先用gcc -g编译带调试信息然后用gdb ./a.out运行崩溃后输入bt看调用栈定位到具体行。如果没有gdb可以在可疑位置加printf缩小范围。另一个常见问题是死循环表现为程序卡住不输出。这时候在循环体里加printf打印循环变量跑几秒就能看出是哪个循环出了问题。8. 刷完这套题之后我的真实感受这套习题我前后刷了两遍。第一遍是跟着课程进度做做完就扔第二遍是隔了半年回头重做发现自己第一遍很多题其实是“背答案”而不是真会。第二遍做的时候我强迫自己每道题先不看答案在纸上写出思路和代码再上机验证。这个过程很慢但效果扎实。最大的收获不是学会了多少语法而是养成了几个习惯写循环先想边界用指针先画内存图函数传参先问自己“要不要改原件”数组操作先确认下标范围。这些习惯让我在后面学数据结构和做项目时少踩了很多坑。如果你也在刷翁恺老师的C语言习题我的建议是不要追求刷题数量把每道错题搞透比刷十道新题有用。错题要记录错误原因是概念不清还是粗心分类整理。过一周再重做一遍错题如果还能做对才算真正掌握。C语言基础打牢了后面学什么语言都快因为指针、内存、类型这些概念是相通的。