简介面向C语言初学者的浙大翁恺教授配套习题源码包涵盖整数运算、时间计算、BCD解密、水仙花数、九九乘法表、素数统计、猜数字游戏、分数约分、连续和计算、字符串大小写转换、单词长度统计、矩阵局部极大值、组个最小数等典型练习每题均附输入输出格式说明与完整C代码适合自学或课堂巩固。资源共4个文件压缩包仅9KB内含txt题解说明、Python辅助脚本、inscode现场编码配置及html页面其中txt给出题目思路Python脚本可用于快速校验结果html便于在线浏览演示inscode则提供可直接运行的环境配置。目前已有1784人学习下载对备考C语言和日常编程练习均有参考价值。通过这套源码可获得一整套可运行的C语言练习实例既能对照题干梳理解法也能直接调试验证输出帮助初学者从基础运算逐步过渡到字符串处理与数据结构综合训练加深对循环、分支、数组等核心语法的理解为后续深入学习编程打下扎实根基。1. 这套C语言习题集最值钱的部分不是答案而是重写过程很多人拿到某高校公开课配套的C语言习题集源码第一步是编译、运行、看输出第二步是关掉编辑器觉得自己会了。但带过几位刚入门的同学之后我更确信一件事看得懂答案和能自己写出来中间隔着一条很宽的训练带。这套习题集好在覆盖面广、梯度合理从顺序结构、分支循环一路爬到指针、结构体和文件操作几乎覆盖初学者最需要砸实的全部语法点。它真正的使用方式不是背题而是把每道题当一个小型程序去设计、拆解、重构。它对三类人最有用学完语法却写不出完整代码的新手、准备上机考试或复试机试的备考者以及想快速找回C手感、顺便补上工程习惯的从业者。2. 先搭一个不折腾的编译运行环境目录划分、gcc参数与Makefile雏形2.1 为什么我建议放弃一键运行改用命令行很多同学拿到习题集后第一件事是丢进集成开发环境点绿色的运行按钮看到输出就安心。这个动作本身没有错但有一个隐藏成本IDE把编译命令、警告信息、可执行文件路径全部藏了起来一旦程序段错误或者输出莫名偏差你完全没有线索去排查。我更习惯先用命令行把编译过程彻底摸一遍。命令行下你能看到警告来自哪一行能自由控制是否生成调试信息也能在批量验证时直接编译和运行塞进脚本。这是后面所有操作的基础。Windows环境装MinGW-w64Linux直接用系统自带gcc命令都差不多。等熟练之后再切回IDE也不迟训练期正好把编译过程这一课补齐。还有一点不少上机考试环境本身就是命令行编译加自动评测提前适应没有坏处。我见过有人到了考场上找不到一键运行按钮才意识到平时过度依赖IDE。用命令行不是倒退而是把依赖的抽象层撕掉让你真正看清程序是怎么变成可执行文件的。2.2 把每道题整理成独立目录可复现的基础拿到源码包后先别急着看代码把目录重新整理一遍。常见做法是把十几道题平铺在一个文件夹里文件名像ex1.c、test2.c时间一长连自己都分不清哪份是哪道。我一般会按章节建目录例如c_exercises/ ├── 01_sequence/ │ ├── ex01_hello.c │ ├── in01.txt │ └── out01.txt ├── 02_branch/ │ ├── ex02_leap_year.c │ ├── in02.txt │ └── out02.txt ├── 03_loop/ ├── 04_array/ ├── 05_string/ ├── 06_pointer/ ├── 07_struct/ ├── 08_file/ ├── Makefile └── run_all.sh目录名里的数字序号保证它在文件管理器里按学习顺序排列不会被系统按字典序排乱。每个目录里的in和out文件分别保存输入样例和预期输出这是后面批量验证的地基。目录命名采用编号_题型让程序能一眼定位in/out配对文件让验证脚本可以无脑遍历。如果某道题有多组样例就命名为in01_1.txt、in01_2.txt这样的后缀脚本只需要循环匹配。我在实际整理时还会顺手把每道题的输入约束抄在README里例如n最大是多少、字符串长度有没有上限。这些约束直接决定了数组该开多大也决定了调试时该构造哪些边界输入。2.3 最小编译命令与三个高频参数单文件题目的编译命令其实很简单gcc ex02_leap_year.c -o ex02_leap_year但裸奔式编译会放过太多警告。C语言的老问题在于很多错误在语法层面并不致命只是行为未定义编译器给了warning就继续生成可执行文件。作为习题训练我更推荐每次都带上同样的三件套gcc -stdc99 -Wall -Wextra -g -o ex02_leap_year ex02_leap_year.c参数说明-stdc99把标准固定在C99防止编译器默认标准不同导致行为漂移-Wall和-Wextra开启绝大多数警告新手有时看不懂警告在说什么但至少能知道有没有问题-g生成调试信息没有它后面的gdb和valgrind都拿不到行号。如果怀疑数组越界或未初始化变量我会追加-fsanitizeaddress,undefined让运行时替你做边界检查代价是程序体积变大、速度略慢但对习题集规模完全无感。等确认无误后再回到普通编译模式做性能测试。遇到一个源文件一个小题的场景这些参数已经足够以后写多文件工程时再引入头文件路径和库链接参数也不迟。2.4 一个够用的Makefile雏形题量到二三十个文件之后重复敲编译命令本身就是时间黑洞。这时值得引入一个极简Makefile把重复劳动收口。以下是我常用的雏形CC gcc CFLAGS -stdc99 -Wall -Wextra -g SRCDIRS 01_sequence 02_branch 03_loop 04_array 05_string 06_pointer 07_struct 08_file all: \ 01_sequence/ex01_hello \ 02_branch/ex02_leap_year \ 03_loop/ex03_prime 01_sequence/ex01_hello: 01_sequence/ex01_hello.c $(CC) $(CFLAGS) -o $ $ 02_branch/ex02_leap_year: 02_branch/ex02_leap_year.c $(CC) $(CFLAGS) -o $ $ clean: rm -f $(SRCDIRS:%%/*.exe)Makefile逻辑说明CC和CFLAGS是变量顶部改一处就能作用于所有规则all目标作为入口把要交付的程序列出来后面每条规则格式是目标文件: 依赖文件下一行开头必须是Tab键不能用空格这一条几乎每个新手都会翻车。$代表目标文件$代表第一个依赖文件。想新增习题只需要仿照已有规则复制一份改文件名即可。注意clean那一行我第一次写时图省事直接rm -f $(SRCDIRS:%%/*)结果把目录下的C源码也一起删了。正确做法是只删可执行文件比如按扩展名过滤或者用find . -type f -perm -111 -delete这样不会误伤代码。我一般还会在Makefile里加一个test目标内容是调用run_all.sh让make test变成完整回归编译、测试、提交三条动作收敛成一条命令。3. 从「看得懂答案」到「自己能写出来」按题型拆解重写路径3.1 习题集里最常见的是哪三类题拿到源码后如果只是按顺序一题一题往下做很容易陷入做完了又忘了的死循环。我自己带人的习惯是先把题目按考点分类每类集中刷因为同一类题目的套路极其相似集中练习才能形成肌肉记忆。这套习题集里出现频率最高的是三类第一类是数值计算与分支判断典型题有闰年、日期差、一元二次方程求根、水仙花数考察重点是格式化输入输出、浮点数精度和边界条件。第二类是数组与字符串典型题有冒泡排序、选择排序、字符串反转、单词计数、字符频率统计考察重点是下标管理和缓冲区边界。第三类是指针与结构体典型题有链表插入、学生成绩排序考察重点是间接访问、动态内存和结构体的整体赋值。分类之后你会发现每类题都能提炼出稳定骨架。数值题核心是读完变量、分情况讨论、按格式输出字符串题核心是遍历、条件判断、维护状态结构体排序题核心则是定义结构体、确定交换对象、保持稳定性。骨架一旦搭好具体题目只是往骨架里填不同的判断条件而不是每道题都从零开始。3.2 先写伪代码再搭函数框架我遇到过不止一位同学拿到题就动手敲代码敲到一半发现逻辑绕成一团最后只能偷偷翻答案。这不是能力问题而是没有把算法设计和语法实现分开。我的习惯是先在纸上把输入、输出和变换过程理一遍写成一行行伪代码。例如单词计数题读入一行文本 初始化单词数0当前是否在单词内否 遍历每个字符: 若字符是字母: 若当前不在单词内: 单词数加一 标记为在单词内 否则: 标记为不在单词内 输出单词数这个伪代码里没有任何语法细节却能准确描述状态机的全部逻辑。等它稳定之后再翻译成C语言骨架#include stdio.h #include ctype.h #include string.h int count_words(const char *line); int main(void) { char line[1024]; if (fgets(line, sizeof(line), stdin) NULL) { return 1; } printf(%d\n, count_words(line)); return 0; } int count_words(const char *line) { int count 0; int in_word 0; /* 记住当前是否处于单词内部 */ for (const char *p line; *p; p) { if (isalpha((unsigned char)*p)) { if (!in_word) { count; in_word 1; } } else { in_word 0; } } return count; }逻辑说明main里用fgets读一行传给count_wordscount_words遍历字符串in_word记录上一个字符是否属于单词。遇到字母且之前不在单词内时说明进入一个新单词计数加一遇到非字母则把in_word置零。参数说明const char *line让函数只能读取不能改写原字符串避免副作用isalpha的参数强转成unsigned char是因为标准库规定传负值给isalpha是未定义行为处理中文或扩展ASCII字符时尤其要注意。编译命令用第2.3节的三件套即可。如果想验证思路是否完整再补几组输入空字符串、只有一个字母、单词间多个空格、结尾紧跟换行符。这些边界情况往往就是上机测试最容易刷人的地方。3.3 完整推导一道结构体排序题数值题和字符串题的套路相对容易真正把很多人卡住的是结构体。某题要求读入若干学生的学号和成绩按成绩从高到低输出成绩相同按学号升序。常见错误是写了冒泡排序却只交换了成绩而没交换整个结构体。正确做法如下#include stdio.h typedef struct { int id; int score; } Student; void sort_students(Student *stu, int n) { for (int i 0; i n - 1; i) { for (int j i 1; j n; j) { if (stu[j].score stu[i].score) { /* 交换整个结构体而不是只交换score */ Student tmp stu[i]; stu[i] stu[j]; stu[j] tmp; } else if (stu[j].score stu[i].score stu[j].id stu[i].id) { Student tmp stu[i]; stu[i] stu[j]; stu[j] tmp; } } } } int main(void) { Student list[100]; int n 0; while (scanf(%d %d, list[n].id, list[n].score) 2) { n; } sort_students(list, n); for (int i 0; i n; i) { printf(%d %d\n, list[i].id, list[i].score); } return 0; }逻辑说明sort_students用选择排序每次从未排序部分找到更好的一项放到当前位置。比较逻辑同时处理成绩降序和学号升序tmp的类型必须是Student整块结构体而不是int否则只换成绩不换学号输出必然错位。参数说明scanf的返回值应检查是否等于2因为题目可能以EOF结束输入如果用while(scanf(...) ! EOF)遇到非法输入时n照样自增数据就歪了。如果题目不限定学生人数上限定长数组就不够稳妥。习题集里题量小、约束明确用定长数组通常够用但我会在注释里注明最大容量来自哪里避免将来挪到别的场景时误用。结构体排序这块核心考点其实就是整个结构体的赋值是合法且直接可用的很多新手习惯于逐字段拷贝然后漏掉某个字段折腾半天。3.4 验证正确性不能只靠「输出对了」很多同学做编程题跑一两次样例看到输出一致就宣布完成。但上机考试里最残酷的地方在于评测数据往往包含大量边界输入和异常输入只靠样例验证的程序很容易挂掉。我的验证方法是建立一张对比表把边界情况逐一列出输入期望输出实际输出判定空行00通过一个字母a11通过a b c连续空格33通过行尾有多个空格33通过单词中包含数字22通过表格本身不复杂但它迫使你把正常路径之外的场景都过一遍。养成这个习惯之后你会发现自己对每道题的理解比原来深很多因为你开始主动思考每个边界条件是怎么被处理的而不是只盯着主流程。这也是应对上机测试里各种玄学翻车的最后一道防线——与其猜测评测点不如自己先跑到边界。4. 调试与验证让程序自己证明它是对的4.1 断言不是鸡肋把前置条件固化下来初学阶段很多人没用过assert觉得它只是程序员的玩具。实际上在习题集这种小规模程序里断言能发挥比大项目更直接的作用。它的本质是告诉程序这个条件如果不满足就马上停下来。这样错误不会被埋到几层调用之后才爆发。在一段只有几十行的程序里错误通常不会隐藏太久前提是你给了它暴露的机会。举个例子动态创建链表节点时#include stdio.h #include stdlib.h #include assert.h typedef struct Node { int value; struct Node *next; } Node; Node *create_node(int value) { Node *node (Node *)malloc(sizeof(Node)); assert(node ! NULL); /* 分配失败立即终止 */ node-value value; node-next NULL; return node; }参数说明(Node)malloc(sizeof(Node))把void显式转为Node*方便看清意图assert在条件为假时打印文件与行号并调用abort不会继续执行危险的解引用。写链表、二叉树这类结构时断言能让你心里有底而不是靠感觉猜错因。提示在高性能发布代码里有人会用-DNDEBUG在编译时剥离断言。但习题集训练阶段应保持断言开启它能在最早期拦截错误。4.2 用gdb定位段错误先看调用栈再查数据C语言新手最怕的报错不是语法错而是段错误。程序跑着跑着突然崩溃输出可能只到一半。有些同学会选择重跑几次期待概率性通过但段错误几乎总是稳定的逻辑问题只是触发条件稍复杂。我按以下流程处理gcc -stdc99 -Wall -Wextra -g -o ex ex.c gdb ./ex (gdb) run in.txt (gdb) btrun命令把in.txt作为标准输入重定向给程序每次触发崩溃的输入都一样可复现性拉满。崩溃后输入btgdb会打印从main到崩溃点的函数调用栈直接看到是哪个函数的哪一行在做非法访问。如果看不出变量值再用print查看(gdb) print list[i] (gdb) print i (gdb) watch list[i]print直接输出变量当前值watch设置数据断点当list[i]被改写时自动暂停。这两个命令对排查越界写尤其有效。还有个小技巧编译时不开-gbt给出的全是地址区间完全没法看所以调试会话用的编译命令必须保留-g。如果gdb启动时找不到core文件可以用ulimit -c unlimited打开核心转储开关崩溃现场会被保存下来后续能反复gdb ./ex core查看习题集规模通常用不上这招知道即可。4.3 valgrind把内存错误变成人类可读的报告段错误只是内存错误的极端表现更隐蔽的是内存泄漏和越界读写。这类问题在纯逻辑层面几乎不可能靠读代码找到建议直接把程序交给valgrindvalgrind --leak-checkfull --error-exitcode1 ./ex in.txt输出里需要重点关注几类字样Invalid read/write表示越界访问了无效内存uninitialised value表示使用了未初始化的变量definitely lost表示有内存泄漏。如果程序存在上述问题--error-exitcode1会让valgrind以非零退出码结束后续脚本可以把内存有错误等同于测试不过。valgrind运行速度比原生慢一个数量级但习题集程序体量极小运行时间几乎无感。真正需要注意的是编译时保留-g否则报告里只有地址没有行号。valgrind不是玄学它只是替操作系统做了一层内存访问记账凡是你用了没还、还了又用、越界访问的它都会记录在案。对指针题来说跑一遍valgrind基本能覆盖绝大多数隐患。4.4 批量验证一份脚本让回归测试自动化回到第2.2节建立的目录结构当我们积累了十几道题之后最想做的事是一次把全部程序跑一遍确认改动没有破坏任何旧功能。脚本如下#!/bin/bash for dir in 0*/; do for src in $dir*.c; do bin${src%.c} gcc -stdc99 -Wall -Wextra -g -o $bin $src || { echo [FAIL] $src compile; continue; } for in in $dirin*.txt; do [ -e $in ] || continue out$dirout$(basename $in | sed s/in/out/) ./$bin $in /tmp/program.out if diff -q /tmp/program.out $out /dev/null; then echo [OK] $src with $in else echo [FAIL] $src with $in fi done done done逐行逻辑外层for dir in 0*/遍历所有数字开头的目录内层对每个.c文件编译编译失败则直接跳到下一个源文件然后遍历该目录下所有in*.txt输入文件把程序输出重定向到/tmp/program.out最后通过diff与预期输出对比。参数说明diff -q表示只比较文件是否一致不输出具体差异/dev/null把正常差异输出丢弃只保留退出码。这个脚本对目录结构有硬性要求输入文件必须命名为in开头输出对应out开头。如果某道题要输入多组数据只需额外添加in文件脚本会自动覆盖。运行一次当前所有题目的编译和运行状态一目了然任何一次改动造成的回归都会被立即标记出来。5. C语言习题集避坑记录新手集中翻车的五个场景5.1 scanf与fgets混用导致第二次输入被跳过现象程序先scanf读一个整数再用fgets读一行字符串运行时发现fgets没有等待输入直接拿到一行换行符字符串全部丢失。原因scanf读到所需字符后会把末尾的换行符留在输入缓冲区里fgets读到缓冲区里残留的换行符就立刻返回表现就是用户没输入程序已经继续。解决最省心的方式是全部改用fgets读行再用sscanf从字符串中解析。这样每一行输入都由同一个函数读取缓冲区里的状态永远一致。char line[256]; int n; fgets(line, sizeof(line), stdin); sscanf(line, %d, n);逻辑说明fgets把整行连同换行符一起读走sscanf再按格式解析出整数。这样即使前一次输入有异常也不会污染下一次读取。参数说明sizeof(line)指定缓冲区上限防止溢出sscanf的返回值可用来判断是否成功解析出预期个数。5.2 数组越界时不一定报错对一次输入对换一个输入就崩现象程序在当前测试样例上运行正常换一组稍微大点的输入后就段错误有人往程序里加printf调试之后崩溃点还会移动。原因越界写入可能改写相邻变量也可能恰好落在尚未使用的填充区编译器在优化时还会假设你不越界据此调整指令顺序所以崩溃点会随优化级别和调试信息改变。这是未定义行为的正常表现不是玄学。解决编译时增加地址消毒器把它当作运行时的边界哨兵gcc -stdc99 -Wall -Wextra -g -fsanitizeaddress,undefined -o ex ex.c ./ex in.txt参数说明-fsanitizeaddress在读写前后插入检查能在越界的精确位置报告错误undefined同时检测未定义行为比如无效位移和符号溢出。它比事后手动检查所有下标可靠得多也是我强烈建议在习题集阶段就养成的习惯。5.3 字符串比较用了明明内容相同却不相等现象两个字符串内容都是abc但if(s1 s2)永远不成立改用strcmp之后又有人把返回值理解反写成if(strcmp(s1, s2))才进入分支。原因s1和s2是char*类型比较的是指针本身所存的地址而不比较两个地址指向的内存内容是否相同。两个字符串内容相同不代表它们被放在同一个地址。解决使用strcmp且显式判断返回值if (strcmp(s1, s2) 0) { /* 内容相等的分支 */ }strcmp返回0表示相等大于0表示s1字典序更大小于0表示s1更小。判断相等时一定要写 0不要只写if(strcmp(...))那是把不相等当成条件成立。这个返回值约定和其他语言中的compare接口一致但在C语言里特别容易手滑。5.4 返回局部数组的地址拿到手就变成乱码现象函数内定义char buf[64]填入内容后返回buf调用方printf输出异常或第一次打印正确第二次打印就乱码。原因buf是函数栈上的局部数组函数返回后栈帧被回收那片内存随即被系统或后续调用复用。返回的指针成了悬空指针指向的内容随时可能被改写。解决让调用方提供缓冲区或在函数内动态分配内存并约定调用方负责释放。前者更符合习题集场景void build_message(char *out, int size, int value) { snprintf(out, size, value%d, value); }逻辑说明调用方先声明一个足够大的数组把地址传给函数函数通过snprintf往这个地址写入内容所有内存生命周期都由调用方控制。参数说明size必须传数组真实长度防止snprintf越界返回值不用于传递地址因此不存在悬空引用。5.5 编译警告随手忽略运行时行为才开始跑偏现象编译时看到warning: implicit declaration of function没仔细看就继续运行结果输出完全不对或者链接时报错找不到函数。原因缺少头文件或函数原型时编译器在C99下会警告并按旧规则隐式推断函数类型推断出的签名和真实函数不一致时参数传递就会错位行为毫无规律。解决把这类警告直接升级为错误从源头截断隐患gcc -stdc99 -Wall -Wextra -Werrorimplicit-function-declaration -o ex ex.c参数说明-Werror把指定警告变成编译错误。对于新手我建议先用它让编译失败成为常态而不是默认放行的噪声。等到代码能无警告通过编译再考虑放宽。这样能逼自己把每个函数的头文件和原型补齐不再依赖编译器惯着。6. 把习题集升级成一份可维护的代码资产从交作业到写工程6.1 用Git管理每一次重写在习题集阶段Git看起来像过度设计但重写本身就是一个持续修改的过程。每完成一道题提交一次每修正一个边界条件再提交一次几个月后回来能清楚看见每个决策发生的原因。只需三条命令就能开工git init、git add .、git commit。即便在没有图形界面的服务器环境这套工作流也完全够用。6.2 给核心函数加一个最小测试壳与其依赖main函数一次次手动输入不如把关键函数直接做成可被断言验证的形式。比如count_words单独写一个测试入口用几组已知输入验证返回值这条习惯能在任何改动后立刻暴露回归。#include assert.h void test_count_words(void) { assert(count_words() 0); assert(count_words(hello) 1); assert(count_words(a b c) 3); assert(count_words(a b c) 3); }逻辑说明每个断言对应一种独立输入场景断言失败时精确到文件行号。参数说明函数本身只读输入、不产生副作用适合被反复调用测试壳放在独立文件中不影响习题集程序的主流程。6.3 用统一脚本作回归基线最后一个习惯是把第4.4节的run_all.sh当成基线。每次改代码先跑一遍基线再提交如果引入回归脚本会直接标出是哪道题哪个输入挂了。这套流程坚持下来习题集就不再是临时交作业的产物而是随时可以重建、复用、扩展的资产。我自己最早带人刷题时偷懒不写验证脚本某天把数组起始下标从0改成1跑了四个样例才发现所有输出偏移一位。那次之后先写测试后改逻辑的习惯才真正定型后来参加一次上机考试也是靠这个习惯在改代码后一分钟内发现了回归没有对着满屏乱码干瞪眼。希望帮到你。本文还有配套的精品资源点击获取