
简介这份C语言学习资料整合了学习笔记与7个可直接运行的C程序实例面向初学入门以及想巩固基础的开发者内容围绕C语言核心概念展开覆盖变量与数据类型、运算符、流程控制、排序与查找算法、函数封装以及文件操作等关键主题。压缩包共8个文件由7个cpp源代码和1个doc笔记文档组成实例按知识点递进编排包括基础输入输出、控制结构、函数应用、数组操作、指针与内存管理、结构体联合体以及文件读写与错误处理doc笔记可与代码对照学习帮助理解语法细节。整个资源包仅8KB轻量便捷但内容密度较高适合利用碎片时间系统研读。目前已有135人学习下载无论是作为课程配套资料还是自学辅导都能通过动手实践快速提升编程能力为后续项目开发打下扎实基础。 我刷了一圈“C语言学习资料”相关的热词发现挺有意思一方面翁恺、必背100代码、VSCode配置这些词高频出现说明大多数人都处在“入门—跑通—练手”的阶段另一方面还有字符串逆序PTA、文件读写、二维数组这类具体到不能再具体的需求。学C语言最不缺的就是资料缺的是一条不绕弯的路和一批真正能敲出结果的实例。这篇博文不打算再给你列一百个链接而是把被高频搜索的点挨个拆开哪些资料值得留哪些实例值得练VSCode怎么配才不踩坑编译报错怎么查以及从语法到小项目之间那段最容易被忽略的经验。如果你是刚接触C语言、或者学了一段时间还在“看得懂但写不出”的朋友这篇文章就是按你的搜索记录定制的一份实践地图。每个例子我都会给出完整代码、运行效果和背后的设计逻辑保证你在电脑前照着敲就能跑。1. 资料筛选先把手里的C语言学习资料分成能用的和吃灰的1.1 被高频搜索的资料哪些真正值得看先说说“翁恺C语言练习题”。翁恺老师的课在B站和MOOC上确实是一代人的入门记忆但很多人只看了视频没跟上练习题导致一到自己写代码就卡壳。我的建议是视频课一周内集中看完之后立刻投入练习不要边看边刷手机更不要攒着等“有空了再练”。练习题的价值不在于题目本身而在于逼你跳出被动输入进入主动输出。再说“C语言必背100代码PDF”。这类资料我见过不下十份内容大同小异无非是冒泡排序、九九乘法表、斐波那契、字符串逆序、水仙花数之类的经典题。我要泼一盆冷水与其“背”这100个代码不如把这100个代码的题目打印出来每天手写3到5个完全靠回忆写写不出来再对照。真正的进步发生在你合上资料、打开编译器的那一刻而不是复制粘贴的那一刻。还有“C语言入门CSDN”这类搜索词说明很多人还是习惯先翻博客再动手。CSDN上有不少好文章但质量参差不齐有些甚至把指针和数组的概念都写得含混不清。我建议把CSDN当作“报错排查工具”而不是“系统教材”真正常看的应该是KR《C程序设计语言》或者人民邮电出版社的《C Primer Plus》一本足够。1.2 一条不过时的C语言学习路径根据我在嵌入式岗位多年的观察C语言学习最合理的路径是先搞定开发环境再打语法基础接着练经典实例然后深入指针和内存最后结合硬件或项目做一次完整实践。顺序千万不能乱尤其是不要在没搞懂数组和循环的时候就冲去学指针那样只能学个形似。具体的阶段可以这样规划第1周环境搭建学会用VSCode或命令行编译运行一个hello.c知道预处理、编译、汇编、链接大概是怎么回事。第2周到第4周啃完变量、运算符、分支、循环、数组、函数。每一个知识点都要配合至少两个手写实例不要只看书。第5周到第8周集中练指针、字符串、结构体、内存管理。这个阶段是分水岭决定你是“知道C语言”还是“会C语言”。第9周开始做一个综合项目比如学生成绩管理系统、贪吃蛇、文件版通讯录把前面的知识串起来。这条路径没有涉及任何花哨的技巧就是老老实实地打怪升级。你会发现只要前两周的环境和基础弄扎实了后面学什么都顺因为C语言的语法体系性特别强一环扣一环。2. 必练的5个C语言实例从能看懂到能写出来2.1 冒泡排序算法入门的第一道坎冒泡排序是C语言实例中出现频率最高的一个也是面试和笔试的常客。很多人觉得它简单但真让他在纸上写能一次写对的人并不多。这里我给出一版最容易理解的写法#include stdio.h void bubble_sort(int arr[], int n) { for (int i 0; i n - 1; i) { for (int j 0; j n - 1 - i; j) { if (arr[j] arr[j 1]) { int temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; } } } } int main() { int arr[] {64, 34, 25, 12, 22, 11, 90}; int n sizeof(arr) / sizeof(arr[0]); bubble_sort(arr, n); for (int i 0; i n; i) { printf(%d , arr[i]); } printf(\n); return 0; }看懂这段代码并不难关键是要理解两件事。第一为什么内层循环的结束条件是n - 1 - i因为每一轮外循环都会把当前未排序部分的最大值“冒泡”到最后面这个元素下一轮就不用管了所以每轮可以少比较一次。第二为什么外循环是n - 1次因为n个元素最多只需要n-1轮比较就能完全有序最后一轮其实只处理两个元素也可以理解为还剩最后一个元素时它自然就位了。有基础之后还可以想一想怎么优化如果某轮内层循环一次交换都没有发生说明已经有序可以直接跳出。这个小小的优化思路在很多面试题里都会被问到建议自己动手改一版。2.2 字符串逆序数组、指针、内存的三个考查点“字符串逆序C语言PTA”是很多学校作业和在线评测系统里的经典题。它看起来简单但能考出你对数组下标、指针移动和字符串结束符的理解。下面这种写法是最高效的原地逆序方案#include stdio.h #include string.h void reverse_string(char *s) { int left 0; int right strlen(s) - 1; while (left right) { char temp s[left]; s[left] s[right]; s[right] temp; left; right--; } } int main() { char str[] hello world; reverse_string(str); printf(%s\n, str); return 0; }这个题有一个特别容易踩的坑如果你写成char *str hello world;然后传给reverse_string运行时会直接崩溃。原因是字符串字面量存在只读区域任何修改它的操作都是未定义行为。正确做法是像上面这样用字符数组char str[]来承载。还有一个延伸点反转整个字符串和反转每个单词是两类题。PTA里经常有第二种做法是先把整个字符串反转然后再遍历一遍把每个单词内部的字母反转回来两步合起来就是单词顺序不变、字母逆序的效果。建议把这两种变体都敲一遍你的字符串基本功会扎实很多。2.3 while和do-while一个经典误区的现场演示热身词里同时出现了“while和do-while区别”这个知识点确实容易让人绕晕。一句话版区别是while先判断后执行可能一次都不执行do-while先执行后判断至少执行一次。光背这句话没用看代码#include stdio.h int main() { int count 5; while (count 10) { printf(while 执行了\n); } do { printf(do-while 执行了\n); } while (count 10); return 0; }运行结果只有一行“do-while 执行了”而while的循环体内一次都没进去。这就直观展示了二者对“初始条件不成立”时的不同处理。实际应用场景里do-while最容易出现在“先读入数据再判断是否继续”的交互程序中比如菜单循环#include stdio.h int main() { int choice; do { printf(1. 新建文件\n); printf(2. 打开文件\n); printf(0. 退出\n); printf(请选择: ); scanf(%d, choice); // 根据 choice 执行对应逻辑 } while (choice ! 0); return 0; }菜单至少会显示一次然后根据用户选择决定是否退出这天然符合do-while的语义。我见过有人强行用while配合一个初始flag变量来实现同样的效果能工作但绕了一圈代码也不够直观。记住先判断还是先执行不只是语法细节更是对业务逻辑的映射。2.4 文件读写被严重低估的必备技能“C语言文件读写操作代码”是热词里的高频项原因很现实不管是作业还是工作中程序的输入输出都不可能永远停留在键盘和屏幕上。文件读写的核心函数就那几个fopen、fclose、fread、fwrite、fprintf、fscanf。我用一个完整实例演示最常用的二进制文件写入和读取#include stdio.h #include stdlib.h struct Student { int id; char name[32]; float score; }; int main() { struct Student students[] { {1, zhangsan, 89.5}, {2, lisi, 76.0} }; FILE *fp fopen(students.dat, wb); if (fp NULL) { perror(fopen); exit(1); } fwrite(students, sizeof(struct Student), 2, fp); fclose(fp); fp fopen(students.dat, rb); if (fp NULL) { perror(fopen); exit(1); } struct Student s; while (fread(s, sizeof(struct Student), 1, fp) 1) { printf(id%d, name%s, score%.2f\n, s.id, s.name, s.score); } fclose(fp); return 0; }这里最重要的不是fwrite和fread的参数怎么写而是两点经验第一fopen之后必须判断返回的指针是否为NULL文件可能因为权限、路径等原因打开失败不判断直接往下用就是未定义行为第二用二进制方式读写结构体时结构体内部可能有padding即编译器为了对齐插入的空字节导致不同编译器或平台之间文件格式不兼容所以在跨平台场景下更稳妥的做法是逐字段用文本方式写入或使用固定的序列化格式。另外很多人经常忘记fclose短小的demo程序没问题但长期运行的程序里这会导致文件句柄泄漏。写文件后没有fflush或fclose就退出还可能造成数据还留在缓冲区里没真正落盘。2.5 指针入门用“门牌号”理解C语言的灵魂指针是C语言被搜索最多的概念之一也是劝退很多新手的分水岭。我打个比方变量就像一栋房子变量名是房子的名字变量的值就是屋子里住的人。如果我们要让人去另一个房间找人我们不能把这个屋子里的人复制一份搬过去更高效的做法是把门牌号告诉对方让对方按门牌号找到那个屋子。指针就是这个门牌号。看一个最简单的例子#include stdio.h int main() { int a 42; int *p a; printf(a 的值: %d\n, a); printf(a 的地址: %p\n, a); printf(p 的地址: %p\n, p); printf(通过指针 p 取到的值: %d\n, *p); *p 100; printf(修改后 a 的值: %d\n, a); return 0; }int *p a的意思是定义一个指针p里面存的是a的地址。*p是解引用操作也就是顺着门牌号找到那间屋子看到里面的人。你写*p 100本质上就是在修改那个地址里的内容所以a的值变成100。理解指针的关键有两点第一指针本身也是一个变量它也有自己的地址只是它存的值比较特殊是另一个变量的地址第二*在声明时表示“这是一个指针类型”在使用时表示“解引用”两种场景含义不同。把这个理清了后面什么指针数组、数组指针、函数指针都只是换着花样套这个逻辑。3. 从写代码到跑起来环境配置与编译错误实录3.1 VSCode配置C语言环境少走一步都不行“VSCode C语言环境配置”和“VSCode配置C语言环境”连续出现在热词里说明很多人在编译运行这一步就被劝退了。VSCode本身只是一个编辑器不管编译所以核心其实是安装编译器本身。Windows下最常见的方案是安装MinGW-w64把bin目录加入PATH环境变量然后在VSCode里安装C/C扩展。这一步有一个常见的坑装完编译器后VSCode找不到gcc命令。原因通常是PATH没有生效或者安装时没有勾选“Add to PATH”。验证方法是在终端里运行gcc --version如果提示找不到命令就手动把gcc所在目录添加到系统环境变量PATH然后重启VSCode让设置重新加载。接着在代码文件里用快捷键CtrlShiftB打开任务管理器第一次会提示“没有任务”然后选择“创建 tasks.json”把里面的command改成gccargs改成类似下面这样{ tasks: [ { type: cppbuild, label: C/C: gcc build active file, command: gcc, args: [ -fdiagnostics-coloralways, -g, ${file}, -o, ${fileDirname}/${fileBasenameNoExtension}.exe ], options: { cwd: ${fileDirname} }, problemMatcher: [ $gcc ], group: { kind: build, isDefault: true }, detail: 编译器: gcc } ], version: 2.0.0 }还有一个经常被忽略的细节单文件编译用这个tasks.json没问题但如果你的项目里有多个.c文件需要把args里的${file}换成所有源文件的列表否则会出现“undefined reference to main”或者“undefined reference to function name”这类链接错误。多个源文件的编译我建议尽早熟悉命令行方式gcc main.c utils.c -o program。把这些搞明白比在图形界面里点来点去更接近C语言的真相。3.2 编译错误“unreferenced label”的修改方法热词里有一个特别有意思的报错unreferenced label。这不是常见的语法错误而是你写了一个label标签就是goto跳转的目标但这个标签在代码里没有任何地方引用它。gcc在编译时认为这是个无用的标签就会发出警告如果开了-Werror就会直接升级为错误。来看一个典型代码#include stdio.h int main() { int x 0; if (x 0) { goto out; } printf(这行不输出\n); out: printf(程序结束\n); return 0; }这段代码没有问题因为out标签被goto引用了。但如果你删掉了goto剩下的out:就成了无人引用的标签编译就会给出警告。修改方法有几种把标签删掉或者确认代码逻辑是否需要保留标签如果需要保留则检查是不是漏写了goto。还有一种可能是case后面没有break导致一个case标签掉进另一个case但这不是unreferenced label而是switch穿透语义完全不同。提示遇到编译错误不要急着去搜索引擎复制粘贴。先看gcc报错给出的文件名、行号和错误类型。八成问题出在你自己能看出来的地方培养自己读报错信息的能力比记住一百个错误代码更值钱。4. 进阶实例内存管理、面向对象思路与嵌入式实战4.1 C语言的“面向对象”思路结构体加函数指针热词里有“C语言面向对象编程嵌入式实战pdf”和“codesys集成c语言动态库”这指出了C语言学习的一条进阶之路用C语言写出类似面向对象的效果。严格来说C语言没有class关键字但可以用结构体加函数指针模拟出对象和方法的形态。#include stdio.h #include string.h typedef struct { int x; int y; } Point; void point_move(Point *self, int dx, int dy) { self-x dx; self-y dy; } int main() { Point p {3, 5}; point_move(p, 2, 4); printf(移动后的坐标: (%d, %d)\n, p.x, p.y); return 0; }这里把Point结构体当作一个“类”把point_move函数的第一参数设为Point *self就相当于给这个“类”加上了一个可以操作自身数据的“方法”。在Linux内核源码和很多嵌入式开发框架里你都能看到这种风格函数指针作为结构体成员指向具体的实现函数从而模拟多态和回调机制。我自己在实际项目中用过这种方式封装串口驱动和传感器驱动。每个设备对应一个结构体结构体里放一堆函数指针初始化时把具体的函数地址填进去使用时统一通过结构体调用。这样做的好处是底层硬件换了上层业务代码几乎不用大改。4.2 内存管理malloc和free的正确姿势“C语言内存管理”也是高频热词。很多人写代码时对malloc和free存在两个极端一种是完全不释放等操作系统回收一种是过度紧张到处free导致二次释放崩溃。正确的姿势是牢记三条原则谁分配谁释放释放后指针置空以及尽量用数组替代动态分配。写一个容易踩坑的倒序例子#include stdio.h #include stdlib.h #include string.h int main() { char *dst (char *)malloc(64); if (dst NULL) { return 1; } strcpy(dst, hello); printf(dst %s\n, dst); free(dst); // dst NULL; printf(dst after free: %s\n, dst); // 危险 return 0; }这段代码里free之后再次使用dst指针尝试访问内存这是悬垂指针问题。在复杂代码里这种问题很难排查因为你printf的字符串可能看起来还是“hello”但那已经是未定义行为了。解决方法是free之后立即将指针置为NULL这样后续再访问时或者在free之前加一个if判断才能安全兜底。调试内存问题还有一个特别实用的工具AddressSanitizer。用gcc -fsanitizeaddress -g编译你的代码运行时会精确报告是哪一行出现了use-after-free、内存泄漏还是数组越界。这个工具我建议每个学C语言的人都学会能省下无数根掉头发的时间。4.3 嵌入式实战视角为什么C语言至今不可替代从热词里还能看出一个趋势很多人学C语言的目的很明确就是为嵌入式开发做准备。换机配置实例、蓝奏云资料汇总、IBM网络学习资料这些词的出现说明大家已经不满足于纯软件层面的“跑通程序”而是想去控制真实设备。C语言在嵌入式领域地位稳固的核心原因有两个一是它可以直接操纵内存地址和寄存器和硬件打交道不需要中间层二是它的运行时开销极小可以跑在只有几十KB内存的单片机上。我在一个小型项目里用STM32控制步进电机和传感器整套逻辑就是状态机加定时器中断全部用C语言实现。如果在类似场景里用一些高级封装和运行时依赖很重的语言光是运行时环境就会把资源占满。如果你也有嵌入式方向的目标建议在学习C语言的同时尽早接触一块开发板比如STM32或Arduino。不要求一开始就搞懂寄存器配置重点是感受“代码如何通过编译器变成机器指令再控制引脚高低电平”这个过程。这件事会反过来加深你对C语言编译原理和内存模型的理解。5. 常见问题与排查技巧速查表5.1 一份实用的C语言开发问题速查表我把这些年在社区里和项目中被问得最多的问题整理成表格方便你对照排查现象可能原因快速排查方法程序运行崩溃空指针解引用、数组越界、free后继续用用gdb调试或在关键位置加printf打印变量编译报错unused variable变量声明了但没使用删除变量或者确认是否漏写了使用代码undefined reference to main源文件列表不完整或者main函数没写检查tasks.json编译参数确保包含了含有main的源文件控制台输出乱码字符串编码问题通常是中文字符串编码不匹配在Windows下把源文件用UTF-8保存或在代码开头设置区域循环只执行一次就退出变量在循环体内被意外修改或者条件判断里用了检查if判断是否误写为赋值运算scanf输入无效缓冲区残留换行符或输入类型不匹配在scanf前用while(getchar() ! \n)清空缓冲区文件读写结果为空文件路径不对、权限不够、忘记fclose用perror输出具体错误检查文件是否真正存在还有一个特别容易被忽视的经验C语言的学习代码一定要用git管理起来哪怕只是本地仓库。你会经常遇到“昨天还能跑今天改了改就不行了”的情况git status和git diff能帮你迅速定位自己改了什么。这不仅是习惯更是专业开发者的基础素养。5.2 练手项目的挑选原则很多人学完语法之后卡在“没有项目可做”这一步。其实练手项目也有分阶段第一个项目应当是纯控制台的增删改查程序比如学生成绩管理系统只用数组和函数就能完成第二个项目加入文件持久化把数据存到磁盘体验文件读写的意义第三个项目再加入指针和结构体的高阶用法比如用链表替代数组实现动态增删。选择项目的原则是“比当前能力高但又不高太多”。如果代码写起来完全没有思考说明太简单如果连第一步都不知道怎么下手说明太难。我个人推荐一个百试不爽的方向做一个命令行版通讯录。它覆盖了结构体、数组或链表、函数封装、文件读写、字符串操作这些C语言核心内容又不涉及到复杂的编译链接非常适合作为第一个真正完整的项目。很多人在这一步会陷入“我一定要做一个小型操作系统”或者“我要写一个游戏引擎”的幻想。不是说这些目标不好而是当你连指针都还没彻底理解时奇思妙想只会造成挫败。先写三百行有完整功能的通讯录再写一个贪吃蛇能力会肉眼可见地涨。写在最后学习C语言这件事没有任何捷径但它也远没有想象中那么难。资料再多最后都要落到一行一行敲出来的实操上实例再经典都逃不过指针、数组、内存这几个核心概念。我见过很多开发者在经历了多年的高级语言开发之后又回头补C语言的基础原因无它——只有理解了地址、内存和编译链接才真正算是理解了计算机。如果你还在刷着各种“C语言学习资料”的帖子建议今天就打开编辑器把这篇里的某个例子亲手敲一遍哪怕只是最简单的冒泡排序。代码只有敲出来才算是你的。本文还有配套的精品资源点击获取