首页 / 资讯中心 / 文章详情

郝斌C语言笔记+源码:从语法到内存模型,彻底搞懂指针与结构体

郝斌C语言笔记+源码:从语法到内存模型,彻底搞懂指针与结构体 ★ FEATURED ARTICLE
简介这份郝斌C语言详细笔记附源码面向零基础及初学编程的读者是一套系统梳理C语言核心知识的入门学习资料适合需要打牢语法基础、理解底层原理并养成规范编码习惯的人群。资源包内共1个doc文档约6.04MB以文字笔记形式完整记录了课堂讲解要点并附带源码便于对照练习。笔记从C语言的发展历程与结构化特点讲起对比Fortran、Basic、Pascal等语言的分工分析C代码量小、速度快、功能强大但危险性高、开发周期长的优缺点并延伸到Windows内核、Linux与Unix系统开发中的应用。内容还涵盖关键字与程序格式、CPU与内存硬盘的关系、数据类型与变量定义、进制与常量存储、代码规范化等基础模块强调多上机、多调试、在排错中成长。目前已有121人学习适合作为C语言入门与复习的参考笔记。1. 从一份“郝斌C语言详细笔记(附源码).doc”说起为什么指针和内存模型才是C语言真正的分水岭很多人学C语言卡在同一个地方语法都懂if、for、函数、数组写起来没问题但一碰到指针、二级指针、函数指针、动态内存就发懵。这份“郝斌C语言详细笔记(附源码).doc”之所以被反复搜索本质上不是大家缺一份文档而是缺一条从“会写语法”到“看懂内存”的路径。笔记类资料的价值在于它把课堂上的推导过程留下来了而附带的源码让每个结论都能跑一遍验证。我带过几个刚入门的同学他们最大的问题不是不努力而是把C语言当成了“背规则”的科目。指针的规则背得再熟只要脑子里没有一张内存布局图*p和(*p)永远分不清。这份笔记配合源码恰好能解决这个问题先看代码怎么跑再看内存怎么变最后回头理解语法为什么这么设计。适合谁适合已经能写几十行C程序、但一遇到指针和结构体就翻车的自学者也适合想重新梳理底层模型的开发者。2. 把笔记和源码跑起来环境搭建与第一轮验证2.1 编译器选择与最小验证工程拿到一份.doc笔记和配套源码第一件事不是从头读到尾而是先把源码跑通。C语言的环境搭建比很多人想象的简单但也比想象中容易踩坑。Windows上常见的是MinGW-w64Linux和macOS自带gcc或clang。我一般建议用VS Code加命令行编译器而不是一上来就用重型IDE因为命令行能让你看清编译的每一步。先建一个最小工程目录把源码按章节分文件夹放好。下面是一个典型的编译命令# 编译单个源文件开启所有警告这是学C语言必须养成的习惯 gcc -Wall -Wextra -g -o chapter01 chapter01.c # 如果源码有多个文件比如笔记里常见的分文件示例 gcc -Wall -Wextra -g -o demo main.c utils.c -I./include # 运行 ./chapter01-Wall -Wextra这两个参数必须加。C语言有很多“合法但危险”的写法编译器默认不报加上警告能提前发现大部分指针误用。-g是生成调试信息后面用gdb或者VS Code调试时要用。-I./include是指定头文件搜索路径笔记里的源码如果有自定义头文件不加这个会报找不到。提示如果编译时报“undefined reference”先检查是不是漏了某个.c文件而不是急着改代码。C语言的链接错误九成是文件没加全。2.2 用调试器看内存而不是靠printf猜笔记里讲指针的时候一定会画内存图。但画出来的图和真实内存是两回事。我建议从第一天就用gdb看真实的内存变化。下面是一个典型操作# 编译时带-g然后启动gdb gdb ./chapter01 # 在main函数打断点 (gdb) break main (gdb) run # 单步执行 (gdb) next # 查看变量地址和值 (gdb) print a (gdb) print a (gdb) print *p # 以十六进制查看内存比如查看指针p指向的4个字节 (gdb) x/4xb px/4xb的意思是以十六进制x显示4个单元4每个单元1字节b。这个命令在验证指针偏移、数组越界、结构体对齐时特别有用。笔记里说“指针加1移动4个字节”你在gdb里用x/4xb一看就信了。参数说明x是examine4是数量x是十六进制b是byte。换成w就是4字节字。2.3 源码目录的组织方式与笔记对照一份好的笔记源码通常按知识点分目录比如01_basic、02_pointer、03_struct。我一般会建一个notes文件夹把.doc里的关键结论用自己的话重写一遍每个结论后面贴上对应的源码文件名和行号。这样做的好处是以后忘了某个细节不用翻几百页文档直接看自己的索引。具体操作用VS Code打开整个源码目录按CtrlShiftF全局搜索笔记里的关键词比如“二级指针”找到对应源码。然后在该行加一个书签。笔记和源码对照着看比单独看任何一样都有效。这一步不需要写代码但决定了你后面能不能坚持下来。3. 指针与内存模型笔记里最容易被跳过、也最不能跳过的部分3.1 指针的本质地址、类型、解引用三件事笔记里讲指针通常会从“指针就是地址”开始。这句话对但不够。指针有三要素地址值、指向的类型、以及通过解引用能访问的内存宽度。很多人分不清int *p和char *p的区别以为都是存地址。地址确实一样宽但*p时取几个字节、怎么解释这些字节完全由类型决定。看一段典型代码#include stdio.h int main(void) { int a 0x12345678; int *p a; char *c (char *)a; printf(p指向的值: 0x%x\n, *p); // 输出 0x12345678 printf(c[0]: 0x%x\n, c[0]); // 取决于字节序 printf(c[1]: 0x%x\n, c[1]); printf(c[2]: 0x%x\n, c[2]); printf(c[3]: 0x%x\n, c[3]); return 0; }这段代码在gdb里配合x/4xb a看能直接看到内存里4个字节的排列。小端机器上c[0]是0x78c[3]是0x12。笔记里如果只写“指针存地址”你就不会理解为什么char *能一个字节一个字节地访问。参数说明(char *)a是强制类型转换把int *转成char *告诉编译器“我要按字节访问”。这是理解内存布局的基础操作。3.2 二级指针与函数传参为什么你的交换函数不生效笔记里一定会讲“值传递”和“地址传递”。但很多人写完交换函数发现没生效原因不是没传地址而是传的地址层级不够。看下面两段对比#include stdio.h #include stdlib.h // 错误版本只交换了局部变量 void swap_wrong(int *a, int *b) { int *tmp a; a b; b tmp; } // 正确版本交换指针指向的值 void swap_right(int *a, int *b) { int tmp *a; *a *b; *b tmp; } // 二级指针版本修改指针本身的指向 void swap_ptr(int **a, int **b) { int *tmp *a; *a *b; *b tmp; } int main(void) { int x 1, y 2; int *px x, *py y; swap_wrong(px, py); // px和py没变 printf(after wrong: *px%d, *py%d\n, *px, *py); swap_right(px, py); // x和y的值交换了 printf(after right: *px%d, *py%d\n, *px, *py); swap_ptr(px, py); // px和py的指向交换了 printf(after ptr: *px%d, *py%d\n, *px, *py); return 0; }swap_wrong里a和b是形参是px和py的副本。交换副本不影响原件。swap_right里*a和*b访问的是px和py指向的内存所以生效。swap_ptr里**a才能访问到px本身。笔记里如果只画了图没跑代码你很容易在swap_wrong和swap_right之间混淆。参数说明int **a表示a是一个指针指向一个int *。调用时传pxpx本身是int *取地址就是int **。3.3 动态内存与指针的常见误用笔记里讲malloc和free通常会强调“配对使用”。但实际翻车最多的是三个地方忘记检查返回值、释放后继续用、以及指针偏移后free。看代码#include stdio.h #include stdlib.h #include string.h int main(void) { // 1. 忘记检查malloc返回值 int *arr (int *)malloc(10 * sizeof(int)); if (arr NULL) { // 这行不能省 fprintf(stderr, malloc failed\n); return 1; } // 2. 释放后继续用use after free free(arr); // arr[0] 1; // 危险未定义行为 // 3. 指针偏移后free char *buf (char *)malloc(100); if (buf NULL) return 1; char *p buf 10; // free(p); // 错误必须free原始指针 free(buf); // 正确 return 0; }malloc返回NULL在内存紧张或请求过大时会发生不检查就是等着程序崩溃。释放后继续用编译器不会报错但运行时可能改到别的数据。指针偏移后free堆管理器会直接报错或静默损坏。笔记里如果只写“要配对”这三个坑你得自己踩一遍才记得住。参数说明malloc(10 * sizeof(int))分配40字节假设int为4字节返回void *需要强制转换。free的参数必须是malloc返回的原始地址。4. 结构体、文件操作与笔记里的综合案例从语法到可用代码4.1 结构体对齐与内存布局笔记里讲结构体通常会提到“内存对齐”。这个概念不搞懂后面写文件读写、网络协议、硬件寄存器映射都会出问题。看一个典型例子#include stdio.h #include stddef.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(void) { printf(sizeof(struct A) %zu\n, sizeof(struct A)); printf(sizeof(struct B) %zu\n, sizeof(struct B)); printf(offset of i in A %zu\n, offsetof(struct A, i)); printf(offset of d in A %zu\n, offsetof(struct A, d)); return 0; }在常见的4字节对齐规则下struct A是12字节struct B是8字节。offsetof宏能直接告诉你每个成员在结构体里的偏移。笔记里如果只写“对齐会浪费空间”你不会知道浪费在哪。参数说明offsetof(struct A, i)返回i相对于结构体起始地址的字节偏移。这个宏在写序列化代码时特别有用能确保你按正确的偏移读写。4.2 文件读写fread/fwrite与二进制安全笔记里讲文件操作通常会从fopen、fclose开始。但真正要写可用代码必须理解文本模式和二进制模式的区别。看一段把结构体写入文件的代码#include stdio.h #include stdlib.h typedef struct { int id; char name[32]; float score; } Student; int main(void) { Student s {1, Alice, 95.5f}; Student out; // 写入二进制文件 FILE *fp fopen(student.dat, wb); if (fp NULL) { perror(fopen); return 1; } size_t written fwrite(s, sizeof(Student), 1, fp); if (written ! 1) { fprintf(stderr, fwrite failed\n); fclose(fp); return 1; } fclose(fp); // 读取二进制文件 fp fopen(student.dat, rb); if (fp NULL) { perror(fopen); return 1; } size_t read fread(out, sizeof(Student), 1, fp); if (read ! 1) { fprintf(stderr, fread failed\n); fclose(fp); return 1; } fclose(fp); printf(id%d, name%s, score%.1f\n, out.id, out.name, out.score); return 0; }wb和rb里的b表示二进制模式。在Windows上文本模式会把\n转成\r\n二进制模式不会。写结构体必须用二进制模式否则数据会错位。fwrite返回实际写入的元素个数不等于请求个数就说明出错了。参数说明fwrite(s, sizeof(Student), 1, fp)表示把s开始的1个sizeof(Student)大小的数据写入fp。fread同理。笔记里如果只写fopen的w和r你写二进制文件时就会踩坑。4.3 综合案例用笔记里的知识点写一个学生成绩管理笔记最后通常会有综合案例。我一般会带着同学把案例拆成三个模块数据结构、文件存储、交互逻辑。数据结构用结构体数组或链表文件存储用上面的fread/fwrite交互逻辑用switch加循环。下面是一个最小骨架#include stdio.h #include stdlib.h #include string.h #define MAX_STUDENTS 100 typedef struct { int id; char name[32]; float score; } Student; static Student students[MAX_STUDENTS]; static int count 0; void add_student(void) { if (count MAX_STUDENTS) { printf(已满\n); return; } Student *s students[count]; printf(输入id 姓名 成绩: ); scanf(%d %31s %f, s-id, s-name, s-score); count; } void list_students(void) { for (int i 0; i count; i) { printf(%d %s %.1f\n, students[i].id, students[i].name, students[i].score); } } void save_to_file(const char *path) { FILE *fp fopen(path, wb); if (fp NULL) { perror(fopen); return; } fwrite(count, sizeof(int), 1, fp); fwrite(students, sizeof(Student), count, fp); fclose(fp); } void load_from_file(const char *path) { FILE *fp fopen(path, rb); if (fp NULL) { perror(fopen); return; } fread(count, sizeof(int), 1, fp); fread(students, sizeof(Student), count, fp); fclose(fp); } int main(void) { int choice; while (1) { printf(1.添加 2.列出 3.保存 4.加载 0.退出\n); scanf(%d, choice); switch (choice) { case 1: add_student(); break; case 2: list_students(); break; case 3: save_to_file(students.dat); break; case 4: load_from_file(students.dat); break; case 0: return 0; default: printf(无效\n); } } }这个骨架把笔记里的结构体、指针、文件操作、循环分支都串起来了。scanf(%31s, s-name)里的31是防止缓冲区溢出因为name是32字节留一个给\0。fwrite(students, sizeof(Student), count, fp)一次写多个元素比循环写效率高。参数说明count是写入元素个数加载时先读这个数再读对应数量的学生。笔记里的综合案例如果比这个复杂按这个思路拆解就行。5. 避坑与排查学C语言笔记时最容易翻车的五个地方5.1 编译通过但运行崩溃先查指针和数组越界现象程序编译没警告一运行就段错误。原因最常见的是指针未初始化、数组越界、或者scanf漏了。解决用gdb跑在崩溃处看backtrace定位到具体行。然后检查该行的指针是否有效、数组下标是否在范围内。我一般会先在代码里加assert把可疑的下标和指针都断言一遍。5.2 笔记里的代码能跑自己改一行就错注意隐式类型转换现象照着笔记抄能跑改个变量类型就出问题。原因C语言的隐式类型转换规则很绕int和unsigned比较、float和double混用、char的符号性都可能让结果偏离预期。解决编译时加-Wconversion让编译器提示所有隐式转换。然后显式写出强制类型转换别偷懒。5.3 文件读写乱码或数据错位检查模式和结构体对齐现象写进去的数据读出来不对或者文本文件打开是乱码。原因文本模式和二进制模式混用或者结构体里有指针成员直接写入了文件。解决二进制数据一律用wb和rb。结构体里如果有指针不能直接fwrite要写序列化函数把指针指向的内容也写进去。对齐问题用#pragma pack或者手动填充解决。5.4 内存泄漏查不到用工具而不是靠眼睛现象程序跑久了内存越来越大。原因malloc后忘了free或者在中途return时跳过了free。解决Linux上用valgrind命令是valgrind --leak-checkfull ./your_program。Windows上可以用Dr. Memory。工具会直接告诉你哪一行分配的内存没释放。别靠眼睛看几百行代码里漏一个free根本看不出来。5.5 笔记版本和编译器不匹配注意C标准差异现象笔记里的代码在某些编译器上报错。原因C89、C99、C11对变量声明位置、for循环内声明、变长数组的支持不同。解决编译时指定标准比如gcc -stdc99或-stdc11。如果笔记比较老可能用的是C89那就把变量声明都移到块开头。我一般统一用-stdc11兼容性和特性都比较平衡。6. 把笔记变成自己的东西一个验证学习效果的笨办法学C语言最怕的是“看懂了”的错觉。笔记写得再详细源码跑得再顺不自己写一遍过两周就忘。我用的笨办法是每学完笔记的一章合上笔记自己写一个最小程序只用到这一章的知识点然后和笔记里的源码对比。差异越大说明你漏掉的东西越多。具体操作分三步。第一步选一个知识点比如“函数指针”。第二步不看书写一个用函数指针做回调的排序函数。第三步编译运行再用gdb单步看函数指针的调用过程。下面是一个验证示例#include stdio.h #include stdlib.h // 比较函数升序 int cmp_asc(const void *a, const void *b) { return (*(int *)a - *(int *)b); } // 比较函数降序 int cmp_desc(const void *a, const void *b) { return (*(int *)b - *(int *)a); } // 自己实现的排序接受函数指针 void my_sort(int *arr, int n, int (*cmp)(const void *, const void *)) { for (int i 0; i n - 1; i) { for (int j 0; j n - 1 - i; j) { if (cmp(arr[j], arr[j 1]) 0) { int tmp arr[j]; arr[j] arr[j 1]; arr[j 1] tmp; } } } } int main(void) { int arr[] {5, 2, 9, 1, 7}; int n sizeof(arr) / sizeof(arr[0]); my_sort(arr, n, cmp_asc); for (int i 0; i n; i) printf(%d , arr[i]); printf(\n); my_sort(arr, n, cmp_desc); for (int i 0; i n; i) printf(%d , arr[i]); printf(\n); return 0; }my_sort的第三个参数是函数指针类型是int (*)(const void *, const void *)。调用时传cmp_asc或cmp_desc排序方向就变了。这个模式在标准库的qsort里就是一样的。参数说明const void *是通用指针比较函数内部再转成具体类型。(*(int *)a - *(int *)b)先解引用再比较返回正数表示a大于b。验证方法写完之后用gcc -Wall -Wextra -g编译确保没警告。然后用gdb在my_sort里打断点看cmp的值是不是你传进去的函数地址。再用x/5dw arr看排序前后数组的变化。如果都能对上这一章才算真的过了。我自己的习惯是每学完一个知识点就在笔记旁边写一行“验证代码xxx.c验证日期xx”。过一个月回头翻如果还能不看笔记写出验证代码才算掌握。这个办法很笨但比反复看笔记有效得多。C语言没有捷径指针和内存模型更是如此。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站