第二次翻开学C语言的书多半是前面那次踩的坑实在太深了。我第一次学的时候指针就当摆设字符串全靠 gets写个链表三天两头段错误最后连“4到6”这么靠前的章节都没能好好啃完。这次重学我不看那种厚到能砸核桃的教材也不幻想一天背一百个代码就能成为大牛只把目标缩到四个字真懂、能跑。这篇文章记的就是我从第4部分学到第6部分的全过程——指针、数组、字符串、结构体、链表外加一堆调试环境上的坑和排查经验。如果你也是学了一遍觉得“像没学过一样”的状态这篇内容应该能让你少走不少弯路。1. 为什么学第二遍以及这次我换了什么思路1.1 第一次学C语言是怎么翻车的大多数人的第一次C语言学习问题不在智商而在节奏和工具链。我第一遍学的时候语法看着都懂变量、循环、函数、数组上课跟着老师写作业抄一抄也交了。但一到指针和内存教材上的文字就变成了一堆绕口令——“指针是变量的地址”“指针的指针”“数组名是常量指针”每个字都认识合在一起就完全不知道在说什么。更糟糕的是我习惯用scanf和gets输入字符串完全不检查缓冲区长度程序一跑就崩。当时身边也没人告诉我应该开编译器警告、用调试器看变量我连“段错误”是什么都搞不清楚只能靠printf到处打点打得眼都花了还是找不到问题。这种状态维持到中间章节基本就没动力了因为每次写代码都在跟环境搏斗而不是在学编程本身。第二次学我最大的改变是先搞清楚“为什么会有指针、为什么要有堆栈、为什么字符串这么难搞”再动手写代码。语法细节放在实际项目里慢慢磨而不是对着语法表死记。这也正是后面第4到第6部分最需要的东西因为它们恰好是C语言从“能写”到“敢写”的分水岭。1.2 重学计划调整4到6到底学什么不同的教材对章节编号有差异但主流C语言教材的“第4章到第6章”通常会被以下几个主题包圆数组与排序算法冒泡排序、选择排序指针与地址运算字符串处理与输入输出细节结构体与自定义类型链表与动态内存分配这几个东西看起来是分开的本质上是一条线C语言不提供太多现成的“字符串类型”和“数组对象”你能用的就是一块块连续的内存。数组是连续内存的视角指针是访问这块内存的“遥控器”结构体是把多个数据打包成一块大内存链表则是用手动申请的内存把结构体串起来。理解了内存后面的一切都顺了。所以这一轮我给自己定的任务是每学一个概念必须写一个小程序来验证它哪怕程序只有二十行。看得懂不等于会写会写不等于能调通能调通还得经得起改来改去不崩。第4到6部分我几乎每天都泡在代码和调试器里才慢慢找回了“对一块内存了如指掌”的手感。1.3 学习环境与工具准备重学的头一天我就把编辑器换了。以前用的是那种点了运行弹黑框的古老集成环境一旦崩了报错信息跟猜谜一样。这次我直接上了VS Code GCC的组合轻量、免费配置一次后面能用很久。安装 VS Code在扩展里搜C/C官方扩展装好安装MinGW-w64或同等工具链把gcc.exe所在目录加入系统 PATH按CtrlShiftP打开命令面板搜“C/C: Edit Configurations”设置编译器路径调试需要配置launch.json和tasks.json让 F5 能直接编译并启动调试这个环境的好处是错误信息直接在终端面板显示你可以看到gcc给出的警告和错误不会像老环境那样吞掉细节。而且调试时能实时看变量值、内存地址、调用栈这对理解指针和数组太重要了。我还是那句话如果你连编译器警告都看不到那你学的就不是C语言是在学猜谜。2. 指针第二次才看懂的几个关键点2.1 指针到底是什么怎么跟变量和内存对应起来指针之所以吓人是因为教材喜欢用“指针就是地址”这种过于简练的话。地址是个数字但数字本身不能让你理解为什么要有指针。我自己的理解方式很简单内存就是一排编了号的储物柜变量名是贴在某几个柜子上的便利贴指针就是一张写着柜号的白纸。比如int x 5;系统在内存某处放了整数 5这个位置的编号假设是0x7ffde8a0。当你写int *p x;p里存的就是这个编号。“用*p”就是把白纸上的柜号取出来去那个柜子里拿东西。有个点很多新手都会绕晕p本身也是一个变量所以p又是另一张白纸新的白纸写的是“存着柜号的那张白纸放在哪个抽屉”。这就产生了“指针的指针”语法上写成int **。第4到6部分如果只让我留一句话我会说看到星号不要慌先想星号前面那个变量的类型是什么再想它指向的是什么。上手建议写个小程序用printf打印x、x、p、p、*p的值肉眼对比一下数字。这一步能抵消掉你80%的恐惧因为你会亲眼看到“x和p是同一个数字*p和x是同一个值”。2.2 数组名、指针运算与函数传参的真相数组在C语言里其实没有自带“长度”信息数组名在绝大多数表达式里会退化成一个指向首元素的指针。这就是为什么你不能把数组直接传给函数并指望附带长度只能额外传一个n。我第一次看到int arr[]作为函数参数只觉得是在偷懒第二次才明白编译器看到int arr[]的时候根本不会拷贝整个数组而是把它当作int *arr。所以在函数里写sizeof(arr)只会得到 864位系统上一个指针占8字节而不是数组真正的大小。指针运算也是重学才搞懂的。p 1不是把地址加 1而是加sizeof(*p)个字节。对int *来说p 1就是加 4 字节对char *则加 1 字节。这个设计其实特别合理因为地址运算的最终目标是“跳到下一个同类型元素”而不是“跳到下一个内存字节”。函数传参又一个核心场景传值 vs 传址。C语言函数参数默认是值传递意味着你改了形参实参不变。想通过函数修改外层变量怎么办只能把外层变量的地址传进去也就是传一个指针函数里用*解引用去改。这个“用指针间接修改实参”的模式是所有C语言代码的地基。2.3 为什么要用指针动态内存、效率与“间接”思维第二次我再看指针总结出它的三个不可替代作用第一动态内存分配。你不可能永远知道程序运行时需要多少数据比如读一个文件行数未知只能运行时用malloc申请指针指向这块新内存。没有指针就没有链表、树这些数据结构的立足之地。第二避免复制开销。结构体可能很大你把一个几百字节的结构体直接传给函数每次调用就要复制一整份。传指针只要8字节函数内部直接操作同一份数据。性能差异在写小练习时感觉不到但等你日后写嵌入式的数据处理、或者做图像处理就会感激这个设计。第三灵魂是“间接”。间接地表示一块内存、间接地修改一个变量、间接地让函数和调用者共享同一份数据。指针把“变量”这个概念从“一组字节”扩展成了“可以指到任何一组字节的卡片”。这个思维一旦建立之前看不懂的很多代码比如返回指针的函数、保存指针的结构体、把所有节点串起来的链表都会变得顺理成章。3. 字符串与内存管理重头戏实操记录3.1 fgets和输入处理第一个要改掉的老毛病字符串在C语言里没有专门的类型它就是一个以\0结尾的char数组。听起来简单但输入处理里全是坑。以前我写gets(buf)时从来没想过如果用户一次性输入了超过buf大小的内容gets会毫不客气地继续往后面写内存直接破坏相邻变量甚至程序状态。这属于缓冲区溢出安全隐患极大。现在的教材和在线系统基本也禁止gets了编译器甚至会直接警告。正确做法是用fgets:#include stdio.h #include string.h int main() { char buf[100]; if (fgets(buf, sizeof(buf), stdin) NULL) { return 1; } // fgets 会把换行符也读进来需要手动去掉 buf[strcspn(buf, \n)] \0; printf(输入的是: %s\n, buf); return 0; }fgets的第二参数是缓冲区大小第三参数是文件流传stdin就是从键盘标准输入读。它能保证最多读sizeof(buf) - 1个字符最后补\0不会越界。这就是为什么所有严肃的C语言练习都推荐它。需要注意fgets读到换行符时会一起读进字符串所以要去掉尾部的\n。我习惯用strcspn(buf, \n)找到换行符的位置再置为\0这个函数跟strlen类似但更安全因为它在缓冲区里查找第一个匹配字符。3.2 字符串拆分、拼接与常见坑字符串处理里最经典的场景就是“把一个字符串按空格拆分成多个单词”。网上搜到很多相关题目典型做法有三种strtok、sscanf、手动遍历。strtok用起来最省事但坑也最多#include stdio.h #include string.h int main() { char str[] hello c language world; char *token strtok(str, ); while (token ! NULL) { printf(%s\n, token); token strtok(NULL, ); } return 0; }strtok会用\0替换分隔符直接修改原字符串所以传给它的一定要是可写的字符数组不能是字符串字面量hello...否则程序会崩。另外strtok内部维护了一个静态状态连续调用时传入NULL代表继续上次拆分这就导致它天生不支持多线程环境下的并发拆分。如果不想修改原字符串或者分隔符比较复杂可以手写一个读单词的函数遇到非空格就记起点遇到空格就截断逻辑不复杂但是可控性好。字符串拼接的经典坑是用strcat前没有确认目标缓冲区空间够不够。strcat不会管目标数组到底有多大它只负责把源字符串接在后面。正确思路拼接前先用snprintf之类的手段做一次格式化或者手动计算长度。总之心里要始终悬着一根弦C语言不会保护你越界就是越界后果是未定义行为轻则数据被改重则段错误。3.3 堆与栈单片机C语言没有堆栈吗热搜里有一条很有意思“单片机C语言没有堆栈吗为什么”。其实所有跑C语言程序的地方都有调用栈单片机也不例外。调用栈是函数调用和局部变量赖以运转的基础架构哪怕是最小的 8 位单片机编译器也会在启动文件里设置栈指针。区别不在“有没有”而在“有多大”。个人电脑上一个默认进程的栈空间通常是几MB级别你用递归函数消耗几万个栈帧基本没事。单片机的RAM可能只有几十KB其中还要分一段给栈可能只有1KB到几KB。一个例程调用嵌套过深、局部数组太大、或者递归没有收敛很容易把栈顶踩到堆区或寄存器备份区表现为程序跑飞、死机、HardFault。所以如果你是学完C语言要转单片机的人需要知道堆栈在嵌入式环境下的特殊性堆在嵌入式软件开发中往往不用默认的malloc/free因为碎片和不确定性在实时场景下不可接受栈则要在链接脚本和启动文件中明确配置。这个知识链条是C语言标准让你不必关心堆栈细节但嵌入式开发逼着你看懂.s文件里的栈设置。建议第4到6部分学完指针和结构体之后找一块单片机开发板把main()之前的启动流程过一遍你会发现“堆栈”这个词瞬间落地。3.4 内存泄漏排查malloc与free的配合动态内存分配让链表成为可能也让漏内存成为新人几乎避不开的毛病。C语言没有垃圾回收你malloc出来的每一块内存都欠着系统的一笔账不主动free这块内存在进程结束前都拿不回来。内存泄漏在一次性短程序里感觉无所谓可一旦写服务器、写嵌入式长时间运行的程序最终会耗尽内存。新手第一次写链表时光顾着malloc新节点常常忘了在删除节点后把free写回来。排查这种问题最直接的办法是检查增删操作对称性每处分配都和一处释放对应。经验不足时可以用valgrind检测gcc -g -o list_test list_test.c valgrind --leak-checkfull ./list_test它会清楚报告“有多少字节在多少个分配点未被释放”。这是我第二次学链表时最依赖的工具几乎每一次内存泄漏都会被它精确地揪出来。动手做链表练习的同学建议从第一天就养成“写完先跑 valgrind”的习惯。4. 结构体、链表与常见算法练习4.1 结构体与 typedef把零散数据打包成“一个东西”结构体解决的是“多个相关变量必须绑定在一起”的问题。以前你描述一个学生需要三个平行数组一个存姓名、一个存学号、一个存成绩操作这些数据时总要对齐索引一不小心就错位。结构体就是把这堆字段打包#include stdio.h #include string.h typedef struct { char name[50]; int id; float score; } Student; int main() { Student s1; strcpy(s1.name, Alice); s1.id 1001; s1.score 92.5; printf(%s %d %.1f\n, s1.name, s1.id, s1.score); return 0; }typedef在这里不是必须的但加上之后以后声明变量就不用每次写struct Student了直接Student s2;就行。很多项目代码里大量使用 typedef 来隐藏结构体的关键细节让调用方的代码更干净。结构体在内存里不是简单地把字段挨在一起而是会按对齐规则插入填充字节。比如一个char加一个int可能占8字节而不是5字节。这个知识在你日后做通信协议、写文件格式或做嵌入式底层时非常重要因为你要算出结构体真实占用的字节数或者用#pragma pack调整对齐。4.2 链表练手从零写一个不崩的链表链表是结构体、指针、动态内存分配三个大件的集合点。第4到第6部分如果没有一次自己动手写链表的经历总觉得少了点什么。我推荐的入门写法是用哨兵头节点简化边界判断#include stdio.h #include stdlib.h typedef struct Node { int data; struct Node *next; } Node; Node *create_list() { // 哨兵头节点不存数据统一了空表和非空表的操作 Node *head (Node *)malloc(sizeof(Node)); head-next NULL; return head; } void insert_head(Node *head, int value) { Node *new_node (Node *)malloc(sizeof(Node)); new_node-data value; new_node-next head-next; head-next new_node; } void print_list(Node *head) { for (Node *p head-next; p ! NULL; p p-next) { printf(%d , p-data); } printf(\n); } void free_list(Node *head) { Node *p head; while (p ! NULL) { Node *tmp p; p p-next; free(tmp); } } int main() { Node *list create_list(); insert_head(list, 3); insert_head(list, 7); insert_head(list, 1); print_list(list); free_list(list); return 0; }写链表的第一个难点是“修改头指针实参不变”的问题。如果插入函数直接操作传入的head指针并在头部用head new_node去改它调用方的head不会变因为指针本身也是值传递。解决方式就是刚才的哨兵头节点或者让插入函数返回新头。带着这个认知去看各种链表题解你才不会被反复出现的**head搞得一头雾水。第二个难点是删除节点时指针的衔接。删除时你要同时知道前一个节点和当前节点把前一个的next跳过当前节点再释放当前节点。很多第一次写删除的人只记得free(p)忘了改前一个节点的指针结果链表断掉一访问就段错误。4.3 冒泡排序与数组练习的经验之谈排序算法也是第4到6部分的固定内容。冒泡排序虽然复杂度不占优但它原理直白适合练双重循环、数组下标和相邻元素交换void bubble_sort(int arr[], int n) { for (int i 0; i n - 1; i) { int swapped 0; for (int j 0; j n - 1 - i; j) { if (arr[j] arr[j 1]) { int tmp arr[j]; arr[j] arr[j 1]; arr[j 1] tmp; swapped 1; } } if (!swapped) break; } }注意这里swapped优化如果某一趟没有任何交换说明数组已经有序提前结束。这是刷题和考试里经常考察的细节。还有一个容易错的点n - 1 - i。每一趟排序都会让一个最大值落到正确位置所以下一趟可以少比较一个。少写那个- i也能排序但会做多余比较复杂度仍然 O(n²)只是常数变大。这些边界下标就是写排序最容易出 bug 的地方调试时建议在关键位置打印i、j和arr[j]。5. 常见问题排查与学习建议5.1 段错误与野指针先用调试器而不是乱打印第二次学过程中我最大的进步是从“到处加 printf 碰运气”变成了系统排查。段错误的本质就几种访问了 NULL 指针、访问了已释放内存、数组越界写坏了未知数据、函数返回了指向局部变量的指针。定位手段优先用调试器。VS Code 里按 F5 启动调试后程序会在崩溃点停下来调用栈面板会显示崩在哪个函数、哪一行。你只要看那行代码访问了什么内存再配合变量面板检查指针值基本两分钟内就能定位。相比在程序里插一堆printf然后重新编译调试器的效率高出一个数量级。如果手里只有命令行就学一下gdb的几个基本命令run、bt、frame、print。bt看调用栈frame切换栈帧print看变量值。这套组合足以应对绝大多数段错误排查。5.2 典型问题速查表现象常见原因排查方向程序一运行就崩溃空指针解引用检查指针是否初始化是否被置为 NULL数据莫名被修改数组越界写入检查fgets/scanf的写长度检查循环边界字符串输出乱码缺少\0检查拷贝和拼接是否在末尾置\0函数里改了变量外面没变值传递检查是否需要传入指针删除链表节点后访问崩溃释放后仍用该指针释放后置NULL避免悬垂指针程序运行后内存不断增长内存泄漏用 valgrind 查分配和释放是否对称第二条“数据莫名被修改”尤其值得记录。我第一次写数组题时循环边界多写了一个等于号导致arr[n]被写坏而这正好覆盖了相邻的另一个变量。程序不崩但输出了完全错误的结果这种 bug 最坑人。以后凡是“结果不对但没崩溃”先怀疑越界。5.3 学习资源怎么选翁恺课程与题库搭配第二次学C语言我在资源上做了取舍。网课选了翁恺老师的C语言课程讲课节奏舒服特别适合补指针和结构体这种概念密集的章节。但我的建议是视频只看“概念没懂时”的那一段不要从头刷到尾更不要用听课替代写代码。练习方面把PTA上的基础题刷一部分尤其字符串和链表相关的题。网上流传的“c语言必背100代码”可以作为入门索引但千万别机械背。代码是要背一些不是背写法而是背“这个场景下数据是怎么流动的”。比如字符串逆序、按空格拆分、日期计算、九九乘法表这些题都对应一类固定的编程思维练到不看代码能默写你的手感就来了。如果是冲计算机二级就在学完第6部分之后集中刷历年选择题和大题。二级考的重点跟这里覆盖的章节高度重合数组、指针、结构体、链表、文件操作。文件读写那部分如果第一次没学透可以单独补一下fopen、fprintf、fscanf原理和标准输入输出几乎一样只是多了打开关闭步骤。5.4 一段“第二次学”才体会到的调试心得最后分享一个我踩过很多次坑后才明白的道理编译器警告不是噪音是你的第一道防线。以前我编译时只要没有 error 就觉得程序没问题后来才知道 warning 里藏着大问题比如“format %d expects argument of type int*”其实已经暗示你漏了。我用printf输入%d却传了一个变量的值而不是地址这种题目在PTA上能把人卡到怀疑人生但只要认真看编译器警告马上就能发现。关于变量用%d输入后字符变量的值问题是C语言一个典型细节字符本质是ASCII码%d和%c只是解释方式不同。有人输入A用%d输出 65就以为数字和字符是一回事其实A这个字符在内存里就是整数 65 的编码你用哪种格式解释都行只要类型和格式对应不错。这个原理搞明白之后很多“输出奇怪数字”的问题就不攻自破。我现在的感觉是第二次学C语言最大的收获不是记住了更多知识点而是建立了一套排查问题的思路先看编译器警告再用调试器定位崩溃点最后用 valgrind 查内存。这套方法论不专属于C语言以后写C、写嵌入式甚至写Java都能迁移过去。第4到第6部分学完之后我建议你可以试着把一个综合性的小项目从头实现一遍比如写一个命令行版本的学生成绩管理系统——读文件、结构体数组、排序、增删改查全都会用上。到了那一天你就不再觉得自己是在“学”C语言而是真的在“用”C语言了。
企业数字化 ERP 产品动态
相关推荐
抖音高清下载真相:绕过App抓取原始4K视频流 1. 为什么“抖音高清下载”这件事,90%的人从第一步就错了你是不是也试过:点开抖音视频,右上角点分享,选“保存到相册”,结果发现——手机里存下来的视频糊得像蒙了层毛玻璃?明明原片在App里看着锐利通透&am… · 2026/9/26 7:44:18
C语言进阶:指针、链表与文件读写实战解析 1. 为什么我要第二次学C语言,而且只盯着“4到6”看?很多人在第一次学C语言的时候,都逃不过同一个结局:考完试就忘,遇到专业课又得从头翻书。我也是这样,大一学完C语言,指针和结构体背得滚瓜烂熟… · 2026/9/26 7:44:18
Claude Code实战:终端里跑起来的AI代理式编程工具 老实说,我第一次在终端里敲下claude这个命令时,习惯性以为它又是一个把对话窗口搬到命令行的玩具。但真正上手之后,我必须承认判断错了——Claude Code 不是聊天框,而是能直接读文件、改代码、跑命令的代理式编程工具。简单来说&a… · 2026/9/26 7:44:18
AI出海实战:从算力调度到大模型部署与Agent开发全解析 1. 从"堆卡"到"拼生态":AI出海这盘棋的底层逻辑变了 2025年做AI出海,如果还停留在"买卡、训模型、发API"这条老三样路径上,大概率会发现自己陷入了一个尴尬的境地:算力成本压不下去,模型… · 2026/9/26 8:51:32
LLM Agent驱动的开源代码评审新范式 1. 项目概述:这不是一个工具,而是一套可落地的开源代码评审新范式“open-code-review”这个标题乍看像某个 GitHub 仓库名,但实际它指向的是一场正在 quietly 发生的工程实践变革——不是简单地把传统 Code Review 流程搬到线上,而… · 2026/9/26 8:51:32
Mycat2 install-template 实战:从安装到分库分表配置与避坑 简介:mycat2 install-template 是一份面向 MyCat 2 数据库中间件的安装模板包,主要帮助开发者和运维人员在部署分布式数据库时快速获得可复用的配置与启动环境。压缩包体积仅 1.19MB,共包含 52 个文件,类型以 SQL 脚本、JSON 配置… · 2026/9/26 8:51:32
深度残差收缩网络:软阈值化与注意力机制如何提升噪声鲁棒性 1. 深度残差收缩网络到底在解决什么问题 第一次看到“深度残差收缩网络”这个名字,很多人会以为它只是把残差网络和某个叫“收缩”的模块拼在一起。实际上,它要解决的是一个非常具体、也非常普遍的问题: 当输入数据里混有噪声或冗余特征时&a… · 2026/9/26 8:51:32
深度残差收缩网络DRSN:软阈值化与注意力机制如何灵活删除冗余特征 深度残差收缩网络(Deep Residual Shrinkage Network,DRSN)这个结构,我第一次接触是在处理工业设备振动信号降噪的任务里。当时用常规的残差网络做故障分类,模型在训练集上表现很好,但一到现场数据就拉胯——… · 2026/9/26 8:51:32
通信客户流失预测:MLP建模与业务对齐的全流程实战 简介:本资源是一份面向大数据与人工智能方向高校教学的Python机器学习实战教案,聚焦通信运营商客户流失预测这一典型业务场景,适用于大数据技术类专业本科生及初阶数据科学学习者。教案系统覆盖客户流失分析全流程:从数据去重、缺… · 2026/9/26 8:51:26
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍 简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第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