资讯详情

资讯详情

翁恺C语言习题集刷题指南:环境搭建、题型拆解与避坑要点

简介浙江大学翁恺教授C语言课程习题与源码合集专门面向C语言初学者、备考计算机等级考试或自学编程的读者覆盖从基础语法到典型算法的多层次练习。资源共4个文件包含txt题解文档、Python辅助脚本、inscode在线交互文件和html页面压缩包整体仅9KB轻量便于直接阅读和运行。目前已有1784人学习。题目涵盖整数运算、时间计算、数字逆序、BCD解密、水仙花数、九九乘法表、素数统计、猜数字游戏、分数约分、数字拼音转换、连续和计算、混合类型数据处理、简单计算器、字符串大小写转换、单词长度统计、矩阵局部极大值与组个最小数等内容每个题目均附有输入输出格式说明和完整C语言代码既能巩固循环、判断和数组等核心知识也能训练数据转换与简单算法设计。读者可据此边练边查快速定位薄弱环节并模仿规范编码适合自主刷题或配合课堂教学使用。1. 一套把自学者劝退又拉回来的习题集翁恺C语言习题集到底在考什么翁恺C语言习题集是很多自学C语言的人绕不开的一道坎视频课看得懂一打开习题集就发现自己其实不会写代码。这套题从分支循环一路做到指针、链表和动态内存题量不小评分点细到让人怀疑人生——字符串结尾的\0有没有写malloc之后有没有配对free不同编译器下会不会崩这些都是它真正在考的东西。它的价值不在“题多”而在每一道题都逼你把代码写到被验证过的细节层面。适合刷它的人有两类一类是跟着网课看完语法、却几乎没有独立写过完整程序的自学者另一类是准备复试或转行考试、需要用一套成体系的题目把自己拉回实战状态的从业者。很多人误以为“刷完视频就等于学会”真实情况是能把这套习题集从头到尾独立写完、且每一道都能在不看答案的情况下重写一遍这才算入门。理解这一点后面所有的练习方法和踩坑才有意义。2. 刷题前的环境准备用本地编译环境把这套习题跑起来刷翁恺C语言习题集第一件事不是打开题目而是把手边的运行环境理顺。很多初学者图省事直接用网页版在线运行器做题前面几道分支循环还能应付一到指针、链表、动态内存就开始翻车。原因很简单在线运行器是一个隔离的黑匣子你只能看到输出结果看不到变量在内存里的变化也没有断点调试能力出了问题全靠猜。而指针题恰恰就是“靠猜必然猜错”的题型。这一章的目标很明确在本地装好一套能编译、能断点、能看内存的C语言开发环境并把第一道习题完整跑通。环境就绪之后后面刷题才谈得上效率。2.1 为什么别用在线网页运行器刷指针题内存视图的差异在线运行器适合验证一段语法上不确定的小片段不适合作为刷整套习题的主力环境。它每次运行都是一个独立容器程序结束后所有状态清零你无法回答“这个指针到底指向哪里”“这块内存释放了没有”这类问题。而指针和动态内存恰恰是翁恺C语言习题集后半段的重点刷这部分题的时候观察内存状态几乎等于解题本身。本地编译环境则完全不同编译器会把源码变成真实的可执行文件调试器可以下断点、看变量地址、查看堆内存内容甚至能一步步观察链表节点是怎么连起来的。这些能力对理解指针语义是不可替代的。另一个实际问题是输入方式很多在线运行器对多行输入、EOF结束、特殊字符的处理和本地终端不一致习题里有一类题要求读到文件尾才结束在线环境里这类题很容易出现“本地能跑提交就错”的怪现象。2.2 本地编译环境的搭建Windows、Linux与macOS三套配置在不同操作系统上选择会稍有差异。Windows 上常见的是 MinGW-w64 配 VSCodeLinux 和 macOS 直接使用系统自带或 brew 安装的 gcc/clang 即可。这里有一个容易踩的坑Windows 下装完 MinGW 之后PATH 没配置或者配错终端里敲gcc提示“不是内部或外部命令”很多人卡在这一步就放弃了。建议装完之后立刻开一个新终端验证。操作系统编译器推荐获取方式验证命令WindowsMinGW-w64 (gcc)从 MinGW-w64 项目页下载安装gcc --versionLinuxgcc包管理器安装如 aptgcc --versionmacOSclang/gccXcode Command Line Tools 或 Homebrewgcc --version安装完成后打开终端验证编译器是否可用gcc --version如果正常输出版本信息说明编译工具链已经就位。如果提示找不到命令定位到编译器安装目录把其中的bin目录加到系统的 PATH 环境变量里再开一个新终端重试。之后在 VSCode 里安装 C/C 扩展它提供语法高亮、智能提示和调试器集成刷题时鼠标悬停就能看到变量值这对排查指针问题帮助很大。这里要特别提醒编译器装好后不要急着去折腾 VSCode 的launch.json和tasks.json。第一次先用命令行手工编译理解“源文件 - 编译 - 可执行文件”这个过程再去配置编辑器顺序反了会把自己绕晕。2.3 用最小程序验证工具链从写代码到看到输出环境配置完成后用一个最小程序把整条链路打通。新建一个目录专门放习题源码把下面这段保存为hello.c然后编译运行#include stdio.h int main(void) { printf(toolchain ok\n); return 0; }在终端里执行编译gcc -Wall -Wextra -o hello hello.c ./hello-Wall和-Wextra是两件值得养成习惯的事前者开启常见警告后者开启更多不太常见的警告。刚刷题的人往往只关注“能不能跑”完全不看警告结果代码是运行了但里面有未初始化的变量、类型不匹配、函数没有声明等问题等提交后测试用例一换就崩。运行-Wall -Wextra编译时编译器会把这些问题提前翻出来相当于随身带了一个基础代码审查员。-o hello指定输出的可执行文件名不写的话默认生成a.outWindows 下是a.exe文件一多就会乱。把这一步当成刷题标配每次编译都开警告每次生成可执行文件都指定名字。看到终端输出toolchain ok环境就绪接下来可以正式进入题目了。3. 按梯度刷题从分支循环到指针链表的典型题型拆解翁恺C语言习题集的题目顺序是有讲究的前面的题练语法和思维习惯中间的题开始引入指针和内存概念后面的题则把数组、指针、结构体、字符串串在一起形成综合型编程题。很多刷题的人死在半路不是因为后面的题真的难而是前面的基础题里养成了坏习惯到后面一起爆雷。这一章按题目的梯度逐个拆解每一阶段的典型题目、标准解法和常见坑都说清楚。我的建议是按顺序刷不要跳。前面的分支循环看似简单但它练的是把自然语言翻译成代码的能力这个能力不过关后面写链表只会更痛苦。3.1 前段习题分支、循环与数组的常见陷阱这一阶段典型的题目是读入一系列整数以某个特殊值结束求最大值、最小值、平均值或统计特定区间内的个数。这类题看起来简单但恰恰是初学者最容易写出“能运行但结果不对”的代码的地方。看下面这个统计最大最小值的例子#include stdio.h #include limits.h int main(void) { int n; int max INT_MIN; int min INT_MAX; int first 1; while (scanf(%d, n) 1) { if (n 0) { break; } if (first) { max n; min n; first 0; } else { if (n max) { max n; } if (n min) { min n; } } } if (first) { printf(no valid input\n); } else { printf(max%d min%d\n, max, min); } return 0; }这里有几个细节值得逐一说清楚。scanf(%d, n) 1是在检查读取是否成功而不是想当然地认为用户一定会输入整数。如果用户输入了字母scanf会返回 0while循环就会退出程序不会陷入死循环。INT_MIN和INT_MAX来自limits.h作用是给最大值和最小值一个初始参照避免第一轮比较时出现逻辑错误。first标志位处理的是“没有任何有效输入”的情况此时不应输出一个假的极值。初学者常在这类题上犯两个错一个是把if (n 0)写成if (n 0)一个是把最大值初始化为0或某个自以为“足够小”的数。前者是赋值而不是比较后者在输入全为负数时直接翻车。这类题本质上是在训练“边界情况意识”代码是否严谨换一组测试数据立刻见分晓。3.2 中段习题指针与动态内存分配的标准解法到了指针阶段题目的特点变成“数据规模由程序运行时输入决定”于是需要malloc动态分配内存。典型题目形态第一行输入一个整数n接下来输入n个整数求它们的和或平均值。很多人的第一反应是“定义一个足够大的数组”这种做法在这个阶段会显得很别扭。题目没有规定n的上限你定义一个int a[10000]万一n是 20000 呢数组越界的问题就是这么来的。#include stdio.h #include stdlib.h int main(void) { int n; int i; int *a; long long sum 0; if (scanf(%d, n) ! 1 || n 0) { printf(invalid n\n); return 1; } a (int *)malloc(n * sizeof(int)); if (a NULL) { printf(malloc failed\n); return 1; } for (i 0; i n; i) { if (scanf(%d, a[i]) ! 1) { printf(not enough numbers\n); free(a); return 1; } sum a[i]; } printf(sum%lld\n, sum); free(a); return 0; }这段代码里最重要的一行是if (a NULL)。malloc在内存不足或请求大小异常时返回NULL如果不检查就直接使用程序会在a[i]这个位置发生空指针解引用轻则段错误重则带崩整个进程。初学阶段有人觉得“这行多余”直到某次真在测试环境里跑挂了才知道检查这一下的分量。另一个关键点是sum用了long long而不是int。输入规模大了之后int很容易溢出这是缓冲区式越界之外的另一种“看不见的错误”。计算平均值时就必须用浮点或者保留足够精度否则题目要求“保留两位小数”时你的整型除法直接截断结果怎么都不对。free(a)必须和malloc配对出现。C 语言不负责帮你回收动态分配的内存忘记释放程序长期运行就会内存泄漏。刷题阶段程序跑一遍就退出泄漏一两块内存肉眼看不见但它是一个必须从一开始就养成的工程习惯。3.3 后段习题链表与字符串处理的边界意识链表题是整套习题集的分水岭。前面的指针题还能靠“把数组改成动态分配”糊弄过去链表题没有任何数组可以替代节点之间靠指针串联每个插入、删除操作都要自己处理连接关系。看这道典型的“按输入顺序建立链表并统计节点个数”的题#include stdio.h #include stdlib.h #include string.h typedef struct Node { int data; struct Node *next; } Node; Node *appendNode(Node *head, int value) { Node *newNode (Node *)malloc(sizeof(Node)); if (newNode NULL) { return head; } newNode-data value; newNode-next NULL; if (head NULL) { return newNode; } Node *p head; while (p-next ! NULL) { p p-next; } p-next newNode; return head; } void freeList(Node *head) { Node *p head; while (p ! NULL) { Node *next p-next; free(p); p next; } } int main(void) { Node *head NULL; int value; int count 0; while (scanf(%d, value) 1 value ! -1) { head appendNode(head, value); count; } printf(count%d\n, count); freeList(head); return 0; }链表题的核心心智模型是“先接后继再动前驱”。插入节点时如果先修改前驱节点的next再修改新节点的next原来的后继节点就丢了。这里appendNode每次从头部开始找尾节点时间复杂度是 O(n)数据量小的时候完全够用如果数据量大就应该在函数外保留尾节点指针。这是算法优化的话题但翁恺的习题里已经有人在第一版代码里写出了这个优化。释放链表的freeList函数值得细看先存next再free(p)否则p被释放后拿不到下一个节点。这个“先保存再释放”的顺序在二叉树递归释放节点时也会遇到练一次就永久长记性。字符串处理题在这个阶段通常会和链表结合比如按行读入字符串然后排序输出strcmp、strcpy、strncpy之间的区别和边界行为刷到这部分时一定要实测一下而不是靠记结论。4. 刷题时最容易踩的五个坑现象、原因与解决方法这套习题集刷到一定量之后你会发现很多人卡住的位置高度相似。下面五个坑是刷题过程中出现频率最高的每条都按“现象 - 原因 - 解决”的顺序说清楚。它们不是“碰巧写错”的细节问题而是C语言基础功架的问题跨过之后后续的题目会顺畅很多。4.1 scanf读取失败导致死循环现象、原因与解决现象程序运行时输入了一个字母或一个带小数点的数终端里的输出像失控一样疯狂刷屏程序根本停不下来只能按CtrlC强制终止。原因scanf(%d, n)遇到非数字字符时会读取失败返回 0但那个坏字符仍然留在缓冲区里。循环体内的代码没有处理这个残留下一轮循环scanf读到的还是同一个坏字符返回 0循环条件却可能没有退出机制于是一次次空转看起来就是死循环。解决读取后立刻检查返回值并在读取失败时清空缓冲区。int n; while (1) { int ret scanf(%d, n); if (ret 1) { break; } else if (ret EOF) { return 0; } else { while (getchar() ! \n) ; printf(invalid input, try again\n); } }这段代码的逻辑scanf成功就跳出循环继续业务返回EOF表示文件结束了直接终止程序其余情况说明输入里有非法字符用getchar()把这一行的残留字符全部读掉然后提示重新输入。这个“失败后清缓冲再重读”的模式是 C 语言输入处理里最基础也最实用的手法值得背下来。4.2 malloc了却不释放现象、原因与解决现象单次运行程序看不出问题但如果你在一个循环里反复malloc或者程序被测试脚本调用很多次物理内存占用持续上涨最终程序被系统杀掉或者机器开始卡顿。原因C 语言没有垃圾回收每一次malloc申请的内存都需要对应的free归还。很多人刷题时觉得“程序跑完就退出了操作系统会回收内存”这话在单次运行的场景下没有错但它掩盖了代码里的结构缺陷——你的代码在真实场景里可能不是跑一次就结束的。解决给“谁申请谁释放”立一条铁律。函数内申请的内存在函数内所有可能退出的路径上释放主函数里申请的内存在程序结束前释放。回忆一下 3.2 里的malloc示例它保证每个可能出错的分支都先free再退出这就是“配对意识”。4.3 字符串比较写成了if (s1 s2)现象、原因与解决现象两个字符串肉眼看起来完全相同if (s1 s2)判断却总是返回假有时候又神奇地返回真表现得非常随机。检测程序跑起来像玄学现场更换编译器、开启优化后结果可能还变。原因s1和s2是char *类型的指针比较的是两个指针的值也就是两个字符串各自在内存中的首地址而不是字符串内容。两个内容相同的字符串字面量编译器可能把它们放在同一个地址也可能放在不同地址这直接导致同一个表达式在不同编译器下结果不一样。解决用strcmp或者strncmp比较字符串内容。字符串处理题里这是最基础也最高发的错误翁恺的习题集中后期一定会有一道题专门踩这个点。#include string.h if (strcmp(s1, s2) 0) { printf(strings are equal\n); }strcmp返回值为 0 表示相等负数表示s1 s2正数表示s1 s2。这里要特别注意判断相等是 0如果写成if (strcmp(s1, s2))那是“不相等才进入”正好和直觉相反。4.4 数组越界本地不报错一上测试就崩现象代码在本地运行一切正常提交到测试系统或者换一台机器编译程序直接段错误或者输出一个干扰结果。回头检查代码发现数组下标访问越界了。原因C 语言不检查数组边界。访问a[n]当数组长度是 n 时合法下标是 0 到 n-1时实际上是在访问数组结束后的第一块内存那块内存可能属于别的变量也可能属于栈上的其他数据。本地环境里那块内存恰好没被使用程序侥幸没崩换一个环境内存布局不同问题立刻暴露。解决养成在代码里写清楚边界的习惯。所有循环访问数组时循环条件里写明“从 0 到长度减 1”不要用“某个恰好等于长度的魔法数字”。更实用的一招是在题目测试之外给程序喂一组“刚好踩边界”的输入比如数组长度是 10你就输入 10 个元素看循环是否在访问a[10]。调试器里的 AddressSanitizer 是另一个强力工具编译时加上-fsanitizeaddress一旦越界它会立刻给出报告比人工肉眼排查快得多。4.5 返回了局部数组的地址现象、原因与解决现象在函数里定义了一个数组填充数据后把数组名返回给调用者。调用者拿到的指针看起来正常打印几个值也还在但过一会儿再访问里面的数据变成了随机内容或者整个程序直接崩溃。原因函数内的局部数组存放在栈帧上函数返回时栈帧被回收这块内存变成了“未分配”状态。指针的值还在但指针指向的内存已经不属于你里面的数据随时会被其他函数调用覆盖。这是 C 语言里最隐蔽的坑之一因为它不一定立刻报错。解决三种方案按场景选择。一是用static修饰局部数组让它变成静态存储期函数返回后数据仍然有效但注意它是全局共享的多次调用会互相覆盖。二是把数组通过参数传入函数由调用方管理内存。三是用malloc动态分配同时规定由调用方负责free。第三种方案最灵活但要严格遵守上一条“谁申请谁释放”的原则。刷题遇到“返回一个字符串”这类需求时这三个方案至少要在心里过一遍不要拿着局部数组往外冲。5. 做完题不等于学会三种办法验证代码真的写对了能通过题目附带的测试用例只能说明代码在“那几组输入”上是正确的不能说明代码在所有边界条件下都正确。翁恺C语言习题集的一大特点是题目本身的测试用例通常温和但提交后评判系统会拿你没见过的边界数据来考验你的程序。所以每做完一道题不要立刻觉得自己过关了而是应该用下面三种方法再验一遍。这不只是为了应付习题更是为了让刷题刷出真正的肌肉记忆。5.1 边界条件自查法把输入逼到题目允许的极限值每道题在开始动手之前先花十秒钟想一想输入的可能边界。常见边界包括最小值、最大值、空输入、只有一个元素、所有元素相同、元素个数达到题目声明上限、输入内容前后带空格。把这些边界写成一个小的测试清单然后逐项运行自己的程序。#include stdio.h #include limits.h int main(void) { printf(INT_MAX%d\n, INT_MAX); printf(INT_MIN%d\n, INT_MIN); return 0; }这看起来和题目无关但它能帮你确认一个关键事实int的数值范围到底是多少。很多人在题目要求“求所有整数之和”时用int存储结果输入稍微大一点就溢出成负数自查时根本不会想到。知道INT_MAX的具体值之后再去看题目的数据范围说明心里就有底了。自查还有一个重要技巧用极小数据集和极大数据集分别跑一遍。极小数据集暴露逻辑分支错误极大数据集暴露时间和内存问题。如果题目说输入最多 100 万个整数那你就要构造一份差不多的输入看程序跑起来是否在合理时间内结束。很多题看着简单数据一放大就暴露算法复杂度的问题。5.2 用调试器观察变量和内存把“黑匣子”打开代码出问题时第一反应不要是在代码里到处加printf。printf调试在简单题目上勉强可用一进入链表和指针题就非常低效——打印一个指针变量的值是看不出它指向的节点结构是否正确的。调试器可以做到这一点而且可以随时暂停观察任何变量在任何时刻的值。命令行下基本操作如下# 编译时加入调试信息 gcc -g -Wall -Wextra -o demo demo.c # 启动调试 gdb ./demo # 在 main 函数入口下断点 break main # 运行程序 run # 查看变量值 print head # 查看 head 指向的结构体 print *head # 单步执行到下一行 next # 查看当前调用栈 bt这里的-g选项是生成调试信息没有它调试器无法把机器指令和源代码行对应起来。print *head在链表题里几乎是神技可以看节点里data的值和next指针的地址一步步追踪整条链。如果只想看某个变量有没有变化可以用watch命令给变 量下断点它会在变量值被修改时立刻停下。这个过程把链表操作从“凭感觉写”变成了“亲眼看它变”指针语义的理解会有一个质的飞跃。5.3 改题重写法把原题换个条件再写一遍验证自己是否真的吸收了题目的解法方法很简单把题目换一个条件重新写一遍。比如原题要求“按从小到大排序”就改成“按从大到小排序”原题数组长度固定为 10就改成“先输入 n 再动态分配”。这样改装之后代码的大部分结构是和原题一样的但有几个关键逻辑发生了改变你必须真正理解原题解法才能成功做出来。“改题重写”比“多做几道新题”更精确地命中问题核心。它迫使你区分“记住了代码”和“理解了思路”之间的差别。如果改装之后能独立写出来说明这一题的要点已经内化。如果卡住了说明原题当时的解法里有一部分只是照抄的趁这个机会把那一块彻底弄明白。对于链表题、递归题、指针题这三类我建议每一道都至少在第二天重写一遍。间隔一天之后记忆中的代码细节会淡去留下的只有思路框架这时候重写写出来的是自己的背出来的是别人的。6. 把习题集用透从单文件作业到工程化练习的进阶技巧当你能流畅地把题目做出来下一步就值得把练习方式升级到工程化。翁恺C语言习题集前期的题是单文件就可以解决的但随着题目复杂度上升链表、文件读写、字符串处理组合题把代码全部堆在一个main.c里会变得难以阅读也难以排查问题。开始时把同类功能拆成函数、拆成模块这是从“会做题”走向“在真实项目里能干活”的关键一步。一个常见的组织方式是把字符串处理、链表操作、内存管理这类在多个题目里反复出现的代码整理成一个公共工具模块。比如list.h和list.c专门放链表的创建、插入、删除、释放函数utils.h和utils.c放readInt、clearBuffer这类处理输入的工具函数。然后用一个简单的Makefile管理编译写一道新题时直接复用之前的代码而不是每次从零敲一遍链表。CC gcc CFLAGS -Wall -Wextra -g all: practice practice: main.c utils.c list.c $(CC) $(CFLAGS) -o practice main.c utils.c list.c clean: rm -f practice这个Makefile看起来简单但它建立了“分开写、一起编”的模块意识。main.c只负责题目逻辑utils.c和list.c提供通用能力各自都能被测试。在练习阶段就形成这种习惯后后续做更大的项目面对几十个源文件时文件的组织和管理会自然得多。我自己的血泪经验是刚刷到链表题时所有代码都堆在一个main.c里测试函数和题目函数混在一起链表插入函数改了三次每次都要重新编译整个文件。后来把链表操作的测试单独拆出来问题定位立刻快了很多。如果你练习时也感到“代码一旦变长就不好维护”说明该拆分了而不是你有问题。动手试一试这个流程把最近做过的两道题拆成模块main.c只保留题目逻辑公共部分放进utils.c编译、运行、过一遍边界输入。你会发现代码的清晰度和可复用性立刻上一个档次。多加一个test()函数把所有边界测试集中到一块每次改完代码直接跑一遍回归比每次手动输入测试数据高效得多——这是这套习题能带给你的最后一层收益也是刷题和工程能力之间的那道台阶。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →