我在整理自己的C语言学习笔记时第1到第6章这一摞翻得最慢。不是因为内容多而是边翻边冒冷汗——当初踩过的坑、绕过的弯、以为懂了结果一做题就现形的知识点全在这里。这份C语言学习笔记全书合集记录的是从零基础到能写出几百行小项目的那个阶段涉及数据类型、运算符、分支循环、数组、函数这六大块。它适合正在啃C语言教材的初学者也适合已经学完但想回头把地基补扎实的人。说句实话这六章就是C语言的地基部分后头那些指针、结构体、链表全是在这六章的骨架上长出来的地基歪了上面的楼怎么盖都是危房。其实很多人学C语言卡住恰恰不是卡在后面的指针而是卡在前面这几章——数据类型搞不清、运算符优先级记混、循环一嵌套就大脑宕机、函数参数传进去就改不动外部变量。这些问题看起来小但每一个都会在后头放大成“为什么我的程序跑不出预期结果”的玄学问题。这篇我就把这套笔记里最有价值的部分拆开聊一聊顺便把我反复强调的注意事项和踩坑经验一并写出来。1. 数据类型与运算符——地基里的第一道裂缝1.1 数据类型不是背概念而是换一种视角看内存笔记第1章重点讲的是数据类型、常量与变量、格式化输入输出。很多初学者把这些当“概念”背背完了写代码还是不知道怎么选类型。这里我要特别强调一个观点数据类型本质上是在告诉编译器“我要用几个字节来存这个数以及怎么解释这些字节”。你定义了一个int在常见平台上它占4个字节能表示的整数范围大约是-2147483648到2147483647一旦超出可能会溢出绕回变成负数或错误值。这个理解必须跟随整个C语言学习过程因为它直接关系到后面函数返回值、数组下标、结构体内存对齐等一切内容。笔记里我专门留了一页做“类型体检”用sizeof运算符打印每个类型在当前编译器环境下的字节数比如sizeof(int)、sizeof(float)、sizeof(double)、sizeof(char)。为什么要做这个实验因为C语言标准对不同类型的大小只给了下限不同平台、不同编译器做出来的结果可能不一样。你在这台机器上int是4字节换一个嵌入式小环境可能就是2字节。知道这一点至少你不会在跨平台编译时因为“我以为int就是4字节”而写出有隐患的代码。1.2 运算符优先级、自增自减、短路求值三大拦路虎第2章通常是运算符与表达式这一章的坑密度相当高。我总结下来坑主要来自三个方面优先级、自增自减、短路求值。先讲优先级。int result a b 2;这行的意思是(a b) 2还是a (b 2)很多人会答错因为加法优先级高于左移所以实际上相当于(a b) 2。类似这种容易混的优先级组合在真实项目里经常因为“少写一个括号”变成bug。我笔记里的原则是常见的优先级记住其他的统统用括号表达意图。写if ((a b) 0)时括号是给人看的也是给你自己三个月后回看的。我见过太多人在综合题里因为没加括号让位运算和比较运算打架整个逻辑变形。再说自增自减。i 和 i 的区别几乎每个初学者都倒背如流但放到表达式里就抓瞎。比如下面这个例子int x 5; printf(%d %d, x, x);这种题目实际运行结果在不同编译器下都可能不一样因为函数参数的求值顺序是未指定的。我的建议很简单不要在复杂表达式里用自增自减更不要在同一个表达式里对同一个变量多次自增自减。这不是胆小是工程素养谁也不想靠猜来写代码。最后说短路求值。a b或a || b这种表达式中如果左边已经能确定整个表达式的真假右边就不会执行。这个特性用好了很强大比如if (p ! NULL p-value 0)左边为假时右边根本不执行天然防空指针。但反过来如果你在右边写了x之类的副作用语句并且指望它一定执行那就会踩空。1.3 这一章笔记里最值得反复看的一组对比我在这一章笔记里记了两组对比至今都觉得很关键一组是float和double的精度差异另一组是算术类型转换的方向。float只有大约6到7位有效数字你对两个float做大量累加运算时误差会一点一点放大。用float存金额、存计数器这类场景真的不是C语言不严谨是你在选型的时候就埋了雷。类型转换方面int和float混合运算时int会被隐式转换成float再参与计算反过来把float赋给int小数部分会被直接截断。很多人第一次做平均值时写int avg sum / count;发现结果总是整数就是因为两个整数相除的结果也是整数小数部分直接被切掉了。这一组对比我建议初学者抄进自己的笔记里后面做算法题、做小项目天天碰得到。2. 分支与循环——从“顺序执行”到“控制流”的思维升级2.1 悬挂else一个花括号就能解决的经典话题第3章讲选择结构if-else和switch是主角。if-else最大的坑是悬挂else问题。C语言规定else总是和最近的、未匹配的if配对所以写嵌套条件时如果省略了花括号代码缩进完全可能欺骗你的眼睛。比如if (a 0) if (b 0) printf(both positive); else printf(a not positive);这段代码从缩进看else似乎对应第一个if实际上它对应的是第二个if因为编译器只看语法配对不看缩进。要解决这个问题最可靠的手段就是无论分支里有多少条语句都写上花括号。我甚至见过一些项目直接定下强制规范所有if-else一律带花括号哪怕只有一行。这个习惯越早养成越好它真的能帮你省下无数个调试到崩溃的夜晚。2.2 switch的穿透不是bug是特性switch里另一个绕不开的话题是fall-through也就是穿透。每个case后面如果不写break程序会继续执行下一个case的代码直到遇到break或整个switch结束。很多初学者第一次遇到时觉得这是BUG其实C语言允许这样写是为了让多个case共享同一段逻辑。比如希望case 1、case 2、case 3走同一个处理分支时可以故意让它们“叠”在一起。不过我的建议是如果你刻意使用穿透一定要在代码里写清楚注释比如/* fall through */不然同事或者未来的你都会以为你漏了break。因为这种写法太容易被误读了很多公司代码规范里干脆规定每个case都必须以break、return或continue结尾从根上避免穿透。2.3 for、while、do-while三种循环怎么选第4章的循环结构几乎是初学者第一次感受到“逻辑思维”的分水岭。for、while、do-while三者的区别不只在语法层面更在使用场景。for适合“我已经知道要循环多少次”的情况比如遍历一个确定长度的数组while适合“循环次数不固定需要靠条件判断”的情况比如读取文件直到末尾do-while则保证循环体至少执行一次比如做用户菜单时至少得先显示一次菜单用户才能做选择。我笔记里记了一个特别典型的错误用while实现遍历时忘记在循环体内更新循环变量结果程序死循环像卡死了一样不退出。排查这种问题的方法其实很简单——检查循环变量在循环体中有没有被更新循环条件最终会不会变成假。这两点只要确认死循环问题基本可以定位。2.4 循环嵌套里的break和continue不是你想跳就能跳break和continue的语义需要认真抠一遍。break是退出“当前这一层”循环不是退出所有外层循环。在双重循环里内层break外层仍然会继续执行下去。continue则是跳过本次循环的剩余部分直接进入下一次迭代注意它跳过的只是本次循环体而不是整个循环。我见过一道很经典的作业题要求打印某个范围内的素数。很多同学在使用break时没有区分“当前层循环”和“函数执行”导致一个判断标志位被提前置位结果无论怎么改都不对。笔记里我把break和continue的影响范围整理成了一个小小的层级图分别标注“作用于哪一层循环”后来每次写循环嵌套我都会先想想这张图。你也可以按自己的理解画一张类似的理清关系之后嵌套循环就没那么可怕了。3. 数组与字符串——C语言里第一个真正的“容器”3.1 下标从0开始不是故意为难你第5章进入数组这是C语言里第一个让人感受到“数据管理”概念的地方。int a[5]表示申请5个连续的int型存储单元下标是0到4不存在a[5]。下标从0开始这件事对初学者来说需要一点时间适应。我用一个类比帮人理解把数组名a当成一栋楼的门牌号起点a[0]是0号房间a[1]是1号房间a[4]是4号房间这栋楼总共5个房间你想进5号房间只能从屋顶跳下去。这个类比有点夸张但能让人一下记住越界的危险感。3.2 越界不报错才是最危险的地方数组越界在C语言里属于未定义行为编译器不一定报错程序也可能“正常”运行也可能随机崩溃更可能悄悄改掉别的变量的值。这是初学者最容易忽略的一点你写a[5]它不一定立刻崩但它在内存里改的可能是紧挨着的另一个变量或者破坏了栈上的什么东西后果延后到不知道哪一行才暴露。排查这种问题极其痛苦。我在笔记里把“越界”列为本阶段第一防线给出的方法很朴素遍历时统一用i 数组长度的写法别写成i 数组长度每次声明完数组先写一个for循环把整体访问范围走一遍确认边界对不对。还有一个练习方法故意让下标越界一次观察程序各种稀奇古怪的输出亲身体会“没报错才是最吓人的”。提示不要因为“没报错”就认为越界访问是安全的。在C语言这里没报错往往只意味着问题还没轮到你头上。3.3 字符串字符数组以及那个必须存在的 \0字符串在C语言中本质上是char数组但比普通char数组多了一个特殊约定以\0也就是ASCII码为0的空字符作为结束标志。这就是为什么char str[10] hello;看起来只有5个字符实际数组里要占用6个字节才算完整保存了这个字符串。不理解这一点经典错误就来了用strlen计算字符串长度得到5再用sizeof计算得到6甚至10取决于数组容量。strlen是数到\0为止sizeof是统计整个数组占用的字节数两者的职责完全不一样。我整理这一节笔记时特意写了一个小实验分别打印strlen(str)和sizeof(str)看到结果的那一刻才真正记住二者的区别。函数/运算符计算对象结果含义典型坑strlen(str)字符串内容到\0之前的字符个数忘记字符串结尾有\0sizeof(str)数组本身整个数组占用的字节数以为和strlen结果一样3.4 二维数组它本质上还是一维二维数组这个概念让不少人绕晕其实拆开看并不玄。int a[3][4]本质上是“3个元素每个元素又是一个包含4个int的数组”在内存里是按行连续存储的。连续存储意味着a[0][0]和a[0][1]在内存里紧挨着a[0][3]和a[1][0]也是紧挨着的。这带来两个结果第一遍历时按行访问比按列访问更顺因为内存布局是行优先的第二C语言里其实没有真正的多维数组只有“数组的数组”。我当初理解二维数组时做过一个地址实验printf(%p, a[0][0])、printf(%p, a[0])、printf(%p, a)三个打印结果在数值上完全一样都是整个二维数组的起始地址。这种“同一个地址不同名字”的现象一开始确实让人迷惑但理解之后对后面指针专题和动态内存分配的帮助是立竿见影的。4. 函数——从“写代码”到“组织代码”的分水岭4.1 声明、定义、调用三者的顺序能绕晕不少人第6章讲函数这是很多人第一次从“写一坨代码”过渡到“组织代码”。函数声明是告诉编译器“有一个函数长这样”函数定义是真正实现它函数调用则是用起来。如果函数定义写在调用后面编译器会默认帮你推断一个返回类型为int的函数原型一旦真实返回类型不是int就会产生原型冲突报出一些让人摸不着头脑的警告。最稳妥的做法就是在文件开头统一声明所有函数或者在头文件里写好函数原型。这个细节我在笔记里记了不下三遍因为每次调试“implicit declaration of function”这类问题时都会想回去翻一遍。养成“先声明后使用”的习惯基本可以避开这一类问题。4.2 值传递为什么在函数里改了参数外面的变量没变这个知识点几乎能把初学者集体搞破防。写一个void swap(int a, int b)用来交换参数回到main函数一打印两个变量纹丝不动。原因很简单C语言的函数参数传递是值传递形参是实参的一份拷贝你在函数里修改形参影响的只是拷贝不是原变量。只有理解了这一点后面学指针时才会明白“传地址”的本质是借助指针间接修改外部数据而不是“把变量本身传进去”。很多初学者在这个地方开始怀疑人生。其实只需要接受一个模型就够了变量名对应一块内存空间值传递时编译器为你复制了一份数据放进函数全新的空间里函数结束时这片空间就被回收。想改外面的变量要么用返回值接收要么传指针要么用外部变量这是第6章笔记最后一部分的内容也是为下一阶段的指针内容做的铺垫。4.3 变量的作用域和生命周期局部变量在函数结束就没了与函数密不可分的是变量的作用域和存储期。局部变量定义在函数内部从定义处开始生效到函数返回时结束全局变量定义在函数外部从程序开始运行到结束全程可见。这个知识点看起来简单但配合递归函数就会变得烧脑。每次递归调用都会创建一套全新的局部变量互不干扰只是参数的值逐层变化。我当初写递归时老觉得变量会被“复用”实际上每次调用都有独立的栈帧各过各的日子。理解这一点之后递归里的参数传递和返回过程才算真正看懂。4.4 递归优雅的写法昂贵的代价第6章的递归函数一般以阶乘、斐波那契数列作为例题。递归写法非常优雅但代价是函数调用本身有开销而且递归深度过大会导致栈溢出。我用斐波那契数列做过对比递归实现的Fibonacci(40)跑起来要肉眼可见地卡一下而用循环叠加的方式则瞬间完成。这不是说要完全不用递归而是要有意识递归适合“问题天然具有递归结构”的场景比如树的遍历、分治算法在能用迭代解决时优先考虑迭代。我笔记里写过一句批注递归是一种思维训练但工程上要用得有节制。这个判断放在现在的大项目里依然成立。5. 六章之后笔记整理与复盘方法才是真正的“内功”5.1 我的笔记结构知识点、代码示例、踩坑记录三栏并行不少学C语言的朋友问过我笔记到底应该怎么记是不是抄板书就行。我的经验是像C语言这种偏实践的科目笔记最好分成三块左边记知识点中间记代码示例右边记踩过的坑。这里的“坑”必须是自己的错误不是书上的错误。比如我写错过循环边界输出多出一行我就会把错误的输出和正确的输出都贴在一起再写一行“为什么错”。这样一本笔记复习时看的不只是知识点而是“我过去会在哪里翻车”效果完全不同。知识点的东西书上有个人化的踩坑记录书上没有这才是一本笔记真正值钱的地方。5.2 把例题当实验而不是当阅读材料整理第5、6章笔记时我最大的体会是例题必须亲手敲一遍、改一遍、跑一遍。只“看懂”和“能写出来”之间隔着一条巨大的鸿沟。你可以拿书上的例题先按原样敲一遍跑通后改一个参数比如把数组长度从5改成6观察会发生什么再把for循环改成while循环看逻辑怎么变。这种玩法远比重复做十道类似的题更能加深理解。很多初学者怕改坏代码其实改坏代码正是学习的一部分坏代码产生的报错信息甚至比好代码更值钱。报错信息会告诉你编译器在挑剔什么、运行时在不满什么这些都是实战中真正需要敏感的东西。5.3 学完前六章你应该能独立做哪些事这份合集到第六章结束其实正好完成了一个完整的阶段。学完之后你应该能独立写出一个包含菜单选择、数据输入、数组存储、函数封装的小程序比如一个班级成绩管理系统用数组存成绩用循环控制菜单用函数实现录入、统计、排序的封装。不要小看这个目标它意味着你可以把前六章的所有语法点串成一条线而不是零散地“学过了”。如果做到这一步再去学指针、结构体、链表你会发现之前的地基足够扎实新知识点只是在往上加楼。我最后再分享一个自己的小习惯每学完一章我在本子第一页画一棵简易的“知识树”只写关键词和它们之间的关系不写长句子。这棵树的树枝越来越密到第六章结束时你能看到整个C语言基础框架就铺在那张纸上这比任何教科书目录都更有说服力。