1. 为什么我要第二次学C语言而且只盯着“4到6”看很多人在第一次学C语言的时候都逃不过同一个结局考完试就忘遇到专业课又得从头翻书。我也是这样大一学完C语言指针和结构体背得滚瓜烂熟一到上机写链表就原地转圈。后来工作里要重新捡起单片机、嵌入式相关的东西才真正动了“第二次系统学习C语言”的念头。所谓“4到6”不是说我就学第4到第6章而是我自己给这次复习划的三个模块指针和内存管理、结构体与链表、文件读写与综合应用。原因很简单这是C语言里最像“分水岭”的部分。数组、循环、分支这些语法第一次学的时候就能掌握个七八成但指针、结构体、文件操作这种涉及内存布局、数据组织和跨函数协作的内容必须放到真实场景里再学一遍才能真正弄懂。这次重新学习我给自己定的目标不是“把书再看一遍”而是“把以前跳过的问题全部捡回来”。比如为什么char *s和char s[]不一样为什么fgets读进来的字符串总会带个换行为什么链表头节点销毁后输出还正常。这些问题不解决写出来的C代码永远是半吊子。如果你也处于“学过一遍但还想真正写会C语言”的状态这篇文章会把这三个模块里最值得重新咀嚼的细节、最容易踩的坑以及一个能直接跑起来的小系统全部摊开讲。不管你是计算机专业补基础还是转行自学准备单片机开发都能在里面找到对应自己痛点的内容。2. 第4个主题指针不是玄学是内存的门牌号2.1 先把*a、a、数组名这些事掰开揉碎第二次学C语言我做的第一件事就是重新理解指针。第一次学的时候老师在黑板上写下int *p a;我满脑子只有一句话“p指向a”。但这句话到底什么意思我是真没想清楚。这次我换了个比喻把内存想象成一大排带编号的储物柜每个变量占一个或者几个柜子a就是拿到a所在柜子的编号int *p就是告诉你“p这个东西是用来记录柜子编号的”。而*p就是拿着编号去打开对应的柜子取出里面存放的东西。很多初学者在*a这个问题上栽跟头是因为同一个*符号在声明和取值时含义完全不同。声明里的*表示“这是一个指针类型”比如int *a说的是a是一个指向int的指针变量而在表达式中使用*a表示“取出a指向的那个内存单元里的值”。所以int *a b;之后*a和b是等价的你print出来都是同一个值。我自己在重新学习时专门做了一组对比实验来加深理解写法含义典型用法int a 5;a是一个int变量内存里放整数5直接赋值、参与运算int *p a;p是一个指针变量存的是a的地址函数传参时修改变量本身*p 10;通过p把a的值改成10让函数外部变量被修改int **pp p;pp是一个指向指针的指针链表插入、二维数组传参这些看起来基础但很多人写代码时经常混淆p和*p。我自己以前就是总在函数参数里写void func(int *p)函数体里却写p ...然后抱怨为什么外面的变量没变。第二次学习让我彻底明白**传指针进去是为了能修改指针指向的内容而不是修改指针本身。**如果想让函数修改指针变量本身就得传二级指针int **这是后面链表操作里最常见的坑之一。2.2 指针和数组、字符串的真实关系C语言里数组名和指针的关系是第二次学C语言的另一个重点。数组名在大多数表达式里会被“退化”成指向首元素的指针。比如int arr[5];在printf里传arr传进去的实际是arr[0]。但arr本身不是指针变量你不能执行arr或arr other因为数组名是一个常量地址。字符串这一块更容易乱。char *s hello;和char s[] hello;看似一样实际完全不同。前者s指向一个字符串字面量它存在只读区你尝试修改s[0]会直接段错误后者s[]是在栈上分配了一个字符数组内容可以修改。我第二次学习时专门用sizeof验证过char s[] hello; sizeof(s)是6里面包含了结尾的\0而char *s hello; sizeof(s)在64位机上是8只保存了一个地址。还有一个非常容易忽略的点字符串运算和数字运算不一样。在键盘上读一个数字scanf(%d, n)传的是变量地址读一个字符串scanf(%s, buf)传的是数组名也就是首地址不需要再加。但如果用%c读一个字符就得写scanf( %c, ch)前面那个空格专门用来吞掉上一次输入遗留在缓冲区里的换行。这些细节不重新学一遍根本不会注意到。2.3 内存管理与堆栈的坑C语言里的变量到底放在哪块内存我第二次学习时才真正分清楚。局部变量和函数参数放在栈上函数结束后自动回收用malloc申请的内存放在堆上必须手动free字符串字面量放在只读区全局变量和静态变量放在静态区。以前我根本不管这些后来写一个单片机相关的模拟程序时在函数里定义了一个大数组结果栈直接溢出我才意识到内存布局不是考试知识点而是真实故障来源。典型的错误是把局部数组的地址返回出去。比如char *getString() { char buf[100]; sprintf(buf, hello); return buf; // 危险 }函数一返回栈上的buf就失效了外面再去访问拿到的可能是垃圾数据。正确的做法是用static局部数组或者在堆上malloc由调用者负责free。这个问题在链接串口、文件解析的代码里特别常见因为缓冲区需要跨函数使用。3. 第5个主题结构体、链表与数据处理3.1 结构体定义与内存对齐第二次学习结构体我不再纠结“结构体就是把几个变量包在一起”这种表层理解了而是关注它是怎么被存进内存的。结构体成员之间可能存在“空隙”这就是对齐。比如struct Student { char name[20]; int age; float score; };20个字节的name结束后age可能不会紧跟着放在偏移20处而要跳到偏移24处对齐到4字节边界。整个结构体大小不一定是204428可能会因为尾部对齐变成32。这在用fwrite直接把结构体写到文件时特别关键你写进文件的内容里有空洞换一个编译器或平台读回来可能对不上。我第二次学的时候特意写了一小段代码把结构体大小打印出来验证#include stdio.h struct A { char c; int i; }; int main() { printf(%zu\n, sizeof(struct A)); // 在很多平台上是8而不是5 return 0; }如果不知道内存对齐你根本猜不到为什么结果是8。知道这个特性之后就有了一个实用技巧把结构体成员按从大到小的顺序排列能减少填充浪费。比如char、int、char的排列往往比int、char、char占更多空间。这在开发内存受限的单片机程序时是实打实的优化手段。3.2 链表增删查与冒泡排序的C实现第二次学习C语言我把单链表从头写了一遍才敢说自己会了。链表的本质就是一组不连续内存节点通过指针串起来。每个节点除了存数据还要存一个指向下一个节点的指针。听起来简单写起来却死了一片人核心问题在于“修改指针本身”时需要二级指针。最简单的链表头插法struct Node { int data; struct Node *next; }; struct Node *head NULL; void insert(int value) { struct Node *node (struct Node*)malloc(sizeof(struct Node)); node-data value; node-next head; head node; }这里head是全局变量所以函数里直接改没问题。但如果把head作为参数传进函数想在里面改它就必须传入struct Node **headPtr。原因是C语言传参是值传递函数里只能修改指针指向的内容不能改变指针变量本身的值。我第二次学习时为了验证这个故意写错了一遍看到head在外面还是NULL才彻底记住。链表最常见的配套操作就是排序。冒泡排序在数组上非常好写两个for循环套起来交换值就行。但在链表上如果只交换两个节点的值其实也很简单本质上跟数组一样比较两个节点的data字段需要交换的时候互换里面的数据。这样做避免了繁琐的指针重连适合初学者理解。如果想追求性能再去做节点交换那是后话。我在练习时写过这样一个函数用冒泡排序对链表按data升序排列void sortList(struct Node *head) { if (head NULL) return; int swapped; struct Node *p; do { swapped 0; p head; while (p-next ! NULL) { if (p-data p-next-data) { int temp p-data; p-data p-next-data; p-next-data temp; swapped 1; } p p-next; } } while (swapped); }这里我犯过一个很典型的错误外循环条件用for固定循环次数但链表长度没有单独计算导致越界访问。改为do...while加swapped标志后逻辑就清晰了。所以排序前一定要确定链表长度或者像我这样用“本轮是否交换过”来判断结束。3.3 字符串处理函数fgets、fscanf、fprintf的正确打开方式字符串处理是C语言里最麻烦、也最出成绩的部分。第二次学C语言我把fgets、fscanf、fprintf这几个函数放在一起比较因为它们都涉及输入输出和缓冲区非常容易互相干扰。fgets是从流里读一行最多读n-1个字符然后自动补一个\0。我要强调一点**如果读到的行长度小于n-1那么末尾的换行符也会被读进来。**所以很多人用fgets之后都要做一步char buf[100]; fgets(buf, sizeof(buf), stdin); buf[strcspn(buf, \n)] \0; // 去掉结尾换行如果不去掉后面把buf跟其他字符串比较或拼接时总会莫名多一个换行。这个问题在OJ上刷题时特别常见比如“字符串逆序”“单词统计”这类题目一提交就显示答案错误十有八九是换行符没处理干净。fscanf和fprintf是格式化方面的一对好搭档。fscanf读文件时如果遇到空白字符会跳过所以用%s读单词很方便。但%s在遇到空格时会停止如果一行里有多个词想整行读入就不能用它得用fgets。这几年我在文件操作上养成的习惯是**读数字用fscanf读整行用fgets写文件尽量用fprintf因为格式化输出最直观。**比如把结构体数据写到文件FILE *fp fopen(data.txt, w); if (fp NULL) { perror(open failed); return; } fprintf(fp, %d %.2f\n, stu.id, stu.score); fclose(fp);后面的fscanf(fp, %d, id)格式必须与之一致。另一点是打开文件后一定要检查返回值否则空指针操作会让你排查半天。4. 第6个主题文件读写与一个小系统4.1 文件读写操作的关键细节文件读写是C语言里把数据和程序打通的关键也是第二次学习C语言时最有成就感的模块。很多人觉得文件操作无非就是fopen、fread、fwrite、fclose但实际写起来打开方式选错、缓冲区没刷新、文件路径不对都会让程序表现得很诡异。fopen的第二个参数是打开模式r只能读、写不了w会把文件清空重写a在末尾追加。二进制模式和文本模式在Windows系统上r和rb有区别r模式下读取\r\n会被转换成\n写入时则反向转换rb则不做任何转换。如果你在Linux上开发一般没有这个差别。但我在做跨平台练习时吃过亏写文件用w把结构体二进制数据写进去再换平台读结果数据错乱。所以存二进制结构体数据一定要用wb和rb。文件操作还有两个容易忽略的点。第一写入后的内容不一定立刻落盘可能在缓冲区里程序异常退出时会丢失。所以在关键写操作后可以调用fflush(fp)或者在结束前记得fclose(fp)。第二fread的返回值是成功读取的块数不是字节数拿它判断文件是否读到头while (fread(stu, sizeof(stu), 1, fp) 1) { // 处理 }这个循环能正确停止是因为读到文件末尾时fread会返回0。如果数据块本身可能不完整还要结合feof(fp)来判断。4.2 一个答题系统/学生信息管理系统的完整流程光讲知识点容易忘我第二次学习C语言时做的一个综合项目就是“学生成绩管理系统”。它正好把指针、结构体、链表、排序、文件读写全部串起来推荐你也照着做一遍。需求很简单从文件读取学生信息支持添加、删除、修改学生按总成绩排序最后把结果写回文件结构体定义为struct Student { int id; char name[32]; float score; struct Student *next; };程序启动后用fscanf或fgets从文件逐行读取数据构建链表每次插入节点时维护整个链表的顺序或者最后统一排序退出前把链表内容用fprintf写回文件。这个项目能覆盖C语言里八成的高频考点动态内存分配、字符串复制、指针传递、链表操作、文件流。我在做这个系统的过程中最大的收获是理解了“数据在内存里怎么组织跟文件里怎么组织其实是两回事”。文件里是连续的一行行文本内存里是乱序的节点它们之间的桥梁是fscanf和fprintf。读的时候一行一行拆成字段写的时候再把结构体字段拼成一行。这个过程不涉及高深算法但要求你把指针和字符串的基本功打牢否则根本写不通。4.3 综合实操从键盘输入到文件落盘下面我写一段迷你版的数据录入保存代码突出核心步骤。它演示了从键盘读入学生信息追加到链表最后把链表写进文件#include stdio.h #include stdlib.h #include string.h struct Stu { int id; char name[32]; struct Stu *next; }; struct Stu *head NULL; void addStu(int id, const char *name) { struct Stu *s (struct Stu*)malloc(sizeof(struct Stu)); s-id id; strcpy(s-name, name); s-next NULL; if (head NULL) { head s; } else { struct Stu *p head; while (p-next) p p-next; p-next s; } } void saveToFile(const char *path) { FILE *fp fopen(path, w); if (!fp) { perror(打开文件失败); return; } for (struct Stu *p head; p; p p-next) { fprintf(fp, %d %s\n, p-id, p-name); } fclose(fp); } int main() { int id; char name[32]; while (1) { printf(输入学生id和姓名(输入0 0退出): ); scanf(%d %s, id, name); if (id 0) break; addStu(id, name); } saveToFile(students.txt); printf(保存成功\n); return 0; }注意几个细节scanf(%d %s, id, name)这里name是数组名不需要addStu复制字符串时要手动strcpy不能直接node-name name因为左边是数组名不能整体赋值。这段代码看起来短实际上把结构体、动态内存、指针、文件流全串起来了适合当作第二次学习的收尾练习。5. 常见问题与排查第二次学习时最容易翻车的8个点5.1 段错误、缓冲区残留、乱码……我练习期间整理了8个高频问题每一个都能写一篇吐槽贴。第一个是段错误Segmentation Fault。触发原因几乎都是空指针解引用或数组越界。排查思路是先打印可疑指针的地址看是否为NULL再看循环边界。比如链表遍历经常把p-next ! NULL写成p ! NULL导致循环多跑一步最后访问了空地址。第二个是scanf缓冲区残留。用scanf(%d)后再用scanf(%c)或fgets程序会不等待输入直接跳过。解法是%c前加空格或者用getchar()把残留换行吃掉。第三个是fgets读入换行符。之前提到过buf[strcspn(buf, \n)] \0是最省事的清理方法。第四个是字符串比较错误。C语言里不能写if (name 张三)要strcmp判断返回值是否为0。这个错误太经典了新手期几乎必犯。第五个是结构体直接赋值导致指针成员浅拷贝。比如两个结构体变量互相赋值如果结构体里有char *指针两个变量会指向同一块内存一个释放另一个也跟着失效。深层拷贝需要手动分配内存。第六个是文件打开失败没检查。fopen返回NULL后直接fprintf程序崩溃。无论是读还是写第一步必须判断指针。第七个是内存泄漏。malloc之后不free程序长时间运行会把内存耗尽。尤其是链表删除节点时必须先保存next再free当前节点否则会断链。第八个是乱码。读取UTF-8中文时用%c按字节处理会看到乱码文件读写时模式用错也可能损坏数据。如果只想处理纯英文/数字避开中文会少很多麻烦。5.2 快速排查思路和工具建议第二次学C语言我最大的进步不是多写了多少代码而是学会了系统排查问题。我的排查顺序基本固定看编译警告别无视。很多新手写main不写return编译器给警告也不管。警告里往往藏着真正的错误。用打印确认指针值。在每个关键函数入口和出口打印指针地址、计数器、关键变量能迅速定位是“数据错了”还是“流程错了”。用valgrind检查内存。如果Linux环境valgrind --leak-checkfull ./a.out会告诉你哪一行泄漏了内存这是排查动态内存问题的利器。单元测试式的最小复现。把出问题的那段逻辑抠出来单独写一个几十行的测试程序比在大程序里瞎改要快得多。我给自己的建议是不要一开始就开着调试器逐行走除非逻辑实在混乱。大部分C语言bug是有规律可循的你打印出地址和值基本就能猜出问题在哪了。6. 第二次学习C语言我踩过的最值得说的一次坑最后分享一个真实的排查经历。当时我在写学生信息管理的删除功能链表删除的代码看着是对的但每次删除第一个节点后一遍历程序就崩。我检查了很久最后发现问题出在我的代码习惯上我用一个全局的head指针删除函数里却传入了一个局部的struct Stu *head副本。也就是说我在函数内部把局部指针改成了head-next但外部的全局head还是指向已经free掉的旧节点。解决的办法就是传二级指针或者用返回新头节点的方式。这个坑让我彻底理解了“值传递”和“指针传递却不等于引用传递”的本质区别。此后我在任何需要修改链表头指针的函数里都会强迫自己停下来想一下这个参数到底该写struct Stu *head还是struct Stu **head。第二次学习C语言对我来说最核心的收获就是语法可以背但内存布局和指针传递不能靠背必须靠画图、打印、造一个小项目去一遍遍验证。如果你也想把C语言真正捡起来我劝你不要再一本一本看书了把上面这三个模块捡起来写一个自己的小系统踩一遍我写的这些坑比你看十遍教程都有用。
企业数字化 ERP 产品动态
相关推荐
Claude Code实战:终端里跑起来的AI代理式编程工具 老实说,我第一次在终端里敲下claude这个命令时,习惯性以为它又是一个把对话窗口搬到命令行的玩具。但真正上手之后,我必须承认判断错了——Claude Code 不是聊天框,而是能直接读文件、改代码、跑命令的代理式编程工具。简单来说&a… · 2026/9/26 7:44:18
开源可审计的LLM代码审查工作流:Git+CLI+LLM三层协同 1. 项目概述:这不是一个“工具”,而是一套可落地的开源代码审查工作流“open-code-review”这个标题乍看像某个具体软件的名字,但实际它指向的是一种正在快速成型的新型开发协作范式——用开源、透明、可审计的方式,把大语言模型&… · 2026/9/26 7:44:18
LangChain摘要中间件实战:解决长会话Token超限与断片问题 1. 长会话为什么会“断片”,以及摘要中间件到底在解决什么做过对话类应用的人大概率都遇到过这种场景:用户跟你的 AI 助手聊了四五十轮,前面明确说过“我预算八千、主要拍娃、不要单反”,结果聊到后面推荐相机时,它又开… · 2026/9/26 7:44:18
移动端反作弊主动干预技术:Frida与IDA在ARM64下的Hook实战 1. 反作弊攻防的底层逻辑与整体设计思路反作弊这件事,说到底是一场信息不对称的博弈。做安全的人想尽量隐藏自己的检测逻辑,做逆向的人想尽量看穿对方的每一行代码。而“主动干预技术”这个词,核心在于一个“主动”——不是被动地等作弊行为发… · 2026/9/26 8:19:04
移动端反作弊主动干预实战:Frida与Hook检测对抗 1. 反作弊攻防的战场早已从"特征对抗"转向"运行时博弈"做移动端安全的人这两年应该有个明显感受:单纯靠静态特征扫描已经很难拦住真正有威胁的作弊行为。原因不复杂——作弊工具本身在进化,从早期改内存、改返回值,到现在… · 2026/9/26 8:19:04
从20分钟到1分半:基于MaaS的电商资料包合规核验自动化实战 做电商运营的朋友多少都经历过这种场面:新品要上架,运营拿着一份商品资料包找过来,里面有主图、详情页、A页面、授权书、质检报告、产品文案,说“麻烦帮我看一遍,别出岔子”。你打开文件,一张图一张图地看&… · 2026/9/26 8:19:04
isomorphic-git 的 git.fetch 全解析:从远程仓库拉取提交的 API 参数、返回值与底层实现 开发工具 【免费下载链接】isomorphic-git A pure JavaScript implementation of git for node and browsers! 项目地址: https://gitcode.com/gh_mirrors/is/isomorphic-git 点击查看 免费下载 导读
本文以 isomorphic-git 项目官方文档中关于 git.fetch 的说明&… · 2026/9/26 8:19:04
Spring AI 2 集成 MCP 实战:SSE 与 stdio 两种模式全打通 一开始折腾 Spring AI,大家基本都是同一个路径:聊天、流式、Spring AI Alibaba 或者 OpenAI 兼容接口,跑通就算完事。但聊到后面你就会发现一个尴尬的问题——大模型只能“嘴上说说”,它没办法真的看你磁盘上的文件,更… · 2026/9/26 8:18:58
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍 简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践 一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46