ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

C语言指针五兄弟:指针数组、数组指针、函数指针、函数指针数组一次讲透

2026/10/3 4:40:00 拓冰建站 浏览量
C语言指针五兄弟:指针数组、数组指针、函数指针、函数指针数组一次讲透 搞C/C的人早晚都要过指针这一关。尤其是标题里这串名词——指针数组、数组指针、指针函数、函数指针、函数指针数组——光看名字就让人头大不少人在面试前背了又背一到写代码还是分不清int *p[3]和int (*p)[3]到底谁是谁。这篇文章我就把这几个硬骨头串在一起讲清楚从指针和变量、一维数组、二维数组开始一路拆到函数指针数组顺带把声明怎么读、传参怎么写、底层的寻址逻辑是什么一次说透。文章适合正在学C语言的同学也适合学了一段时间依然对指针和数组的结合犯迷糊的人只要跟着例子自己敲一遍这几个概念基本就不会再混了。1. 指针与变量先搞清楚指针到底存了什么1.1 指针的本质是一个普通变量只不过存的是地址很多初学者的误区是觉得指针很玄好像变量前面加个*就变成了某种特殊生物。其实指针就是一个普通的变量它里面存的东西不是数值本身而是另一个变量的地址。也就是说指针变量和int、char变量没什么本质区别都是先开辟一块内存往里存数据唯一的不同是它存的数据恰好是一个地址编号。举个例子int a 10; int *p a;这段代码里a在内存中占4个字节里面存的是数值10p也占内存空间里面存的是a的首字节地址。是取地址操作符它的作用就是把a在内存中的地址编号取出来放进p。那么*p又是什么*在这里是间接访问操作符解引用意思是根据p里面存的地址去访问那块内存里的数据。所以*p的值就是10而且*p和a是同一块内存的两种叫法。你可以直接*p 20;然后打印a会发现它变成了20。我在带新人时最爱打一个比方变量名是人的名字地址是门牌号指针变量是一张写着别人门牌号的纸条。你拿着纸条就能找到那个人的家还能进屋改东西。这跟指针指向谁是同一个意思。1.2 指针的大小和步长为什么int*加1跳4个字节这是很多新手忽略的细节。一个指针变量本身占多大内存答案是固定的取决于平台32位程序里所有指针占4字节64位程序里占8字节跟你指向的是int还是char没关系。你可以用printf(%zu\n, sizeof(int*));自己验证。但指针的步长跟它指向的类型直接相关char *pc NULL; int *pi NULL; double *pd NULL; pc 1; // 地址加1个字节 pi 1; // 地址加4个字节 pd 1; // 地址加8个字节p 1并不表示地址数值加1而是跳到下一个同类型变量。所以int *pi加1实际上是地址值加sizeof(int)也就是4。这个特性是后续数组遍历的理论基础也解释了为什么不同类型的指针不能随便互相赋值——步长定义完全不同。常见一个低级错误是int a 5; char *p a; // 编译会警告就算你用强制类型转换强行编译过去*p也只能读到a低地址的1个字节剩下的3字节被丢弃了。这属于类型不匹配不是我这里说的步长问题但原理是相通的指针的类型决定了你通过它能看到多大范围的内存。1.3 指针的初始化和空指针用指针前必须初始化否则它里面是垃圾值解引用不知道会踩到哪块内存。两种规范写法int a 10; int *p a; // 指向变量 int *q NULL; // 或者暂时不指向任何对象NULL在C语言里就是(void*)0代表地址0。地址0在绝大多数平台上是被系统保护的内存不允许读写。好处是你可以用if (p ! NULL)来判断指针是否有效。坏处是如果你对NULL解引用程序会直接崩溃段错误不过崩溃总比乱写内存强——后者通常表现为数据莫名被篡改排查起来痛苦得多。2. 指针与一维数组数组名为什么能假装是指针2.1 数组名在大多数表达式中会退化成首元素地址一维数组和指针结合核心就一句话数组名在绝大多数情况下会退化成指向首元素的指针。int arr[5] {1, 2, 3, 4, 5}; int *p arr; // 这里arr退化为arr[0]arr的类型是int[5]但在表达式里赋值给int*时编译器会把arr退化成int*也就是arr[0]。所以p和arr指向同一个地址但注意它们不是同一种类型。arr是数组类型长度为5的元素序列p是指针类型只存了一个地址。这个退化规则有两个例外一个是sizeof一个是int arr[5]; printf(%zu\n, sizeof(arr)); // 20整个数组的大小不是4或8 printf(%p %p\n, arr, arr); // 数值相同但类型不同sizeof(arr)算出的是数组总字节数5 × 4 20说明在这里arr保持了数组的身份。arr的类型是int(*)[5]指向整个数组的指针后面会细讲。2.2 下标访问的本质arr[i]就是*(arri)C语言的数组下标[]只是一个语法糖编译器的处理逻辑是arr[i] 等价于 *(arr i)怎么理解arr退化为首元素地址后arr i就是地址往后跳i个int的长度然后*解引用取出那个位置的值。这个转换是双向的arr[i]可以写成*(arr i)也可以写成i[arr]。听起来匪夷所思但编译器就是这么处理的i[arr]会被翻译成*(i arr)结果一模一样。我遇到过有面试官喜欢考这个其实它就是考你有没有真正理解下标的本质。对于一个变量int x 10; int *p x;来说p[0]也合法因为p[0]等价于*(p 0)也就是*p。很多人在做链表或者动态数组时看到p[0]不理解为什么数组下标能用在指针上这个转换就是答案。2.3 遍历数组的几种写法对比int arr[5] {1, 2, 3, 4, 5}; // 写法1下标 for (int i 0; i 5; i) { printf(%d , arr[i]); } // 写法2指针遍历 for (int *p arr; p arr 5; p) { printf(%d , *p); } // 写法3直接操纵首地址 for (int i 0; i 5; i) { printf(%d , *(arr i)); }三种写法输出一样。写法2在C语言底层很常见比如操作字符串时常用while (*s) s;来走完整个字符串因为在C里字符串就是char数组末尾有个\0作为哨兵。边界判断p arr 5是可行的因为指针大小比较的前提是指向同一个数组内部或末尾之后一个位置。如果你乱指到别的内存去比较那属于未定义行为。3. 指针与二维数组行指针才是理解的关键3.1 二维数组在内存里其实是一维的很多新手把二维数组想象成一个矩阵其实内存里根本没有二维这个概念。C语言的二维数组是连续存放的线性内存int a[3][4]就是先放第0行4个int紧接着放第1行4个int再放第2行。总字节数3 * 4 * 4 48字节在内存里是连续的48个字节。之所以用a[row][col]访问是因为编译器帮你做了换算a[row][col]等价于*(*(a row) col)。先理解这个换算逻辑a的类型是int (*)[4]指向一行含4个int的数组。a row按行为步长跳转跳到第row行的首地址类型依然是int (*)[4]。*(a row)解引用这一行得到第row行的首元素地址类型是int*。*(a row) col按列int为步长跳转跳到第col个元素。再解引用*(*(a row) col)得到具体的int值。这就是为什么二维数组名不能直接赋值给int**下面细说。3.2 指向二维数组的指针怎么写看一个非常容易混淆的声明对比表声明类型含义典型用途int *p[4]指针数组p先和[4]结合是数组数组每个元素的类型是int*存放多个字符串首地址int (*p)[4]数组指针p先和*结合是指针指向一个含4个int的数组用来指向二维数组的某一行int **pp二级指针pp是指针指向一个int*用于指针数组的头指针用代码看数组指针怎么用int a[3][4] { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} }; int (*p)[4] a; // p指向第0行 printf(%d\n, (*p)[2]); // 1*p得到第0行首元素地址[2]取第3列 printf(%d\n, *(*(p 1) 3)); // 8第1行第3列p 1为什么能跳到第1行因为p的类型是int (*)[4]指针步长是一个含4个int的数组的大小也就是16字节。这就是行指针的精髓它把行作为移动单位。3.3 二维数组当参数传递时的写法写函数时接收一个二维数组有好几种等价的声明方式void func1(int a[3][4]) {} // 第1种直接写完整维度 void func2(int a[][4]) {} // 第2种省略第一维第二维必须写 void func3(int (*a)[4]) {} // 第3种使用数组指针三种写法在编译器层面完全相同int a[3][4]和int a[][4]都会被调整为int (*a)[4]。这也是一个理解要点数组参数本质上是通过指针传递的数组名退化为指针所以函数内没法通过sizeof(a)获得数组大小必须在参数里额外传行列数。初学者常犯的错是把二维数组名a直接传给int**类型的形参void func(int **a); func(a); // 编译警告类型不匹配原因就是前面分析的a的类型是int (*)[4]它退化成指针后指向的是含4个int的行数组不是指向int*。int **要求先按int*的步长移动再按int的步长移动跟二维数组的实际内存布局对不上。3.4 二级指针与二维数组为什么它们不是一回事二级指针int **pp指向的是一个int*类型的变量常用于动态分配的指针数组比如int row0[3] {1, 2, 3}; int row1[3] {4, 5, 6}; int *rows[2] {row0, row1}; int **pp rows; // 合法rows是int*数组首元素类型是int*这里rows是数组里的元素本身是指针每个元素指向一块独立的内存这块内存之间不保证连续。而二维数组int a[3][2]是一整块连续内存。两者的内存布局有本质区别二维数组一行紧挨一行连续存储。指针数组或者二级指针访问的对象每行可能分布在内存的不同角落。所以访问方式都有差异用int **pp访问时第一跳取到rows[i]指向某行的指针第二跳rows[i][j]才取到具体数值而二维数组a[i][j]是纯索引换算。4. 指针数组与数组指针一字之差方向相反4.1 怎么读声明优先级和结合性是突破口区分int *p[3]和int (*p)[3]有一个非常实用的方法叫优先级读法比死记硬背靠谱得多先看标识符变量名左边有没有*右边有没有[]或()。[]的优先级高于*所以如果没有括号p先和[3]结合p是数组。如果加了括号让*先和p结合p就是指针。具体来说int *p[3]; // p先和[3]结合 p是一个数组数组里有3个元素 // 元素类型是int* 存放int指针的数组即指针数组 int (*p)[3]; // 括号使p先和*结合 p是指针 // 指向的类型是int[3] 指向含3个int数组的指针即数组指针面试时考的最多的就是这两种。你可以默念口诀没括号数组优先有括号指针优先。4.2 指针数组的实际用途字符串表、参数列表指针数组最经典的应用是保存多个字符串的首地址char *str_arr[] { hello, world, C language };这里有三个字符串字面量它们存储在静态存储区str_arr是一个数组每个元素是一个char*分别指向三个字符串的首字符。这种写法比char str[3][20]省内存因为每行不用固定申请20字节字符串多长就占多少空间。代价是指向的字符串字面量是只读的不能通过str_arr[0][0] H修改。程序入口main函数的参数其实也是这个结构int main(int argc, char *argv[])argv就是一个char*数组argv[0]是程序路径argv[1]及以后是命令行传入的参数。理解指针数组后操作argv跟操作str_arr没区别你会遍历它就能处理任意数量的命令行参数。4.3 数组指针的实际用途二维数组传参和矩阵运算数组指针主要用来指向二维数组的某一行或者干脆作为二维数组的行指针来操作整个矩阵。最常见的场景是编写矩阵运算函数void print_matrix(int rows, int cols, int (*mat)[cols]) { for (int i 0; i rows; i) { for (int j 0; j cols; j) { printf(%5d , mat[i][j]); } printf(\n); } }这里int (*mat)[cols]是可变长数组的写法C99支持cols在运行时传入。如果编译器不支持可变长数组那就老老实实写固定列数或者使用int**配合手动索引。数组指针还有一个容易踩的坑int a[3][4]; int (*p)[4] a; p; // 跳过整整一行地址加16字节 (*p)[0]; // 访问下一行的第一个元素很多新手拿p当移动一个元素来理解结果发现地址跳了4个int那么多就是因为没意识到int (*)[4]的步长是一个含4个int的数组16字节。这是一个非常典型的指针步长问题。5. 指针函数与函数指针返回指针还是用指针调函数5.1 指针函数返回值是地址的函数名字里带函数结尾的本质上是个函数只不过它的返回值是个指针。声明格式int *find_max(int *arr, int size) { int *max arr; for (int i 1; i size; i) { if (arr[i] *max) { max arr[i]; } } return max; }int *find_max(...)的意思是find_max是一个函数它的返回值类型是int*。调用方式int nums[] {3, 7, 2, 9, 5}; int *p find_max(nums, 5); printf(%d\n, *p); // 9指针函数最常见的一个坑是返回局部变量的地址。局部变量在函数结束后内存就被回收了你再拿着那个地址去解引用读出来的是垃圾值。比如int *bad_func(void) { int x 10; return x; // 悬垂指针x已经生命周期结束 }解决办法有三条路返回静态局部变量的地址static int x、由调用者传入缓冲区地址、或者用动态分配内存malloc记得释放。这是面试常考点也是实际开发中非常容易出bug的地方。5.2 函数指针把函数当成数据来传递函数指针的声明语法是三者中最抽象的int (*fp)(int, int);读法是fp先和*结合说明fp是指针往外一层fp指向一个函数这个函数有两个int参数返回int。它最典型的应用是回调函数——把一个函数作为参数传给另一个函数让框架在特定时机调用。举个例子自定义排序比较函数#include stdio.h #include stdlib.h int compare_int(const void *a, const void *b) { int ia *(const int*)a; int ib *(const int*)b; return ia - ib; } int main(void) { int nums[] {5, 2, 8, 1, 9, 3}; qsort(nums, 6, sizeof(int), compare_int); for (int i 0; i 6; i) { printf(%d , nums[i]); } return 0; }这里qsort的第四个参数就是函数指针类型是int (*)(const void*, const void*)。没有函数指针的话你还得写几个不同的排序函数来应对不同的比较规则有了它排序算法本体只需要实现一次比较逻辑由调用方随时注入。这就是把变化的部分参数化的设计思路。5.3 区分记忆技巧从名字位置和*配合来看类别声明示例读法本质指针函数int *func(int a)func是函数返回int*一个返回指针的函数函数指针int (*fp)(int a)fp是指针指向函数一个指向函数的指针变量这里又可以用优先级读法*func和*fp的位置关系。int *func(int a)中func先和(int a)结合说明func是函数*只是修饰返回值。int (*fp)(int a)中括号让fp先和*结合说明fp是指针外面的(int a)说明它指向的是一个接受int参数、返回int的函数。注意一个细节fp func;和fp func;两种写法在C语言里完全等价因为函数名在表达式中会退化为指向函数的指针。调用时fp(10)和(*fp)(10)也都行日常习惯写fp(10)就够了。6. 函数指针数组转移表与回调机制的基石6.1 声明、初始化和调用函数指针数组就是把多个函数指针放进一个数组。声明语法看着吓人但一层层剥开并不难int (*fp_array[4])(int, int);按优先级读法拆解fp_array先和[4]结合所以它是数组数组元素类型是int (*)(int, int)也就是每个元素是一个函数指针。整个声明的意思是一个长度为4的数组里面存了4个函数指针。初始化并调用#include stdio.h int add(int a, int b) { return a b; } int sub(int a, int b) { return a - b; } int mul(int a, int b) { return a * b; } int divide(int a, int b) { return b 0 ? 0 : a / b; } int main(void) { int (*oper[4])(int, int) {add, sub, mul, divide}; for (int i 0; i 4; i) { printf(%d\n, oper[i](10, 3)); } return 0; }这段代码定义了4个二元运算函数把它们放进数组oper然后用oper[i](10, 3)直接调用。这是函数指针数组最直白的用途根据下标选函数而不是根据条件写一长串if-else。6.2 用函数指针数组替换switch-case以计算器为例很多教材讲计算器都会先写一版switchchar opt; int a 5, b 3; switch (opt) { case : result a b; break; case -: result a - b; break; case *: result a * b; break; case /: result a / b; break; }这种写法本身没错但当你需要支持几十个操作时switch会膨胀得很厉害。用函数指针数组可以把操作符映射和运算逻辑彻底分离#include stdio.h int add(int a, int b) { return a b; } int sub(int a, int b) { return a - b; } int mul(int a, int b) { return a * b; } int divide(int a, int b) { return b ? a / b : 0; } typedef int (*op_func)(int, int); int main(void) { op_func ops[128] {0}; ops[] add; ops[-] sub; ops[*] mul; ops[/] divide; char opt ; int a 9, b 4; if (ops[(int)opt] ! NULL) { printf(%d\n, ops[(int)opt](a, b)); // 13 } return 0; }这里使用了typedef给函数指针类型起别名op_func可读性大大提高。数组大小为128是因为char的下标最大到127直接用ASCII码做索引查表代替分支判断。这种表驱动的设计在嵌入式菜单系统、命令解析器、图形界面事件分发中非常常见本质就是一种轻量的回调机制。6.3 从计算器到命令解析实际的进阶应用我在实际项目中用过一次这样的设计一个简单的调试终端要解析set、get、help、dump、reset这几种命令。如果每个命令写一个case分支后期命令越来越多每个case里还要处理参数解析、错误返回函数会变得又长又难维护。用函数指针数组之后每个命令就是一个独立的函数命令名到函数的映射单独成一张表新增命令只需要加一个函数、加一行映射struct command { const char *name; void (*handler)(int argc, char *argv[]); }; struct command cmd_table[] { {help, help_handler }, {set, set_handler }, {get, get_handler }, {dump, dump_handler }, {reset, reset_handler}, }; void dispatch(const char *cmd, int argc, char *argv[]) { for (size_t i 0; i sizeof(cmd_table) / sizeof(cmd_table[0]); i) { if (strcmp(cmd, cmd_table[i].name) 0) { cmd_table[i].handler(argc, argv); return; } } printf(unknown command\n); }这种写法在C语言世界里非常成熟也是很多C里策略模式的底层原型。值得一提的是struct里面放函数指针很多数据结构的教科书不会展开讲但你在阅读开源C项目时会频繁遇到。6.4 函数指针最难的点类型太长了怎么办很多人学到这里会崩溃因为函数指针类型写起来太啰嗦比如int (*fp)(int, int);如果你有一堆这种变量每次都写完整类型非常痛苦。解决办法是typedef除了给函数指针起别名也可以给函数签名起别名typedef int (*binary_op)(int, int); typedef int (*unary_op)(int);然后声明就清爽多了binary_op ops[100];注意typedef和变量声明的区别typedef把一个新的类型名放在了原来写变量名的位置。刚开始可能不习惯多用几次就好。实际上在很多C语言项目中函数指针都是藏在typedef后面的直接看到的都是简洁的别名。6.5 使用函数指针数组时常见的坑几个实战中容易踩的坑提前提醒一下第一数组下标越界。用操作符字符做下标如ops[]没问题但如果你用数字索引比如ops[1000]就直接越界了。务必保证下标范围合法或者用if (idx 0 idx size)做防护。第二空指针调用。如果数组里某些位置没初始化或者初始化为NULL调用时就会崩溃。像上面的例子调用前先检查ops[(int)opt] ! NULL。第三函数签名必须完全匹配。函数指针的类型严格限制了参数个数和返回值不匹配会编译报错比如int add(int, int)不能直接塞进int (*fp)(int)类型的数组里。解决办法是统一函数签名让所有函数都用int func(int, int)或者带void*参数的形式然后内部强转。第四别忘记数组名本身也是一个指针这跟你前面学的数组和指针的关系一样。oper[i](a, b)本质上等价于(*(oper i))(a, b)。这行代码能把前面所有知识串起来oper i是数组内第i个元素的地址*(oper i)取到那个函数指针再对函数指针调用(a, b)。7. 结合实战把这几层关系放在一起写段能跑的代码7.1 一个综合示例用二维数组、数组指针、函数指针数组做一个迷你菜单程序光讲概念容易飘我写一个相对完整的小例子尽量把前面所有知识点串在一起。代码不长但覆盖了二维数组、数组指针、指针函数、函数指针数组这些概念。#include stdio.h #include string.h // 功能函数 int add(int a, int b) { return a b; } int sub(int a, int b) { return a - b; } int mul(int a, int b) { return a * b; } int divide(int a, int b) { return b 0 ? 0 : a / b; } typedef int (*binary_op)(int, int); // 函数指针数组下标0-3对应4种操作 binary_op ops[4] {add, sub, mul, divide}; // 操作符表二维数组每行存一个操作符字符和名字 char op_names[4][8] { , -, *, / }; // 指针函数接收行指针返回该行操作符字符 char get_op(char (*row)[8]) { return (*row)[0]; } int main(void) { // 用数组指针遍历操作符表 char (*row_ptr)[8] op_names; for (int i 0; i 4; i, row_ptr) { printf(操作符: %c\n, get_op(row_ptr)); } int a 12, b 5; printf(12 5 %d\n, ops[0](a, b)); printf(12 - 5 %d\n, ops[1](a, b)); printf(12 * 5 %d\n, ops[2](a, b)); printf(12 / 5 %d\n, ops[3](a, b)); return 0; }这里组合了二维字符数组op_names存放操作符信息函数指针数组ops存放运算函数get_op是返回char的普通函数形参用了数组指针char (*row)[8]main里用char (*row_ptr)[8]遍历二维数组的每一行。这个例子虽然小但把数组指针、二维数组、函数指针数组这三者放在同一个程序里彼此的角色一对比就直观多了数组指针负责行级别的移动函数指针数组负责操作级别的分发。7.2 建议的练习路径学习这些概念没有捷径但有一条我自己验证过还算高效的路径按顺序来在纸上手写出int *p[5]、int (*p)[5]、int *fp(int)、int (*fp)(int)四个声明并用优先级读法解释含义直到不看答案能写对。自己写一个程序用三种不同方式遍历一维数组下标、指针、*(arri)打印结果确认一致。写一个3行4列的二维数组分别用a[i][j]、*(*(ai)j)、*(a[i]j)访问同一个元素对比结果。写一个指针数组存放3个字符串再写一个数组指针指向一个3行4列的二维数组分别遍历输出。实现一个计算器程序先写switch版本再改用函数指针数组版本对比两者的代码结构。最后用typedef给函数指针起别名重构你的计算器。这套练习做完基本就能把指针和数组、函数的结合体系打通。我的建议是每个例子都自己敲一遍别光看书尤其是声明读法手一写就出真知。8. 个人体会与经常被问到的几个问题整理8.1 为什么学了这么多年还是容易混以我带过那么多新人的经验来看最容易造成混淆的不是单个概念而是概念之间容易互相暗示。比如int *p[3]和int (*p)[3]名字就差一个括号int *function()和int (*fp)()名字里都有function位置不一样含义就完全不同。我的建议是不要在脑子里同时背好几个名字只记住优先级读法和右左法则这一个工具就够了。拿到任何声明先看标识符右边有没有[]或()再看左边有没有*括号改变结合顺序每一步都按这个规则来所有复杂声明都能拆开。语言本身就是一套规则系统理解了规则就没必要死记硬背。8.2 指针到底要不要用学C用得多吗在很多开发场景里现代C推荐用引用和智能指针但这不意味着C的原始指针不用学。理由很简单第一C里指针依然无处不在数组参数传递、回调、面向对象底层实现都离不开指针第二理解指针是理解内存布局最快的方式而内存布局会影响你对性能、生命周期的判断。很多C代码里的隐蔽bug追溯到底都是指针对内存的错误操作在前。先把C的指针啃明白后面学C的引用、容器思路会顺很多。8.3 一道综合题型的思考过程面试里最常考的类似题char *p1[4]、char (*p2)[4]、char *p3()、char (*p4)(),问各是什么。我的拆法p1:右边先遇到[4]是数组元素类型char*是指针数组。p2:括号让左边*先发挥作用是指针指向char[4]是数组指针。p3:右边先遇到()是函数返回值char*是指针函数。p4:括号让*先发挥作用是指针指向一个返回char的函数是函数指针。整个过程完全不靠背名字纯粹是语法解析。这也是真气人但又很有意思的C语言特色声明本身就是一道编程题破解它你会非常有成就感。8.4 最后分享一个调试小技巧如果程序崩溃但你怀疑是指针问题有个土办法很好用把指针打印出来然后跟一个已知的数组地址对比。比如你怀疑p越界了先打印p再打印arr和arr4看看p是否落在这个区间之外。C语言没有运行时数组边界检查养成打印地址、确认范围的习惯能帮你省下大量排查时间。我工作这么些年遇到莫名其妙的段错误第一反应永远是把指针和数组边界打出来看而不是翻代码逻辑。搞懂指针和数组、函数的结合其实不只是为了应付考试或者面试它直接决定你能不能读懂开源项目里那些错综复杂的函数指针表、回调结构、插件系统。这条路没有捷径但只要你把优先级读法和指针步长这两个基本功打磨扎实再顺着本文的路线把几个概念逐个吃透后续再看任何复杂的C代码都会觉得顺眼很多。