郝斌C语言笔记:指针、内存与文件操作实战解析 简介这份郝斌C语言详细笔记附源码面向零基础及初学编程的读者以doc文档形式系统梳理C语言入门所需的核心知识帮助解决语法零散、概念不清、缺乏规范意识等常见问题。资源包共1个doc文件约6.04MB内容涵盖C语言发展历程与特点、结构化与面向对象语言的分工、Windows与Linux/Unix内核的语言选择、关键字与程序格式、CPU与内存硬盘的关系、数据类型与变量定义、进制与常量存储、代码规范化等模块并附有配套源码便于对照练习。笔记延续郝斌课堂边讲边敲代码的风格对重点知识反复拆解同时给出学习目标与排错调试思路适合作为入门教程逐章研读。目前已有121人学习可作为计算机专业学生或转行自学者的起步参考。1. 一份被低估的 C 语言笔记从指针到文件操作它把黑匣子拆开了很多人学 C 语言卡在指针和内存那一关不是脑子不够用是手头那份资料把关键步骤跳过去了。这份《郝斌 C 语言详细笔记附源码》的 doc 文档走的是另一条路它把每个语法点拆成“为什么需要它 → 内存里长什么样 → 代码怎么写 → 编译运行看到什么”四步指针、数组、结构体、文件操作这些容易翻车的地方都配了可运行的源码。适合两类人一是刚学完语法但一写就报段错误的在校生二是工作几年后想回头补底层基础的开发者。它不教你花哨技巧只帮你把 C 语言这口锅的每个零件摆到台面上看清楚。下面我按自己拆包复现的顺序把这份笔记的用法、参数设置和踩坑点讲透。2. 笔记结构与源码组织先看清目录再动手2.1 文档的章节骨架与源码对应关系拿到这份 doc 之后别急着从头读到尾。我一般先扫一遍目录把章节分成三块基础语法变量、运算符、流程控制、核心难点指针、数组、函数、结构体、进阶应用文件操作、动态内存、链表。这份笔记的章节顺序基本遵循这个逻辑但源码文件是按知识点独立命名的比如pointer_basic.c、struct_demo.c、file_rw.c。你需要做的是把 doc 里的章节标题和源码文件名对上一张映射表否则读到后面会找不到对应代码。常见做法是建一个本地目录把 doc 和源码包解压到同一级然后按下面的结构整理c_notes/ ├── 郝斌C语言笔记.doc ├── src/ │ ├── 01_basic/ │ ├── 02_pointer/ │ ├── 03_struct/ │ └── 04_file/ └── build/这样做的目的是让“读笔记”和“跑代码”两个动作不互相干扰。笔记里讲到一个知识点你直接去src下找对应文件编译不用在文档里翻来翻去。2.2 编译环境的选择与最小验证这份笔记的源码是标准 C没有依赖任何第三方库所以编译环境很宽松。Windows 下用 MinGW 的 gccLinux 和 macOS 自带 gcc 或 clang 都行。我习惯用一条命令先验证环境是否可用# 检查编译器版本确认支持 C99 及以上 gcc --version # 编译一个最简单的测试文件验证工具链完整 echo #include stdio.h int main(){ printf(env ok\n); return 0; } /tmp/env_test.c gcc -stdc99 -Wall -o /tmp/env_test /tmp/env_test.c /tmp/env_test逻辑说明第一行确认 gcc 存在且版本不过老第二段用-stdc99指定标准-Wall打开常用警告这两个参数在后面编译笔记源码时建议一直带着。参数-o指定输出文件名不写的话默认生成a.out在 Windows 下是a.exe容易和别的文件混淆。如果这条命令输出env ok说明环境没问题。如果报gcc: command not foundWindows 用户检查 MinGW 的bin目录有没有加到 PATHLinux 用户装build-essentialmacOS 用户装 Xcode Command Line Tools。提示笔记里部分代码用了scanf和gets在较新的编译器上gets会直接报错。遇到这种情况把gets换成fgets后面讲字符串处理时会具体说怎么改。3. 指针与内存模型笔记里最值得反复看的部分3.1 指针章节的讲解逻辑与代码验证指针是这份笔记的重头戏也是大多数人翻车的地方。笔记没有一上来就讲int *p的语法而是先画了一张内存布局图把变量名、内存地址、存储的值三列并排让你看到“变量名是给人看的地址是给机器看的”。这个切入角度比直接讲语法有效得多。我建议你在读这一章时把笔记里的每个例子都亲手敲一遍并且用printf把地址打出来。比如下面这段验证代码#include stdio.h int main() { int a 10; int *p a; // p 指向 a 的地址 int **pp p; // pp 指向 p 的地址 // 打印变量值、变量地址、指针值、指针指向的值 printf(a %d\n, a); printf(a %p\n, (void*)a); printf(p %p\n, (void*)p); printf(*p %d\n, *p); printf(pp %p\n, (void*)pp); printf(**pp %d\n, **pp); *p 20; // 通过指针修改 a 的值 printf(after *p20, a %d\n, a); return 0; }逻辑说明这段代码把“变量 → 指针 → 二级指针”的链条完整走了一遍。a取地址*p解引用**pp解两次引用。参数方面%p用来打印指针标准要求传void*所以加了强制转换不加在某些平台上会报警告。编译运行gcc -stdc99 -Wall -o pointer_demo pointer_demo.c ./pointer_demo你会看到a的地址和p的值是同一个数*p和a的值相同。改*p之后a也跟着变这说明指针不是“复制了一份数据”而是直接操作原地址。笔记里反复强调这一点但只有自己跑一遍才会真正记住。3.2 数组与指针的关系笔记里没明说但必须知道的边界笔记在讲数组时提到“数组名在大多数情况下退化成指针”但没有展开说哪些情况不退化成指针。这是实际写代码时最容易踩的坑之一。我补充两个关键点第一sizeof(arr)和sizeof(ptr)结果完全不同。数组名在sizeof里不退化成指针所以sizeof(arr)是整个数组的字节数而指针变量本身只占 4 或 8 字节。第二数组名不能自增指针可以。arr是非法表达式p合法。下面这段代码可以帮你验证#include stdio.h void func(int arr[]) { // 在函数参数里arr 已经退化成指针 printf(inside func: sizeof(arr) %zu\n, sizeof(arr)); } int main() { int arr[5] {1, 2, 3, 4, 5}; int *p arr; printf(in main: sizeof(arr) %zu\n, sizeof(arr)); printf(in main: sizeof(p) %zu\n, sizeof(p)); func(arr); // 用指针遍历数组 for (int i 0; i 5; i) { printf(p[%d] %d, *(p%d) %d\n, i, p[i], i, *(p i)); } return 0; }逻辑说明sizeof(arr)在main里输出 20假设int占 4 字节在func里输出 864 位系统指针大小。这说明数组作为函数参数传递时传的是首地址函数内部拿不到数组长度。所以笔记里所有涉及数组传参的例子都额外传了一个len参数这不是啰嗦是必须的。参数方面%zu是size_t的格式符sizeof返回的就是size_t。用%d在某些平台上会报警告建议统一用%zu。注意如果你在 32 位环境编译指针大小是 4sizeof(p)输出 4。不要拿这个数字去判断代码对错要看它和sizeof(arr)是否一致。4. 结构体与文件操作把数据存下来才算完整4.1 结构体对齐与内存布局的验证方法笔记在结构体这一章讲了定义、初始化、指针访问但内存对齐只提了一句“编译器会自动对齐”。这句话背后藏着一个很容易翻车的地方你以为结构体占 12 字节实际可能是 16 字节。如果你要把结构体直接写入文件或者通过网络传输对齐问题会导致数据错位。验证方法很简单用sizeof打印每个结构体的大小再和成员大小之和对一下#include stdio.h struct A { char c; // 1 字节 int i; // 4 字节 char d; // 1 字节 }; struct B { int i; // 4 字节 char c; // 1 字节 char d; // 1 字节 }; int main() { printf(sizeof(struct A) %zu\n, sizeof(struct A)); printf(sizeof(struct B) %zu\n, sizeof(struct B)); return 0; }逻辑说明struct A的成员按char → int → char排列为了对齐int第一个char后面会填充 3 字节最后一个char后面再填充 3 字节总共 12 字节。struct B把int放前面两个char放后面总共 8 字节。成员一样顺序不同大小差 4 字节。这个知识点在实际项目里很关键。如果你用fwrite把结构体写入文件换一个编译器或者换一个平台读出来的数据可能完全乱掉。常见做法是要么在结构体定义时显式加#pragma pack(1)取消对齐要么逐字段序列化成文本格式再写文件。笔记里用的是fwrite直接写二进制读的时候用fread读回来在同一台机器上没问题跨平台就要注意。4.2 文件读写的最小可用模板笔记的文件操作章节覆盖了fopen、fclose、fread、fwrite、fprintf、fscanf但例子比较分散。我把它整理成一个最小可用的读写模板你可以直接拿去改#include stdio.h #include stdlib.h typedef struct { int id; char name[32]; float score; } Student; int main() { const char *filename students.dat; Student stu {1001, A同学, 92.5f}; // 写入二进制文件 FILE *fp fopen(filename, wb); if (fp NULL) { perror(fopen for write); return 1; } size_t written fwrite(stu, sizeof(Student), 1, fp); printf(written %zu\n, written); fclose(fp); // 读取二进制文件 Student read_stu; fp fopen(filename, rb); if (fp NULL) { perror(fopen for read); return 1; } size_t read_count fread(read_stu, sizeof(Student), 1, fp); printf(read_count %zu\n, read_count); printf(id%d, name%s, score%.1f\n, read_stu.id, read_stu.name, read_stu.score); fclose(fp); return 0; }逻辑说明fopen的wb和rb分别表示二进制写和二进制读不加b在 Windows 下会把\n转成\r\n导致文件大小和内容对不上。fwrite返回实际写入的元素个数fread返回实际读到的元素个数这两个返回值必须检查否则文件打开失败或者读到一半出错你都不知道。参数方面sizeof(Student)是每个元素的大小第三个参数1是元素个数。如果你想一次写一个数组把第三个参数改成数组长度第一个参数传数组首地址。提示perror会打印你传入的字符串加上系统错误信息比单纯printf(error)有用得多。文件操作失败时第一时间看这个输出。5. 避坑与排查这份笔记跑起来最容易翻车的五个地方5.1 现象编译报错undefined reference to xxx原因笔记里有些例子把函数定义写在main后面但没有在main前面声明。C 编译器从上往下读遇到函数调用时如果没见过声明就默认返回int链接时找不到实现就报这个错。解决在main之前加函数声明或者把函数定义挪到main前面。更规范的做法是写一个.h文件放声明.c文件放实现。笔记里的小例子可以直接在文件顶部加一行void my_func(int);解决。5.2 现象scanf读字符串时跳过输入原因scanf(%d, n)读完整数后回车符留在了输入缓冲区。下一次scanf(%s, buf)或者fgets会直接读到这个回车看起来像“跳过了输入”。解决在两次读取之间清空缓冲区。常见做法是用getchar()循环吃掉剩余字符或者用scanf( %s, buf)在格式符前加一个空格让scanf自动跳过空白字符。笔记里部分例子没有处理这个问题你跑的时候如果发现输入被跳过先检查这里。5.3 现象指针指向局部数组函数返回后数据乱码原因函数内部定义的数组是局部变量存在栈上。函数返回后栈空间被回收指针指向的地址虽然还在但内容已经被覆盖。笔记在讲指针和函数时提到过“不要返回局部变量的地址”但例子不够醒目。解决要么把数组改成static要么用malloc在堆上分配要么由调用者传入缓冲区。三种方式各有适用场景static简单但不可重入malloc灵活但要记得free调用者传缓冲区最安全但接口多一个参数。5.4 现象fwrite写结构体后文件大小和预期不符原因结构体对齐导致实际大小比成员之和大。比如char int char在 32 位对齐下占 12 字节不是 6 字节。解决用sizeof打印实际大小不要手算。如果必须精确控制字节数用#pragma pack(1)取消对齐或者逐字段写入。笔记里没有展开讲对齐但这是文件操作必须知道的背景知识。5.5 现象程序在 Windows 上跑正常在 Linux 上输出乱码原因换行符处理不同。Windows 下文本模式会把\n写成\r\nLinux 下只写\n。如果你用文本模式打开文件跨平台读写时行尾会多一个\r。解决二进制数据一律用rb和wb文本数据如果跨平台读入后手动去掉\r。笔记里的文件操作例子默认是文本模式跨平台使用时需要改。6. 进阶用法把笔记里的链表代码改成可复用的模块笔记最后几章讲了链表包括单链表的创建、插入、删除、遍历。这些代码是很好的学习材料但直接拿去用会发现一个问题所有操作都写在main里节点数据是固定的int换个场景就要重写。我一般会做一步改造把数据域抽象成void*把操作函数抽成独立模块。先看改造后的头文件// list.h #ifndef LIST_H #define LIST_H typedef struct Node { void *data; struct Node *next; } Node; typedef struct { Node *head; int size; } List; List *list_create(void); void list_destroy(List *list); int list_insert(List *list, int index, void *data); void *list_remove(List *list, int index); void *list_get(List *list, int index); int list_size(List *list); #endif逻辑说明data用void*可以存任意类型调用者自己管理数据的生命周期。List结构体里多存了一个size避免每次求长度都遍历一遍。list_insert返回 0 表示成功非 0 表示失败这样调用者可以判断插入位置是否越界。对应的实现文件里list_insert的核心逻辑是如果index为 0新节点指向当前head然后更新head否则找到index-1位置的节点把新节点插到它后面。注意处理index越界和malloc失败的情况。改造完之后你可以这样用#include list.h #include stdio.h #include stdlib.h int main() { List *list list_create(); int a 10, b 20, c 30; list_insert(list, 0, a); list_insert(list, 1, b); list_insert(list, 2, c); for (int i 0; i list_size(list); i) { int *val (int*)list_get(list, i); printf(list[%d] %d\n, i, *val); } list_destroy(list); return 0; }逻辑说明list_get返回void*调用者知道存的是int所以强转成int*再解引用。list_destroy负责释放所有节点但不会释放data指向的内存因为那部分内存的所有权在调用者手里。这个边界要在文档里写清楚否则用的人容易漏掉free。参数方面list_insert的index可以等于list_size表示追加到末尾。如果大于list_size返回 -1 表示失败。list_remove删除节点后返回data指针调用者决定要不要free。从那以后我每次拿到一份带源码的笔记都会先跑通最小例子再把核心数据结构抽成独立模块最后才回头补理论细节。这份笔记的链表部分值得这么走一遍因为链表是理解指针和内存管理最好的练手材料。希望帮到你。本文还有配套的精品资源点击获取