ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

C语言经典100题刷题指南:从语法细节到工程实践

C语言经典100题刷题指南:从语法细节到工程实践 能力比如单片机开发、嵌入式、Linux C都得靠这套基本功撑着。很多初学者会觉得这 100 题太“基础”了不如直接去啃项目源码来得快。但我的实际体会刚好相反刷完这 100 题你的 C 语言才算真正“落地”了。今天这篇文章我就把这 100 题的价值、覆盖的知识点、以及我刷题过程中踩过的坑全部整理出来分享给你。1. 这 100 道题到底值不值得逐题刷先聊一个很多人纠结的问题网上 C 语言题目一抓一大把为什么偏偏是“经典 100 题”值得过一遍我的答案是它不考验智商考验的是你对语言细节的掌握程度。LeetCode 那种偏算法和数据结构而经典 100 题更偏向语言本身——指针怎么用、数组怎么传参、字符串为什么容易越界、结构体怎么对齐。这些东西直接决定你能不能写出能跑、能改、能维护的 C 程序。1.1 题目的难度梯度设计经典 100 题通常覆盖从“Hello World”到“链表增删改查”的完整路径。我自己粗略分了一下大概能分成四个阶段阶段代表题型核心目的第一阶段1-25题变量交换、奇偶判断、九九乘法表、闰年判断熟悉语法、输入输出、基本运算第二阶段26-50题冒泡排序、选择排序、字符串逆序、回文判断掌握数组、字符串、经典算法第三阶段51-75题递归求阶乘、汉诺塔、结构体学生成绩、链表节点操作理解函数递归、结构体、指针第四阶段76-100题文件读写、动态内存分配、位运算、综合小项目贴近真实工程场景这个梯度设计非常合理。如果你直接跳到最后去做文件操作大概率会因前面的字符串处理不熟练而卡住。我见过很多初学者一上来就写链表结果连结构体指针的-和.都没分清楚最后 Debug 到崩溃。1.2 “满足所有C程序基础”是什么意思这个描述其实没有夸大。C 语言的全部基础语法点大概就是变量与数据类型、运算符、流程控制、数组、字符串、函数、指针、结构体、文件操作、内存管理这一张清单。经典 100 题就是围绕这张清单设计的。刷完它你至少能做到看到需求就能拆出变量和逻辑顺序遇到编译报错能根据经验快速定位知道什么场景该用数组、什么时候该用链表能理解别人源码里指针为什么要这么传2. 100道经典题对应的知识地图从变量到链表的通关顺序我刷题的时候习惯做一件事每做完一题就把这题涉及的知识点在图里画个勾。等 100 题刷完整个 C 语言基础框架的“知识地图”就清清楚楚了。下面这份清单是我重新整理过的知识点归属你可以直接拿来对照着刷。2.1 输入输出与格式化陷阱这一块是很多新手和老手都会踩坑的地方。100 题里前几道几乎都涉及printf和scanf比如“输入一个字符输出它的 ASCII 码”“输入三个数输出最大值”。看起来简单但里面藏着几个特别容易翻车的细节scanf(%d, a)的不能丢丢了就是段错误连续输入字符和数字时缓冲区里的换行符会干扰下一次读取printf的格式占位符要和变量类型严格匹配%d给float会输出乱码尤其是第三点热搜词里有一条叫“c语言变量用%d输入一个字符后的值”说得就是这种类型不匹配导致的结果不可控。我用一个例子告诉你后果有多离谱#include stdio.h int main() { char c A; printf(%d\n, c); // 输出65这是字符A的ASCII码成绩合格 int a 3.99; printf(%d\n, a); // 输出3小数被截断你以为是四舍五入就错了 return 0; }%d用在字符上其实是“按整数解读这个字符的存储值”所以输出 65 反而是正确的。而 3.99 被存进 int 时直接截断成 3跟四舍五入毫无关系。这些都是基础题里反复强调的点也是日后调试程序最容易怀疑人生的地方。2.2 数组、指针和字符串C语言最硬的骨头经典 100 题的中段开始大量出现数组和指针。比如“用指针交换两个数”“字符串逆序”“统计字符串中单词个数”。这里有一个核心概念必须搞清楚数组名在大多数表达式中会“退化”为指向首元素的指针。举个高频考题——字符串逆序#include stdio.h #include string.h void reverse(char *s) { int len strlen(s); for (int i 0; i len / 2; i) { char temp s[i]; s[i] s[len - 1 - i]; s[len - 1 - i] temp; } } int main() { char str[] hello; reverse(str); printf(%s\n, str); // 输出 olleh return 0; }这段代码能成立的前提就是字符数组的名字str传给函数时实际传的是数组首地址函数内部通过指针s直接修改了原数组内容。很多初学者在这里困惑“为什么不是值传递吗为什么函数里改了外面也变了”——因为数组传参本质上就是传地址不涉及拷贝整个数组。2.3 结构体、链表和文件操作从语法到小项目的关键一步到了 75 题往后基本就是结构体、链表加文件读写。这个阶段的目标是让你把前面学的零散语法串成一个整体。比如经典的“学生信息管理系统”你要用一个结构体存学号、姓名、成绩然后把多个结构体串成链表再把数据写入文件或从文件读出。这里我想特别提一下链表。热搜词里有“c语言链表”说明这确实是大家搜索的重灾区。链表的本质是“节点 指针”每个节点既存数据又存下一个节点的地址。难点在于插入和删除时要处理好指针指向顺序错了就会丢节点或造成内存泄漏。我记得当年刷题时写了一个“头插法建链表”的函数因为忘了把新节点的next指向原来的头节点结果链表变成了只有两个节点的死循环。那种调试经历比背十遍书都管用。3. 用一道“冒泡排序”题演示完整做题链路100 题中“排序”是必考主题。与其泛泛讲一堆原理不如我把热搜词里出现的“冒泡排序c语言”作为完整示例带你走一遍从读题到跑通的整个流程。这道题虽小但足以体现所有经典题的通用解法。3.1 读题后先列步骤不要急着写代码题目输入 5 个整数用冒泡排序把它们从小到大输出。我的做题习惯是先在草稿纸上写步骤定义长度 5 的整型数组用循环读入 5 个数外层循环控制“总共需要跑几趟”内层循环比较相邻两个元素逆序就交换输出排序后的数组这个过程看似多余但对新手至关重要。C 语言是一个“你必须清楚地知道每一步在内存里发生了什么”的语言。你没想清楚就直接敲代码结果就是编译过了、运行结果却不是你要的然后在printf里到处加日志去猜。3.2 完整代码与关键解释#include stdio.h int main() { int arr[5]; int i, j, temp; printf(请输入5个整数\n); for (i 0; i 5; i) { scanf(%d, arr[i]); } // 冒泡排序核心外层 n-1 趟内层 n-1-i 次比较 for (i 0; i 4; i) { for (j 0; j 4 - i; j) { if (arr[j] arr[j 1]) { temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; } } } printf(排序后结果\n); for (i 0; i 5; i) { printf(%d , arr[i]); } printf(\n); return 0; }这段代码里最值得讲的是内层循环的j 4 - i。为什么不是j 5因为每一趟冒泡都会把当前未排序部分的最大值“冒”到最后面。第一趟结束后最后一个元素已经是最大值第二趟结束后倒数第二个元素也已经到位。如果内层还傻傻地遍历整个数组就会出现无意义的比较——效率低是小事关键是思路不清。3.3 跑通之后的自测方法很多读者做完题就急着下一道其实漏掉了最值钱的一步边界测试。刷 100 题最大的收获之一就是养成自测的习惯。以冒泡排序为例至少得测这三组数据完全逆序5 4 3 2 1验证排序是否能把最坏情况理顺全部相同2 2 2 2 2验证算法在大量相等元素时是否出错负数混入-3 0 5 -1 2验证负数比较是否正常3.4 对比其它排序法每次做题都顺手做一次横向发散刷题时我常做一件事同一道题想方设法用不同方法再解一遍。比如排序题除了冒泡还有选择排序和插入排序。选择排序的核心是每一趟找到最小值下标内层结束后再交换。代码通常是for (i 0; i 4; i) { int minPos i; for (j i 1; j 5; j) { if (arr[j] arr[minPos]) { minPos j; } } if (minPos ! i) { temp arr[i]; arr[i] arr[minPos]; arr[minPos] temp; } }对比之后你会发现冒泡排序比较和交换都发生在内层循环代码直观但交换次数多选择排序比较次数不变但交换次数大大减少。实际项目中数据量大时这个差异会非常明显。所以刷题时别只满足于“写出来”多问自己一句“还能怎么写哪个更好”这 100 题才算没白刷。4. 刷题现场最常翻车的五个技术点我的排查笔记这 100 题里有一类“坑”是你刷之前不知道、刷之后拍大腿的。我把它们整理成一份“翻车笔记”也许能帮你少走很多弯路。4.1 关于“gets被废弃”和“fgets的正确用法”老教材里经常出现gets(str)但它是 C 语言的著名“毒瘤”因为它无法限制输入长度输入超长就会缓冲区溢出。现在很多开发环境里直接编译不过或者编译通过但运行会报错。所以经典 100 题里凡是涉及字符串输入我都强烈建议用fgets#include stdio.h #include string.h int main() { char str[100]; fgets(str, sizeof(str), stdin); // fgets 会把换行符也读进来必要时手动去掉 str[strcspn(str, \n)] 0; printf(输入的是%s\n, str); return 0; }热搜词里有“c语言fgets”和“c语言隐藏光标”可见很多人都是被gets的坑逼着查到了fgets。注意fgets(str, sizeof(str), stdin)的第二个参数是缓冲区的最大长度包括结尾的\0。它比gets安全得多但副作用是会把你按回车时留下的\n一起读进来所以上例里用strcspn(str, \n)] 0;手动去掉换行。4.2 scanf缓冲区残留引发的“莫名其妙”这是最经典的一个坑。完整过程很难展开但典型场景是先循环读整数再读字符你会发现scanf(%c, c)直接读到了一个换行符程序跳过了你想要的字符输入。原因就是上一次按回车时\n还在输入缓冲区里。解决方案有两种在%c前面加空格scanf( %c, c)这样会跳过所有空白字符用getchar()把残留换行吃掉getchar();4.3 指针初始化和的迷失很多题会考察“用指针操作数组”或“用指针交换两个变量”。新手最常见的问题是声明了指针却不赋初值然后直接往里面写值程序秒崩。因为野指针指向了一个不确定的内存空间。你写进去的那一下不知道写到了哪里。正确姿势是让指针先指向某个合法变量的地址int a 10; int *p a; *p 20; // 现在 a 变成 20热搜词里有“c语言中的*a什么意思”这里我需要多说一句*在 C 语言里有三个完全不同的身份。声明时int *p表示 p 是一个指向 int 的指针解引用时*p表示“取出 p 指向的那块内存里的值”乘法时a * b是乘法运算。把这三种用法分清楚指针学习就顺利了一半。4.4 结构体对齐不是你想的那样当 100 题进行到结构体时你会遇到“这个结构体明明是 6 个字节为什么 sizeof 出来是 8 甚至 12”这种问题。这是结构体对齐规则在起作用。简单理解编译器为了让 CPU 读取数据更高效会按某个对齐数在成员之间插入填充字节。struct Student { char name[5]; // 5字节 int score; // 4字节 }; // 实际 sizeof 可能是 12而不是9因为int需要 4 字节对齐所以char name[5]后面会填充 3 个字节的空洞让score的起始地址落在 4 的倍数上。如果你要在单片机或嵌入式环境下定义协议帧结构体这个细节就非常关键因为填充字节会直接破坏你和硬件约定好的内存布局。4.5 字符串函数使用不当造成的越界100 题里字符串相关题目的隐藏考点基本都是strcpy、strcat、sprintf这类函数它们都不会自动检查缓冲区是否越界。热搜词里有“c语言字符串函数”说明你一定会搜到这个类别。用strcpy时如果目标数组容量不够它还是会硬写进去覆盖掉数组后面的其他数据。安全版本分别是strncpy、strncat、snprintf。实际刷题时可以试着写一个“模拟 strcpy 的实现”你会更深刻理解为什么它会越界。5. 刷完 100 题之后怎么把知识变成实际项目能力刷完 100 题只是开始不是终点。很多人在刷完题之后会产生一种“好像都懂但项目里根本不会用”的感觉。我自己的经验是需要做三件事来转化能力。5.1 从“做对”到“复盘”给自己提三个问题每写完一题别急着提交或下一题先问自己三个问题如果数据量扩大 100 倍这题的程序还能跑吗涉及时间复杂度和内存如果把输入条件改一下比如原来是整数、现在是浮点数我的程序要改几行我这个程序有没有可能因为某种输入而崩溃比如输入 0、输入超长字符串这其实是把刷题思维从“语法练习”转变为“工程思维”的关键一步。很多人说 C 语言难学其实难的不是语法而是这种“关注机器视角”的思维切换。100 题的价值正在于用足够多的小练习帮你完成这个切换。5.2 把经典题改造成“小项目”练手比如排序题可以升级为“学生成绩表的排序和查找”结构体那题可以升级为“图书管理系统”文件题可以升级为“日志文件按行读取并解析”。我当年练的一个课后扩展是把经典题中的“学生信息管理”扩展成用链表存储、支持增删改查、并支持文件保存和加载的程序。这个程序并不大约 300 行但它一口气用到了结构体、指针、动态内存、链表、文件操作几乎覆盖了经典 100 题后半段所有知识点。当你完成这个改造后你会发现你终于明白了为什么要用链表而不是上来就是一个数组你理解了malloc和free为什么必须成对出现你知道文件写入和读取时为什么要注意换行符和 EOF 的处理5.3 结合单片机场景理解 C 的底层能力热搜词里有“单片机c语言没有堆栈吗为什么”这个提问非常典型说明很多人在学 C 语言时会联想硬件。其实单片机是有栈的只是它的栈通常由链接脚本和启动文件分配在片内 RAM 的固定区域而且栈空间很小。你在 PC 上刷 100 题时可以用malloc随意申请几 MB 内存但在单片机里这样写很容易把堆区撑爆导致设备死机。所以刷完 100 题后我建议再去了解三件事栈区、堆区、静态区分别怎么回事malloc在嵌入式环境中的风险和替代方案以及提高程序健壮性的常见做法比如链表操作时判断空指针、文件打开时判断返回值。这部分的真实体会是C 语言基础打不牢后面学单片机、学嵌入式、学 Linux C 都会非常痛苦。5.4 刷题节奏与心态建议最后聊一下刷题节奏。我建议不要一天刷 20 道也不要想一口吃成胖子。一种我自己试过且效果不错的方式是每天 5 道新题每道题控制在 20 到 40 分钟每周末把本周错的题重新写一遍不要看一道题就搜答案至少独立思考 20 分钟同一道题用两种以上解法完成之后才算真正掌握这 100 题不是用来“背答案”的而是用来“训练你的编译器式思维”的。我把其中的关键经验总结成一句话你写的每一行代码都要知道它在内存里干了什么。遵照这个原则刷完 100 题你的 C 语言基础会非常扎实之后再学任何上层技术都会顺很多。按照这个清单去刷你会发现 C 语言并不是那么“劝退”。它只是要求你比用其他语言时更诚实——你对内存做了什么程序就会还给你什么。这恰恰是它迷人之处。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进