ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C语言自学笔记:从环境配置到指针内存管理实战

C语言自学笔记:从环境配置到指针内存管理实战 最近刚把C语言基础部分学完趁着热乎劲儿整理一下自己的学习笔记。这套笔记的底子是菜鸟教程的C语言目录加上我在VSCode里敲的每一段示例代码还有刷OJ题目时候踩过的坑。整套内容“暂已完结”意思是基础语法、指针、内存、文件这些主线内容都过完了并不是说C语言完全学透。这篇文章就把我的学习路线、环境搭建、核心知识点和理解笔记完整梳理一遍希望能给正在入门C语言的自学者一点参考也方便我自己以后快速回顾。如果你是完全零基础刚开始看这篇可能会觉得信息量有点大建议先收藏边学边回来看。如果已经学过一段时间只是卡在指针、字符串处理或者编译环境上那上面的内容可以直接跳到对应章节会比对着教程纯看语法舒服很多。1. 学习路径为什么我选择菜鸟教程作为主线1.1 菜鸟教程C语言教程的结构特点菜鸟教程的C语言模块我在决定系统学C语言之前就收藏过后来发现它其实承担了两种角色第一是目录第二是字典。先说目录。它的知识点排列基本符合C语言学习的主流顺序环境介绍、基础语法、数据类型、流程控制、数组、函数、指针、结构体、文件操作、内存管理。这个顺序不是随便排的它是按照“能写出完整程序”需要的知识依赖关系来组织的。比如数组和指针放在函数之前是因为后面讲函数参数传递、字符串操作都要用到数组指针被单独拉出来重点讲是因为它贯穿结构体、动态内存、文件操作的每一个角落。再说字典。学C语言过程中经常会遇到“split函数怎么没有”“这个头文件里面有哪些函数”“fgets和gets到底区别在哪”这类问题。菜鸟教程的每个章节末尾都有函数列表和示例代码我后来把它当成工具书用遇到记不清的语法就翻回去查比翻编译器报错舒服得多。不过教程也有个明显短板示例代码普遍很短只能让人看懂语法很难让人建立“完整程序该怎么组装”的感觉。所以我的策略是用它当主线目录但每个知识点必须额外找3道以上的练习题去巩固配合公开课和OJ平台才算真正“学过”。1.2 我的学习顺序安排与理由我实际的学习顺序不是完全照着教程往下滑的而是自己重新排了一下先过一遍基础语法和数据类型包括printf、scanf、变量、运算符、分支、循环。然后是数组和函数这部分属于“传统语法”相对容易理解。停下来重点攻指针。花了一周左右反复看指针相关的章节配合调试器观察内存地址。之后回到字符串函数、结构体、文件操作、动态内存管理。最后是预处理指令、多文件编译、调试技巧和常见坑位总结。之所以把指针单独拎出来放在函数之后是因为指针和地址的概念需要一点代码量做铺垫。如果连数组下标、函数返回值都没写过直接看二级指针、指针数组很容易劝退。我当时卡在“指针与数组的关系”那一节卡了两天后来沉下心画了内存示意图才真正理解数组名并不是指针、在表达式中会退化为指针这一层关系。把文件操作和动态内存放在比较靠后的位置是因为这两块属于“组合应用”。文件操作需要同时用到字符串、循环、格式化输入输出、错误检查动态内存管理需要理解指针和栈区堆区的区别。顺序太靠前的话代码能跑但不懂为什么那样写容易留下隐患。1.3 练习资源搭配使用光看教程不写代码等于没学这是我这段时间最深刻的体会。我搭配使用的练习资源有三类第一类是翁恺老师的公开课练习题。翁恺老师在讲课时经常带出一些“课后自己试试”的小题目比如“输入年份判断闰年”“逆序输出数字”。这些题目的价值在于角度刁钻往往能把一个语法点拆出好几种边界情况非常适合检验理解程度。第二类是PTA平台程序设计实验辅助教学平台上的C语言题目。这个平台我非常推荐因为它直接给评测结果每一道题都有隐藏测试点比本地控制台自测严格得多。字符串逆序、冒泡排序这些题目我在PTA上刷过好几遍每一次都能发现新问题比如scanf和缓冲区残留、输出格式多了一个空格导致Presentation Error。第三类是自己给自己加戏。每个章节学完我会把示例代码改造成小工具比如把九九乘法表改成可指定行数的版本把结构体里加一个数组字段然后观察内存变化。这种“额外的小需求”锻炼的是查资料和试错能力教程不会告诉你答案但真实写代码时需要的恰恰是这项能力。2. 开发环境准备把“能跑代码”这件事先搞定2.1 编译器选型MinGW-w64为什么是多数人的选择在Windows下写C语言第一件麻烦事是编译器。我刚开始时在VSCode里安装了Code Runner插件以为装完插件就能跑C语言结果运行菜单是灰的命令行里输入gcc也提示找不到命令。原因很简单VSCode默认只是一个编辑器它本身不包含编译器。常见的Windows下C语言解决方案有这几类MinGW-w64轻量适合配合VSCode使用命令行和gcc一致是开源工具链里文档最全的。MSVC微软的编译工具Visual Studio内置功能强大但命令行工具链使用起来略复杂更适合Windows原生开发。WSL在Windows里跑一个Linux子系统可以完全模拟Linux环境做底层的系统编程比较合适但需要折腾一下子系统本身。Dev-C适合教学场景自带界面安装好就能编译运行但调试器配置比较弱成规模工程容易卡顿。我选的是MinGW-w64加VSCode的组合。理由有两条第一MinGW-w64提供的GCC编译器是Linux下最主流的编译器以后如果转到Linux服务器上写C语言命令完全通用不需要重新学习第二它的调试工具是GDBGDB在就业场景里的出镜率很高配合VSCode调试器熟悉一遍后面写项目排错会轻松很多。安装MinGW-w64时需要把bin目录加入环境变量PATH这样命令行里才能直接执行gcc命令。新手很容易忘了加环境变量或者加了之后没有重启终端导致还报“gcc不是内部或外部命令”的错误。检查方法很简单新开一个CMD窗口输入gcc --version如果能打印出版本号就说明安装成功。2.2 VSCode配置C语言环境的完整步骤我整理了一份自己用得很顺手的VSCode C语言配置流程照着操作基本不会卡壳安装VSCode然后安装扩展“C/C”这是微软官方的扩展包含代码提示、格式化、调试支持。在系统环境变量里把MinGW-w64的bin路径加进去比如C:\mingw64\bin。在项目文件夹里创建.vscode目录新建三个文件tasks.json、launch.json、c_cpp_properties.json。tasks.json配置编译任务核心内容是告诉VSCode用gcc编译当前打开的c文件并生成同名的exe。launch.json配置调试会话程序路径指向刚才生成的exe调试器选GDB。下面给出一份我实测可用的tasks.json配置{ version: 2.0.0, tasks: [ { label: build c file, type: process, command: gcc, args: [ ${file}, -o, ${fileDirname}\\${fileBasenameNoExtension}.exe, -g, -Wall ], group: { kind: build, isDefault: true }, problemMatcher: [$gcc] } ] }注意我加了-g和-Wall两个参数。-g是为了生成调试信息没有它VSCode里的断点会失效-Wall会开启常见警告提示虽然程序能编译但警告里往往藏着数组越界、类型转换不匹配这类潜在问题。launch.json对应配置如下{ version: 0.2.0, configurations: [ { name: C/C Debug, type: cppdbg, request: launch, program: ${fileDirname}\\${fileBasenameNoExtension}.exe, args: [], stopAtEntry: false, cwd: ${fileDirname}, environment: [], externalConsole: true, MIMode: gdb, miDebuggerPath: C:\\mingw64\\bin\\gdb.exe, setupCommands: [ { description: Enable pretty-printing for gdb, text: -enable-pretty-printing, ignoreFailures: true } ], preLaunchTask: build c file } ] }有几个细节值得说明。externalConsole我设置成true这样程序会弹出一个独立控制台窗口而不是在VSCode内置终端里显示。好处是scanf等输入函数在外部控制台表现稳定不会出现按回车没反应或输入被吞掉的情况。miDebuggerPath必须指向你自己的gdb.exe路径这一步很多人漏填导致调试任务启动失败。preLaunchTask的值要和tasks.json里的label保持一致这样每次按F5会自动先编译再调试。2.3 调试器的价值与简单用法很多新手把调试器当成“报错时才会用的工具”其实调试器是用来理解程序运行的工具尤其适合理解指针和内存。VSCode里只需要在行号左侧点一下就会生成红点断点然后按F5启动调试。程序运行到断点处会暂停左侧面板可以查看当前变量的值调用堆栈面板能看到函数按什么顺序被调用。右上角的“监视”窗口可以手动输入表达式比如输入*p就能直接查看指针p指向的内存内容。我第一次直观感受到调试器的价值是在分析数组越界问题时。当时的代码是这样的int arr[3] {1, 2, 3}; printf(%d\n, arr[3]);输出结果莫名其妙有时候像垃圾值有时候甚至不崩。用调试器在printf那一行打断点然后查看arr数组的内存视图发现arr[3]其实访问了数组中最后一个元素之后的内存区域那个值完全取决于栈上其他变量的存放情况。这就是未定义行为的典型表现代码能跑结果不可预知。没有调试器光靠肉眼看代码很难想明白这一层。3. 核心语法与难点拆解这些知识点值得反复琢磨3.1 指针C语言的钥匙指针是C语言里最核心也最劝退的概念但想跨过C语言的入门门槛指针绕不过去。我用一个快递柜的类比给自己解释指针内存是一排编号连续的快递柜每个柜子都有自己的编号。变量就是某个柜子里存放的东西而指针是一个记录柜子编号的便签。你不需要把东西搬出来只要知道便签上的编号就能直接打开对应柜子。指针的值是内存地址指针的“类型”则规定了通过这个地址能存取多少数据以及如何解释这些二进制数据。指针的基本操作就三个int a 42; int *p a; // p保存a的地址 printf(%d\n, *p); // *p访问a的值输出42是取地址运算符*是解引用运算符。声明int *p表示“p是一个指向int类型数据的指针”。很多新手会纠结写法int* p和int *p其实语义一样但为了和int *a, *b这种一行声明多个指针的场景保持一致我更推荐把星号靠近变量名写。指针和数组的关系是最容易混乱的地方。数组名在表达式中会退化为指向首元素的指针但这不代表数组名本身是变量。我在学习时写过一个验证程序int arr[] {10, 20, 30}; int *p arr; // 合法arr退化为指针 printf(%d\n, p[1]); // 输出20指针也支持下标访问 printf(%zu\n, sizeof(arr)); // 输出12arr仍是数组时sizeof是整块大小 printf(%zu\n, sizeof(p)); // 输出8p只是一个指针大小这段程序直观说明了数组名和指针在sizeof场景下的区别。数组名只有在作为sizeof运算符的参数时才会保留“数组”本性其他大多数情况下都会退化。指针作为函数参数的重要性用一个交换函数就能说清楚。如果直接传值void swap(int a, int b) { int temp a; a b; b temp; }这段代码在函数内部确实交换了a和b但主函数里变量的值根本没有变因为函数拿到的是副本。改成指针传参void swap(int *a, int *b) { int temp *a; *a *b; *b temp; }调用时写swap(x, y)函数通过地址直接修改了原变量的值。这就是地址传递的价值让函数能够回写结果而不是只返回单一值。学习指针时我给自己规定了几条警戒线不要访问空指针不要返回指向局部变量的指针不要越界访问动态分配的内存区域。这三条几乎覆盖了C语言最常见的崩溃原因。3.2 字符串与字符串函数边界问题C语言没有真正的字符串类型字符串本质上是字符数组并且约定以\0作为结束标志。正因为这个约定字符串处理函数大多依赖\0判断结束一旦越界没有结束符程序就会一直读下去直到崩溃。字符串函数里我踩坑最多的是gets现在标准里已经不建议用了。gets不会检查缓冲区大小用户输入超过数组容量就会造成缓冲区溢出算是经典安全漏洞。推荐改用fgetschar buf[100]; if (fgets(buf, sizeof(buf), stdin) ! NULL) { // fgets会读取包括换行符在内的内容换行符也会被存入buf }fgets留下的换行符经常引发bug。比如你读入一个字符串然后和另一个字符串做比较就会发现末尾莫名多出一个\n。处理办法是手动清除size_t len strlen(buf); if (len 0 buf[len - 1] \n) { buf[len - 1] \0; }字符串逆序是我练过很多遍的题目对应PTA上的“字符串逆序”题。实现方式可以用双指针一个指向开头一个指向末位交换两个位置然后两边往中间移动。void reverse(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--; } }这段代码简洁但隐含了一个前提字符串可写。如果传入的是字符串常量修改它会触发未定义行为。所以测试时建议用可修改的字符数组用char s[] hello定义不要用char *s hello。常用字符串函数里strlen返回不含\0的长度strcpy、strcat、strcmp、strstr等函数各有各的坑。strcpy和strcat同样不会检查目标缓冲区大小稳妥替代方案是strncpy和strncat但strncpy在src比n短时会在末尾补空字符行为也有一些微妙之处。最保险的办法是自己写代码时优先记录缓冲区剩余空间结合snprintf这类带长度限制的函数来操作。3.3 内存管理从malloc到内存泄漏C语言的内存分为几个区域栈区存放局部变量堆区存放动态分配的内存全局区存放全局变量和静态变量常量区存放字符串常量还有代码区存放机器指令。理解区域划分对排查内存错误非常关键。动态内存分配是C语言相对其他高级语言最直观的区别之一。需要动态创建大小不固定的数据容器时通常用mallocint *arr (int*)malloc(n * sizeof(int)); if (arr NULL) { printf(内存分配失败\n); return -1; }malloc不会初始化内存它只负责划出一块指定大小的区域。改这行为calloc时可以顺便置零但性能上会有损耗。realloc则是在原有内存块基础上调整大小扩容失败时返回NULL但原来的内存块没有被释放。动态分配有多频繁风险就有多高。我整理了一张踩坑对照表常见问题典型表现正确做法忘记free内存泄漏程序越跑越卡分配和释放成对出现谁分配谁释放重复free程序崩溃或内存管理器报错释放后立即将指针置为NULLfree之后继续使用读到垃圾数据偶发崩溃释放后不要再碰那块内存分配大小算错越界写覆盖相邻内存数据用sizeof计算元素大小再乘以元素个数我还在学习时给自己定了一条规矩每次malloc之后马上写对应的free注释说明这内存应该在什么条件下释放。这听起来有点小题大做但写复杂程序时只要有两条以上的逻辑分支内存释放就容易漏。后来我在代码里用了一个简单模式集中释放int *arr (int*)malloc(n * sizeof(int)); if (arr NULL) { goto fail; } // 主体逻辑 free(arr); return 0; fail: // 释放前面已经分配的其他资源 return -1; }虽然不是最优雅的写法但至少避免了中途漏释放的问题。动态内存的另外一个重要场景是“数组扩容”。C语言本身不提供动态数组但网上常见做法是用realloc实现。我的建议是当数组容量不够时一般不要每次只增加一个元素而是按倍数扩容比如从4扩展到8再扩展到16。这样可以显著减少realloc的调用次数避免数据反复拷贝带来的性能问题。3.4 函数与作用域的一些细节函数章节看着简单但里面藏着不少值得掰扯的细节。第一是值传递和地址传递的区别。所有函数参数默认都是值传递即使传的是指针指针本身的值地址也是拷贝过来的只不过拷贝的这个地址可以让你访问到同一个内存对象。这个认知能解释为什么在函数里修改指针本身往往不影响外部比如void init(int *arr, int n) { arr (int*)malloc(n * sizeof(int)); // 修改的是arr的副本外部指针仍然指向原来位置 }要真正改变指针本身需要传指针的指针也就是int **arr。第二是static关键字。static修饰局部变量时变量从栈区挪到了静态存储区生命周期变成整个程序运行期间但作用域仍然限定在函数内部。这个特性常用于计数器int count() { static int counter 0; counter; return counter; }每次调用count函数counter都保持上一次的值。static修饰全局函数时作用是将这个函数限制在本文件内使用这在多文件工程中很有用。第三是递归。递归函数是从理解函数栈帧开始的。每次函数调用都会在栈上分配一块空间用来保存局部变量和返回地址。递归调用其实就是不断压栈到达基准情形后开始逐层返回。如果忘记设置基准条件或者条件永远不满足就会栈溢出程序闪退。3.5 结构体、文件操作与预处理结构体是把相关数据绑在一起的方式。定义结构体和定义变量不要混在一起我习惯用typedef这样声明变量时不需要每次都加structtypedef struct Student { char name[50]; int age; double score; } Student;结构体变量之间可以直接赋值C语言会自动逐成员复制。但如果结构体里有指针字段这个复制复制的是指针的值两个结构体变量会指向同一块内存这不是“深拷贝”。想实现真正的独立复制需要手动给指针字段分配内存并复制内容。文件操作是C语言真实落地的关键场景。fopen打开文件返回一个FILE指针fclose关闭文件必须成对出现。我踩过的坑是使用相对路径时没注意当前工作目录结果程序运行时找不到文件。排查的办法是在代码里打印一下当前工作目录或者干脆用绝对路径先把程序跑通再改成相对路径。fprintf和fscanf是按格式化字符串读写文本文件的函数和printf/scanf用法几乎一致。读文件时最常用的是fgets按行读取配合sscanf可以处理CSV格式的数据。我做过一个小练习从文本里逐行读取“姓名 分数”这样的格式char line[100]; while (fgets(line, sizeof(line), fp) ! NULL) { char name[50]; int score; if (sscanf(line, %s %d, name, score) 2) { // 一行解析成功 } }这里检查sscanf的返回值很关键。如果文件格式不规范返回值不等于2就要跳过该行否则会用错误数据做后续计算。预处理指令在入门阶段容易被忽略但它是理解“编译”这件事的一个切口。#define可以定义常量或者带参数的宏。带参宏有一定陷阱比如#define SQUARE(x) x * x如果写SQUARE(1 2)展开结果是1 2 * 1 2计算结果完全不符合直觉。解决办法是给形参加括号#define SQUARE(x) ((x) * (x))条件编译#ifdef在大型项目里常用于针对不同平台或配置编译不同代码。头文件里为了防止重复包含经典写法是#ifndef MY_HEADER_H #define MY_HEADER_H // 头文件内容 #endif这串“头文件守卫”几乎是每个多文件工程必备的。4. 经典算法与练习从九九乘法表到PTA题目的实战4.1 九九乘法表格式化输出入门九九乘法表是C语言练习的经典开场题看起来简单却也考验printf格式化输出的基本功。基本思路是双重循环外层循环控制行数内层循环控制每行的列数。我一开始写出来的代码虽然能运行但输出对不齐for (int i 1; i 9; i) { for (int j 1; j i; j) { printf(%d*%d%d , j, i, i * j); } printf(\n); }问题是当乘积是两位数时和一位数时长度不同看起来参差不齐。改成指定最小宽度printf(%d*%d%-2d , j, i, i * j);%-2d表示左对齐占2个字符宽度这样整张表就像表格一样整齐。这道题告诉我一个通用经验格式化输出里输出对齐要与业务展示效果放在一起考虑不要只盯功能正确展示排版同样重要。4.2 冒泡排序与算法优化排序算法是C语言练习绕不开的内容冒泡排序则是排序里最直观的一个。原理是反复比较相邻元素顺序不对就交换每一轮把剩余未排序部分的最大值“冒”到末尾。我第一版代码如下void bubbleSort(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; } } } }这段代码的时间复杂度是O(n²)两层循环最差要跑完所有组合。但有一个优化空间如果某一轮内层循环完全没有发生交换说明数组已经有序后面就无需再扫了。加一个标志位void bubbleSort(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 temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; swapped 1; } } if (!swapped) { break; } } }这个优化在最好情况下原本就有序能让时间复杂度降到O(n)。这个案例也让我体会到写算法先写对再优化。过早优化容易引入错误先确保正确性再通过观测实际表现判断是否需要优化。4.3 日期计算把语法串起来PTA上有一道很经典的题目输入年、月、日计算这天是该年的第几天。这道题综合了scanf输入、条件判断、数组、函数定义非常适合作为阶段性练习。我给出的处理思路是先定一个每月天数的数组然后判断闰年最后累加int days[] {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; int total 0; if (isLeapYear(year) month 2) { total; } for (int i 1; i month; i) { total days[i]; } total day;几点细节很值得注意数组下标从0开始所以我给days数组下标0的位置补了一个0方便直接用month作为下标。闰年判断条件是“能被4整除但不能被100整除或者能被400整除”。这里有一个边界条件如果是闰年并且月份在2月之后则天数要额外加1。这道题本身不复杂但它把数组、判断、循环、函数全部串了一遍。做完之后我对“代码是数据加逻辑”这句话的理解深了一层。4.4 在OJ平台刷题的几点建议刷题和本地练习是两回事。本地代码只要自己测试通过输入输出可能“看起来”正确就过了但OJ平台有隐藏测试点格式、边界必须严格。我在PTA上最常犯的错误有这些第一是输出格式。比如要求两个数之间用空格分隔但我多输出了一个末尾空格评测系统直接报格式错误。解决方法是循环里判断是不是最后一个元素如果是那就不加空格。第二是输入缓冲区的问题。有些题目要求先读一个整数再读字符串如果中间用了scanf和fgets混用容易把上次输入遗留的换行符读进去。解决办法是在切换到fgets之前多读掉一个换行符scanf(%d, n); getchar(); // 吸收换行符 fgets(buf, sizeof(buf), stdin);第三是return 0的约定。C语言的main函数通常返回0表示正常退出有些OJ平台会检查退出码如果漏写会导致非零状态。刷题时我还有一个心得是先在本地把代码粘贴到编译器里编译一遍确保没有任何警告再提交。有些警告隐藏了类型转换或未初始化变量的问题一旦遇到特定输入就会暴露。比如%d和%f不匹配、int和long混用这类问题编译时就能发现。5. 踩坑记录与问题排查技巧5.1 编译运行时的典型错误这段时间我积累了数量可观的报错记录新手容易遇到的问题基本可以分为几类。第一类是环境类错误。“gcc不是内部或外部命令”几乎都是环境变量没配好或者改完环境变量没重启终端。这类问题排查起来很快但要提醒自己配置完环境变量后要新开一个终端窗口才生效。第二类是编译错误。常见的是结尾漏了分号、括号不匹配、头文件拼写错误。这类错误VSCode会直接标红线看错误信息就能修。有一个经验当编译器报错行列和实际位置对不上时优先查找上一行有没有漏掉分号很多错觉错误其实是上一行的问题。第三类是运行错误。例如程序一运行就闪退最常见的两个原因数组越界访问越界行为可能不会立刻崩溃但会覆盖其他内存区域。指针未初始化就使用也就是野指针问题。 这类错误在本地运行可能需要多跑几次才复现建议用调试器设置断点逐步看变量值和内存而不是盲目加printf。第四类是输出错误。程序运行正常但结果不对比如字符乱码。Windows中文环境下如果源码是UTF-8但控制台默认使用GBK编码printf输出中文就会乱码。解决办法有好几种我使用的是在VSCode里把编码调整为UTF-8同时参考系统的控制台编码来决定源码里是否使用中文输出。为了减少麻烦我有时候干脆把提示信息改成英文做题时输出中文也不会影响评测但本地看起来会舒服一些。5.2 代码“从入门到跑路”的排查方法遇到程序行为不对时我总结了一套自己的排查顺序先看编译命令确认编辑的是哪个源文件生成的是哪个exe文件。再看运行环境确认当前工作目录下是否有需要的文件。然后用调试器打断点逐行观察变量变化。如果仍然定位不了就把输入数据缩小到最小可复现用例例如把n改成3而不是1000。这套顺序的出发点是把问题分搜索范围。很多新手一上来就盯着代码逻辑看结果看了半天发现是工作目录下根本没有data.txt文件打开没有做判空处理程序继续往下跑自然得不到预期结果。调试时有一个小技巧非常实用在VSCode的“监视”窗口添加一个表达式比如arr[0]然后单步执行观察它什么时候发生突变。如果发现突变的时机和代码里某个操作对不上往往就是数组边界外写入了数据这类问题靠肉眼几乎不可能发现。5.3 学习资源与记忆方法关于资源菜鸟教程我仍然建议作为主线目录但最好不要只依赖它。翁恺老师的公开课视频在一些关键难点上讲解得非常清楚而且他善于用实际代码演示抽象概念适合在卡壳的时候看对应小节。PTA平台负责练习和检验它的题目按难度分级从基础语法到复杂算法都有可以一直做到进阶。做笔记的方法我推荐“二次复述法”。不要大段抄教程而是看完一节内容后关掉页面用自己的话把知识点讲出来讲不清楚的地方就是理解薄弱的地方。对于概念性强的知识点比如指针和内存还要配合画图。我专门找了一个小本子画内存布局每画一遍都能发现之前忽略的细节。有些知识点记不住我创造了联想法。比如函数栈帧我就想象成每个函数都占用一个“临时办公桌”函数返回后桌子被收走桌上文件不能继续用。这能解释为什么返回局部变量地址是危险的因为“办公桌”已经被收走了。6. 笔记后续还能怎么扩展6.1 数据结构方向C语言学完基础语法后最自然的延伸方向是数据结构。栈、队列、链表、树这些基础数据结构用C语言实现一遍对指针和内存管理的理解会有质的提升。我目前最推荐做的一个练手项目是单向链表。链表里的每一个节点都是动态内存分配的删除节点时需要先保存下一个节点的地址再free当前节点否则就会断链插入节点时要注意头节点可能变化需要返回新的头指针或者用二级指针。这个项目几乎把指针、结构体、动态内存、函数几个模块全部融合在一起做完一遍等于复训一整轮C语言重点。6.2 系统与嵌入式方向另一条扩展方向是系统编程和嵌入式开发。C语言是这些领域的底层语言文件读写、多线程、网络通信这些方向都需要扎实的C基础。在向这些方向扩展之前建议先学会makefile的简单编写。虽然IDE能帮你自动编译但看makefile能让你理解一个工程由哪些源文件组成、头文件和源文件之间怎么关联、增量编译是怎么实现的。菜鸟教程的makefile教程可以作为入门工具配合一个多文件的小项目亲手跑一遍效果很好。6.3 给后来学习者的几句实话C语言不是看几章语法就能掌握的编程语言它的难度不在于“写法陌生”而在于它把内存控制的权力完全交给了程序员。学习过程中一定会有大量时间花在排查bug、翻文档、重新理解概念上这些都是正常现象。我个人在实际学习中的体会是光看教程绝对不够一定要把每段示例代码自己重写一遍甚至改动几个参数看看结果有什么不同。有些概念当时怎么想都不通过几天再回头看就像突然开了窍这种顿悟往往发生在手敲代码、亲自调试的过程中。这套笔记我标为“暂已完结”其实也只是基础主线画了个句号后续我打算继续往数据结构和系统编程方向补完到时候再更新这套笔记。最后提醒一句遇到问题先自己调试半小时再问搜索引擎这个过程虽然辛苦但恰恰是提升最快的时候。
返回列表