C语言基础大全:从语法到文件操作的七块知识主线
简介:《C语言基础大全》PDF文档是一份面向编程初学者与在校学生的语法速查型学习资料,聚焦C语言入门阶段的知识梳理与查漏补缺,适合课前预习、课后复习以及笔试面试前的快速回顾。压缩包内共1个PDF文件,大小约68KB,篇幅集中、便于检索;正文按基本语法、数据类型、运算符、控制结构、函数、指针与文件操作七个模块依次展开,其中数据类型细分整型、字符型、单双精度浮点,以及枚举、数组、结构体、共用体等构造类型,运算符与控制结构逐项说明用法与常见写法。指针章节覆盖指针变量的定义初始化、取地址与取内容运算、指针同数组及函数的配合方式;文件操作部分则梳理fopen与fclose、fscanf与fprintf、fgets与fputs,以及fseek、ftell等常用接口的用途。目前已有901人学习下载,可当作课堂笔记的补充材料或知识框架参照。
1. C语言知识地图:七块内容连起来才是一条线
C语言这门课最容易让人误判的地方,是它看起来只有七块内容:基本语法、数据类型、运算符、控制结构、函数、指针、文件操作。很多人复习时按顺序从第一块背到最后一块,背完发现指针还是不会用,文件还是不敢碰。真正的主线其实是这样一条链:语法规定动作怎么写,类型规定数据占多少内存,运算符和控制结构规定流程怎么走,函数规定代码怎么分工,指针规定内存怎么被访问,文件规定数据怎么进出程序。前四块是地基,后两块是把地基连成完整程序的水泥。这份 C 语言基础大全按的正是这条线,适合两类人:正在系统学第一遍、需要一张地图的人,以及学过一遍、想快速查漏补缺的人。
2. 语法骨架与数据类型:从分号到大括号的最小闭环
把「基本语法」和「数据类型」拆成两章背,是很多人第一遍学 C 语言时踩的坑。这两块其实是一件事的两面:语法决定你能写出什么形状的代码,类型决定这些代码在内存里占多大、怎么解释。这一章先把语句、表达式、语句块这三个最小单位讲清楚,再把 int、char、float、double 的字节情况和 enum、数组、struct、union 四种构造类型摊开对照。
2.1 语句、表达式与语句块:三个最小单位
语句是 C 程序里最小的执行单位,标记是结尾的分号。一个语句可以写在一行里,也可以拆成多行写,还可以把好几个语句挤在同一行——编译器只认分号,不认换行。这也是新手最容易翻车的地方:格式化工具和肉眼都觉得「换行了就是一句」,编译器却还在等那个分号。
int a = 3, b = 4; /* 一条声明语句,用逗号一次声明两个变量 */ int sum = a + b; /* 表达式 a + b 求出值,再赋给 sum */ printf("%d\n", sum); /* 语句结尾必须有分号 */表达式和语句不是一回事。表达式是用来求出值的计算式,a + b、i++、x > y都是表达式,它们本身有值;语句则是以分号收尾的完整动作。区分这两者的意义在于:if (x = 5)里括号内是一个表达式,它的值是 5,恒为真,编译器的警告往往就藏在这种地方。
语句块用一对大括号把多条语句包起来,形成复合语句,同时开出一个新的作用域。块内声明的变量出了大括号就失效,这一点在嵌套循环和 if 分支里尤其要注意:很多人把循环体需要的变量声明在块内,出块后再去引用,编译直接报未声明。
{ int tmp = 10; /* tmp 只在这个块内有效 */ printf("%d\n", tmp); } /* 这里再写 printf("%d", tmp); 就编译不过了 */2.2 空格、注释与可读性:编译器眼里什么是空的
空格在 C 语言里的作用是分隔语法单位,防止intx这种连写被当成一个标识符。除字符串和字符常量内部之外,多余的空格对编译结果没有影响,但对读代码的人影响极大。写代码时用空格把运算符和操作数隔开,是最低成本的可读性投资。
注释有两种://管到行尾,/* */管到闭合符号。最容易忽略的是,注释在预处理阶段不是被「删掉」,而是被替换成一个空格。这个细节直接引出一个经典陷阱:int/**/x;会被替换成int x;,合法;但如果你写a/**/b想表达a和b两个记号紧挨着,替换后中间多了空格,语义可能就变了。
另一个硬性规则是:多行注释不能嵌套。/* 外层 /* 内层 */ 后面 */里,编译器看到第一个*/就认为注释结束,剩下的后面 */会变成真正的代码,报一堆莫名其妙的语法错误。调试时想「注释掉一大段」而那段里本来就有块注释,最容易踩这个。
提示:临时屏蔽大段代码时,优先用
#if 0 ... #endif,它不会和内部的块注释打架。
2.3 基本数据类型与字节账:int、char、float、double
C 语言的基本类型里,考试和面试最爱问的是「占几个字节」。原料给的答案是 int 通常 4 字节、char 1 字节、float 4 字节、double 8 字节,这个「通常」两个字很关键:C 标准只规定了最小范围和相对大小关系(比如sizeof(char)恒为 1,sizeof(short) <= sizeof(int) <= sizeof(long)),具体字节数由编译器和平台决定。所以复习时不要死背数字,要记住「用 sizeof 验证」这个动作。
| 类型 | 常见字节数 | 表示范围(常见平台) | 典型用途与坑 |
|---|---|---|---|
| char | 1 | -128 ~ 127 或 0 ~ 255 | 存字符;是否有符号由实现决定 |
| int | 4 | 约 ±21 亿 | 最常用;溢出是未定义行为 |
| float | 4 | 约 6~7 位有效数字 | 精度有限,比较相等要小心 |
| double | 8 | 约 15~16 位有效数字 | 默认浮点类型,数学函数多用它 |
| void | 无 | 无 | 表示无类型,不能声明普通变量 |
#include <stdio.h> int main(void) { printf("char=%zu int=%zu float=%zu double=%zu\n", sizeof(char), sizeof(int), sizeof(float), sizeof(double)); return 0; }这段代码用sizeof打印各类型的字节数,%zu是size_t类型的格式符,写%d在某些平台上会因为类型宽度不匹配而输出错值。把它在你自己的机器和课程要求的平台上各跑一次,比背十遍「int 占 4 个字节」有用得多。
浮点类型还要注意一点:float 大约只有 6 到 7 位有效十进制数字,所以0.1 + 0.2 == 0.3这种判断在浮点世界里基本不成立。复习时记住正确写法是判断差值绝对值小于一个很小的阈值,而不是直接用==。
注意:
sizeof是运算符不是函数,sizeof(int)的括号在跟类型名时必须写,跟变量时可以省略。
2.4 构造类型四件套:enum、数组、struct、union
构造类型是 C 语言里真正开始「组织数据」的地方。enum 把一组相关的整型常量起名字,数组把同类型元素排成一列,struct 把不同类型成员打包成一个整体,union 让多个成员共用同一段内存。前三个理解起来不难,union 是这四个里最容易被跳过、也最容易在考试里出坑的一个。
| 类型 | 内存特点 | 访问方式 | 复习重点 |
|---|---|---|---|
| enum | 本质是整型常量集合 | 直接用枚举名 | 枚举值是整型,可参与运算 |
| 数组 | 同类型元素连续存放 | 下标或指针 | 下标从 0 开始,越界不报错 |
| struct | 成员各占各的空间,可能有对齐填充 | .或-> | 大小 ≥ 各成员之和 |
| union | 所有成员共用一段内存 | .或-> | 大小 = 最大成员,写一个会覆盖另一个 |
#include <stdio.h> struct S { int i; char c; }; /* 成员各自占空间,中间可能有填充 */ union U { int i; char c; }; /* 成员共用一段内存,大小取最大成员 */ int main(void) { struct S s = {1, 'A'}; union U u; u.i = 0x41424344; /* 先写整个 int */ printf("sizeof struct=%zu, sizeof union=%zu\n", sizeof(struct S), sizeof(union U)); printf("u.c = %c\n", u.c); /* 再读 char,取到的是其中一部分字节 */ return 0; }在常见的 4 字节 int、默认对齐的平台上,sizeof(struct S)通常是 8:int 占 4,char 占 1,后面补 3 个字节凑对齐;而sizeof(union U)通常是 4,因为两个成员共用一段空间,大小取最大的那个。读u.c得到什么,取决于机器是大端还是小端——这就是 union 常被拿来考字节序的原因。复习时把「struct 各占各的、union 共用一段」这句话和这张 sizeof 结果绑在一起记,比单背定义牢靠。
2.5 void 与类型转换:那些不起眼但会扣分的地方
void 表示「没有类型」,它有三个用法:修饰函数返回值表示不返回,放在参数列表里表示不接收参数,以及声明void *通用指针。前两个是规范写法,第三个要注意:void *可以接任何类型的指针,但反过来把它赋给具体类型指针时需要显式转换,而且不能直接对它做解引用。
类型转换分隐式和显式。隐式转换会发生在赋值、函数传参、混合运算的时候,规则是「窄的往宽的方向靠」;显式转换就是强制类型转换。最容易出错的是浮点转整型:(int)3.99的结果是 3,直接截断而不是四舍五入;以及有符号和无符号混算,一个负数和一个无符号数比较,负数会被解释成很大的正数,条件判断直接反掉。
复习这一节的标准很简单:能说清void的三种用法,能判断一个混合表达式里各操作数最终被提升成什么类型,能解释(int)3.99为什么是 3。这三点在选择题里出现的频率相当高。
3. 运算符与控制结构:优先级、副作用与分支循环
运算符和控制结构这一块,知识点本身不难,难在「细节量」。运算符有五大类,优先级有十几级,控制结构只有顺序、选择、循环三种,但每一种都带着自己的坑:自增自减的副作用、逻辑运算的短路、switch 的穿透。这一章按「先分类、再抠副作用、最后落到三种结构」的顺序梳理。
3.1 五类运算符与优先级记忆法
原料把运算符分成五类:算术(+ - * / % ++ --)、赋值(=)、关系(< <= > >= == !=)、逻辑(&& || !)、三目(? :)。分类本身好记,真正要背的是优先级和结合方向,因为考试里a && b || c、*p++、x = y = z这类表达式几乎必出。
| 优先级 | 运算符 | 结合方向 | 记忆点 |
|---|---|---|---|
| 1 | ()[]->. | 左到右 | 括号和成员最高 |
| 2 | !~++--+-*&sizeof | 右到左 | 单目运算 |
| 3 | */% | 左到右 | 乘除取余 |
| 4 | +- | 左到右 | 加减 |
| 5 | <<>> | 左到右 | 移位 |
| 6 | <<=>>= | 左到右 | 大小关系 |
| 7 | ==!= | 左到右 | 相等判断 |
| 8 | &^| | 左到右 | 位运算,三者优先级不同层 |
| 9 | && | 左到右 | 逻辑与 |
| 10 | || | 左到右 | 逻辑或 |
| 11 | ?: | 右到左 | 三目 |
| 12 | =及复合赋值 | 右到左 | 赋值 |
| 13 | , | 左到右 | 逗号最低 |
表里第 8 行的位运算符实际分三层,&高于^高于|,为了记忆方便才合并成一行。复习时不用逐级背数字,抓住三句话就够:括号和单目最高,赋值和逗号最低,逻辑运算低于关系运算。a & 1 == 0之所以是坑,就是因为==比&优先级高,实际被解释成a & (1 == 0)。
3.2 自增自减与短路求值:副作用的两个高发区
++和--分前置和后置。前置是先加再用,后置是先用再加。这个规则本身不复杂,复杂的是把它们塞进同一个表达式里。
int i = 0; int a = i++; /* 后置:a 拿到 0,之后 i 变成 1 */ int b = ++i; /* 前置:i 先变成 2,b 拿到 2 */ /* 下面这种写法不要出现在任何作业和考试答卷里: i = i++; 同一表达式内多次修改同一变量,求值顺序未定义 */最后一行注释是重点。C 标准没有规定一个表达式内部各个子表达式的求值顺序,所以i = i++这种「同一变量在一个表达式里被修改多次」的写法属于未定义行为,换编译器、换优化等级,结果都可能不同。复习时记住一条铁律:一个表达式里对同一个变量只修改一次。
短路求值是逻辑运算符的另一个高发区。&&左边为假时右边不再计算,||左边为真时右边不再计算。这个特性常被用来写「先判空再解引用」的安全判断,但反过来,如果右侧表达式带副作用(比如自增、函数调用),短路会让它根本不执行。
int *p = NULL; if (p != NULL && *p > 0) { /* 左边为假,右边不会被求值,不会崩 */ printf("positive\n"); }这段代码能安全运行,靠的就是短路:指针为空时*p > 0压根不执行。复习时要把「短路是一种求值顺序保证」和「副作用可能不执行」这两面都记住,只记一面容易在判断题上失分。
3.3 顺序、选择、循环:三种控制结构的骨架
顺序结构就是按书写顺序执行,没有额外语法,考的是「你能否看懂代码的执行次序」。选择结构有if...else和switch两种,循环结构有for、while、do...while三种。这五种语句的语法都不长,重点是分清楚什么时候用哪种。
if...else适合区间判断和复杂条件,switch适合对一个整型表达式的离散取值做分支。for适合循环次数明确的场景,把初始化、条件、更新三件事集中在一行;while适合次数不确定、靠条件退出的场景;do...while的特点是循环体至少执行一次,因为条件判断放在末尾。
三种循环最容易混的是「循环变量在什么时候更新」。for的更新表达式在每轮循环体执行完之后才运行,while需要你自己在循环体里写更新语句,漏写就是死循环。复习时拿同一个求和任务,用三种循环各写一遍,比看定义有用。
3.4 switch 的穿透与 break:一个标号引发的连锁反应
switch的语法是switch (表达式)后面跟若干个case 常量:和一个可选的default:。两个硬性要求:括号里的表达式必须是整型(char 也算整型),每个 case 后面必须是整型常量表达式,不能是变量。
真正的高发点是穿透。case 在 C 语言里只是跳转标号,不是分支边界。匹配到某个 case 之后,程序会一直往下执行,直到遇到break或者 switch 结束。
switch (grade) { case 'A': puts("优秀"); /* 这里故意不写 break,会继续执行下面的 case 'B' */ case 'B': puts("良好"); break; /* 到这里才跳出 */ default: puts("待提高"); break; }这段代码里,如果 grade 是'A',会连续输出「优秀」和「良好」两行。有时候这种穿透是故意的(多个取值共享同一段处理逻辑),但更多时候是忘了写 break 导致的 bug。判断标准很简单:如果几个 case 之间共享逻辑,就把共用的代码放到最后一个 case 里,上面的 case 留空;如果没有共享意图,每个 case 都要有 break。
default的位置也不是必须放最后。它只是「所有 case 都不匹配时」的入口,放中间照样生效,而且如果它后面没有 break,也会往下穿透。复习时把「case 是标号不是边界」这句话记牢,穿透问题基本不会再错。
4. 函数、指针与文件操作:C语言的两个硬骨头加一个出口
函数、指针、文件操作这三块,是整份基础大全里分量最重的部分。函数解决的是「代码怎么分工」,指针解决的是「内存怎么被直接访问」,文件操作解决的是「程序的数据从哪来、到哪去」。前两个是 C 语言公认的难点,第三个是很多人学完之后一直不敢用的部分。这一章按「函数打底、指针攻坚、文件收口」的顺序梳理。
4.1 函数定义、调用与返回值:把代码切成可复用的块
一个函数定义包含四部分:函数类型(返回值类型)、函数名、形参列表、函数体。函数调用时通过函数名加实参列表触发,实参的值会按顺序传给形参。返回值通过return语句交回给调用者,return同时结束函数的执行。
#include <stdio.h> int add(int x, int y) { /* 返回类型 int,函数名 add,两个形参 */ return x + y; /* return 把结果交回调用处,并结束函数 */ } int main(void) { int sum = add(2, 3); /* 2 和 3 是实参,按顺序传给 x 和 y */ printf("%d\n", sum); return 0; }几个必须分清的点:形参是函数内部的局部变量,函数返回后失效;C 语言传参默认是值传递,函数内改形参不会影响外面的实参,想改外面的值必须传地址;返回类型为void的函数不返回值,但可以用return;提前结束。还有一个高频考点是函数声明与定义分离:调用发生在定义之前时,必须先有函数声明(原型),否则编译器无法检查参数类型。
main函数的参数void写法也常被问到:int main(void)表示不接收命令行参数,int main(int argc, char *argv[])表示接收。两种写法都合法,区别只在于是否需要处理命令行输入。
4.2 指针的定义、初始化与取地址解引用:先把概念钉死
指针变量存的是地址。定义一个指向 int 的指针写int *p;,取地址用&,解引用用*。这三个符号是整块内容的地基,把它们的含义记准,后面的指针与数组、指针与函数才不会乱。
int a = 10; int *p = NULL; /* 先置空,避免野指针 */ p = &a; /* p 保存 a 的地址 */ *p = 20; /* 解引用后赋值,a 变成 20 */ printf("%d %d\n", a, *p); /* 两个都输出 20 */这里的关键是分清「指针本身的值」和「指针指向的值」。p的值是一个地址,*p才是那个地址上存放的 int。&a取的是 a 的地址,*p是顺着地址找回去。指针不初始化就使用,它指向的地址是随机的,读写都可能直接让程序崩掉,这类问题在调试时表现为「莫名其妙的内存错误」,很难定位。
复习这一节,必须能徒手画出「变量 a 在内存里一个格子、指针 p 在另一个格子里、p 的格子里写着 a 的地址」这张图。图画得出来,指针的加减、指针做参数、指针和数组的关系都能顺下来;画不出来,后面全是背定义。
注意:指针声明里的
*是类型的一部分,int *p, q;里只有 p 是指针,q 是普通 int。一行声明多个指针时,每个都要带星号。
4.3 指针与数组、指针与函数:两组最常被拿来考的对应关系
数组和指针的关系,核心就一句话:数组名在大多数表达式里会退化成指向首元素的指针。所以a[i]和*(a + i)是等价的,&a[i]和a + i也是等价的。这个等价关系能解释很多看起来奇怪的写法,但也带来一个高频错误:数组名不是指针变量,不能被赋值,a = p这种写法编译不过。
int a[5] = {1, 2, 3, 4, 5}; int *p = a; /* 等价于 p = &a[0] */ printf("%d\n", *(p + 2)); /* 输出 3,等价于 a[2] */ printf("%d\n", p[2]); /* 指针也能用下标,同样输出 3 */指针与函数的关系,考的是两个名字很像的东西:函数指针和指针函数。区分方法是看名字先跟谁结合。int (*fp)(int, int);里(*fp)有括号,fp 先跟*结合,所以 fp 是函数指针,指向一个返回 int、接收两个 int 的函数。int *f(int, int);里 f 先跟()结合,所以 f 是函数,返回类型是int *,叫指针函数。
int add(int x, int y) { return x + y; } int (*fp)(int, int) = add; /* 函数指针指向 add */ int r = fp(2, 3); /* 通过函数指针调用,等价于 add(2, 3) */ printf("%d\n", r);函数指针的实际用途是「把函数当参数传」,比如写一个通用的排序函数,把比较规则作为函数指针传进去。复习时先记住「有括号先跟星号是指针,没括号先跟括号是函数」这条口诀,再拿两个声明各写一遍、各调用一次,基本就不会混。
4.4 文件操作:打开、读写、定位三件套
文件操作是整份大全里最贴近实际使用的一块。流程固定为三步:打开、读写、关闭。打开用fopen,关闭用fclose,读写有一组函数,定位有一组函数。原料提到的函数可以按用途归类记忆。
| 用途 | 函数 | 说明 |
|---|---|---|
| 打开 | fopen | 返回FILE *,失败返回 NULL |
| 关闭 | fclose | 释放文件句柄,成功返回 0 |
| 格式化读写 | fscanf/fprintf | 按格式串读写,类似 scanf/printf |
| 按行或字符串读写 | fgets/fputs | 读一行或写字符串,比 fscanf 安全 |
| 定位 | fseek | 移动文件位置指针 |
| 定位 | ftell | 返回当前位置偏移量 |
打开模式是另一个必考项:"r"只读,文件不存在直接失败;"w"只写,文件存在则清空、不存在则新建;"a"追加,写在末尾;带+表示可读可写;带b表示二进制模式。模式选错,要么读不到内容,要么把原文件清空,这两种情况在实验课上都很常见。
#include <stdio.h> int main(void) { FILE *fp = fopen("data.txt", "r"); if (fp == NULL) { /* 打开失败必须立刻判断 */ perror("fopen"); /* 打印失败原因 */ return 1; } char buf[128]; while (fgets(buf, sizeof buf, fp) != NULL) { /* 读到 NULL 表示结束 */ fputs(buf, stdout); } fclose(fp); /* 用完必须关闭 */ fp = NULL; /* 关闭后置空,防止误用 */ return 0; }这段代码里有两个必须养成的习惯:fopen之后立刻判空,fclose之后立刻把指针置空。不判空的话,文件不存在时fp是 NULL,后续所有读写都是对空指针操作,程序直接崩。不置空的话,关闭后误用这个指针属于悬空指针,问题更隐蔽。fgets的第二个参数是缓冲区大小,读取时会自动留一个位置给字符串结束符,所以最多读sizeof buf - 1个字符。
文件定位的典型用法是:先用fseek(fp, 0, SEEK_END)移到末尾,再用ftell(fp)拿到文件长度,最后fseek(fp, 0, SEEK_SET)回到开头。这个组合常被用来计算文件大小,考试里出现的频率不低。
5. 易错点清单:五个反复翻车的地方
这一章单独立出来,是因为下面这五个点几乎覆盖了 C 语言基础阶段绝大多数的「明明看懂了却写错」。每一条都按「易漏点是什么、为什么会漏、怎么补」三层写,复习时对着自查一遍,比重新翻一遍教材效率高。
5.1 分号与空语句:多一个分号就多一个空动作
易漏点:if (x > 0);后面多写了一个分号,或者for (i = 0; i < n; i++);后面多了一个分号。为什么漏:格式化之后,分号在行尾几乎看不见,而且加了分号编译器完全不报错,只是逻辑悄悄错了。if (x > 0);会被解释成「条件成立时执行一个空语句」,后面的代码永远执行;for后面加分号则会让循环体变成空语句,循环白跑一遍。
怎么补:养成「if、for、while的右括号后面如果直接换行,下一行必须是左大括号」的书写习惯,中间不给自己留写分号的机会。编译时把警告等级开高,很多编译器会对空语句给出提示。代码 review 时专门扫一遍所有右括号后面紧跟分号的位置。
5.2 整数除法与取余:/和%的两个隐含前提
易漏点:5 / 2的结果是 2 不是 2.5;%只能用于整型,写成5.0 % 2直接编译报错;负数取余的结果符号和被除数一致。为什么漏:数学课上除法就是除法,到了 C 语言里「两个整数相除结果还是整数」这条规则被默认隐藏了,教材往往只写一句「整数除法向零取整」,做题时很容易忘。
怎么补:想要小数结果,至少让一个操作数是浮点,写5 / 2.0或(double)5 / 2。取余只用于整型,浮点求余要用fmod函数。负数取余按「结果符号跟着被除数」记,-7 % 3是 -1。复习时把这三条写成一行便签贴在显示器边上,比反复翻书有效。
5.3 sizeof 与数组退化:函数里量不出数组长度
易漏点:在函数参数里对数组用sizeof求长度,结果永远是 4 或 8(指针大小),不是数组真实长度。为什么漏:数组名在传入函数时会退化成指针,函数内部看到的int arr[]实质就是int *arr,sizeof量的是指针而不是数组。这个错误在写「数组长度」工具函数时几乎人人踩过一次。
怎么补:数组长度必须在数组还没退化的地方算好,通常是在定义数组的那个作用域里用sizeof(arr) / sizeof(arr[0])求出来,再作为参数传给函数。函数参数里同时传数组和长度是 C 语言的标准写法,不要指望函数内部自己量。记住sizeof是编译期运算,它量的是类型的大小,不是运行时的实际元素个数。
5.4 struct 对齐与 union 共用内存:大小不等于成员之和
易漏点:以为sizeof(struct)就是把所有成员大小加起来;以为 union 可以同时保存所有成员的值。为什么漏:教材讲 struct 时常常先讲语法后讲内存,对齐规则被放在很后面;union 的定义只有一句「共用内存」,具体共用成什么样没展开。
怎么补:struct 的大小要把对齐填充算进去,规则是每个成员的偏移量必须是该成员大小的整数倍,结构体总大小必须是最大成员大小的整数倍。union 的大小等于最大成员的大小,写一个成员会覆盖其他成员的内容,读出来的值依赖字节序。复习时拿struct { int i; char c; }和union { int i; char c; }各跑一次 sizeof,把两个结果记在笔记里,比背规则直观。
5.5 指针未初始化与文件返回值不检查:两个「不报错但会崩」的习惯
易漏点:声明指针后直接解引用;fopen之后不判断返回值就读写。为什么漏:这两种错误在编译阶段都不报,程序可能在小数据量下跑得好好的,换一台机器、换一个输入就崩,属于典型的「黑匣子」问题。很多人调试半天,最后发现就是少了一行判空。
怎么补:指针声明时一律初始化为NULL,使用前判空;fopen之后立刻判空,失败就打印原因并返回。这两个动作写起来只多两行,但能省掉大量排查时间。养成习惯之后,可以进一步在fclose之后把文件指针置空,free之后把堆指针置空,形成一套固定的收尾动作。
6. 复习清单:按模块自查,把漏掉的补回来
复习到这一步,与其再从头翻一遍,不如按模块做一次自查。下面这张表把七块内容的必会点和自测问法列在一起,能答上来的打勾,答不上来的回到对应章节补。自测问法这一列是关键,它逼着你用「输出」的方式检验「输入」,比单纯重读有效得多。
| 模块 | 必会点 | 自测问法 |
|---|---|---|
| 基本语法 | 语句以分号结尾、表达式有值、语句块开作用域、注释不嵌套 | 能说出int/**/x;为什么合法吗 |
| 数据类型 | 四种基本类型的字节与范围、构造类型四件套的区别 | 能徒手写出 struct 和 union 的 sizeof 结果吗 |
| 运算符 | 五类运算符、优先级三句话、自增自减的副作用 | 能解释a & 1 == 0为什么不等于预期吗 |
| 控制结构 | 三种结构、switch 的穿透、do...while 至少执行一次 | 能说出 case 后面不写 break 会发生什么吗 |
| 函数 | 定义四要素、值传递、声明与定义分离 | 能在函数里改掉外面的变量吗,怎么改 |
| 指针 | 取地址与解引用、指针与数组的等价、函数指针与指针函数 | 能画出指针指向变量的内存图吗 |
| 文件操作 | fopen 模式、读写函数、fseek/ftell 定位 | 能写出「读文件并逐行输出」的完整代码吗 |
自查的时候还有个技巧:不要只判断「这个知识点我背过没有」,要判断「这个知识点我能不能在一分钟内写出一段能跑的代码」。背过和写出来之间差着一次编译和一次调试,而恰恰是编译和调试的过程,会把那些模糊的地方暴露出来。
#include <stdio.h> int main(void) { int arr[5] = {1, 2, 3, 4, 5}; int len = sizeof(arr) / sizeof(arr[0]); /* 长度必须在数组未退化时算 */ int sum = 0; for (int i = 0; i < len; i++) { /* 循环变量就地声明,作用域限定在循环内 */ sum += arr[i]; } printf("len=%d sum=%d\n", len, sum); /* 期望输出 len=5 sum=15 */ return 0; }上面这段代码建议作为最后一道自测题:它同时用到了数组、sizeof、循环、函数返回值这几块内容。如果你能不看提示写出等价代码,并且能解释len为什么不能在别的函数里算,那基础部分基本就过关了。写不出来或者写出来结果不对,回到第 2 章和第 5.3 节,把数组退化和sizeof的关系再捋一遍。
我带过的一个习惯是:每次复习完一个模块,合上资料,用空白文件从零写一个能跑的小程序,只允许查函数原型,不允许抄代码。第一次这么做的时候,我以为自己指针已经懂了,结果在「把数组传进函数再改元素」这一步卡了二十分钟,最后发现是把int *和int的参数类型写混了。从那以后我每次复习 C 语言都强制走一遍「合上资料手写一遍」的流程,宁可写得慢,也不留那种「看懂了但写不出」的侥幸。希望这份梳理能帮到你,把基础这块地基打扎实。
本文还有配套的精品资源,点击获取