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

深入理解C语言指针进阶:函数指针、回调函数与复杂声明实战拆解

深入理解C语言指针进阶:函数指针、回调函数与复杂声明实战拆解 ★ FEATURED ARTICLE
不少朋友学指针卡就卡在“能看懂书上的代码但自己一写就废”这个阶段。前两篇我们聊了指针的基本类型、指针与一维/二维数组的关系、指针运算的底层含义那都是地基。到了“深入理解指针”这个系列的第三篇面对的就不再是“指针指向谁”这种直观问题而是各种让新手头皮发麻的进阶玩法函数指针、函数指针数组、回调函数、多级指针还有那些看一眼就想放弃的复杂声明。这篇的内容就是帮你把这些硬骨头啃下来读完你再看Linux内核或者开源项目里的代码至少不会觉得那些带星号括号的东西是乱码。第三篇的定位很明确不搞理论背诵直接对准三个最常见的实战痛点——怎么把函数当参数传递、怎么用指针层层操作数据、怎么一眼看穿复杂声明。适合的人包括正在学C语言进阶的学生、做嵌入式开发但不太跟函数指针打交道的工程师、以及写了两三年业务代码想回去补底层功底的开发者。我会配合代码、场景和排错经验来讲尽量让每个知识点都能落到你能用的层面。1. 第三篇到底补什么课先厘清指针进阶的核心盲区1.1 前两篇覆盖了什么这篇又接着讲什么很多培训机构和网课把指针内容机械地切成几节课导致学习者经常出现一种错觉每节课都听懂了串起来却不知道完整拼图长什么样。我记得比较典型的误区是学完指针和数组的关系之后就觉得自己会指针了但一碰到类似void (*signal(int sig, void (*func)(int)))(int);这种声明就彻底懵掉甚至怀疑C语言是不是故意搞人的。在“深入理解指针”的前两个阶段我默认大家已经掌握了几个基础能力指针变量是存地址的变量、*用来解引用、用来取地址、数组名在多数表达式中会退化为首元素地址、指针的加减法移动的是“所指向类型的大小”。这些内容很重要但说实话它们只是把指针当成“一种间接寻址方式”来用还没有完全释放指针作为C语言核心抽象的力量。第三篇要补的正是从“数据层面间接寻址”跳到“代码层面间接寻址”和“更深的寻址层级”。具体来讲包括三大块。第一块是函数指针。函数不是变量但它也有地址可以被存下来、传出去、通过指针调用。很多实际问题比如写一个通用排序、实现一个事件分发器、做插件化设计都离不开把函数当作对象来引用。第二块是函数指针数组、回调机制。它们建立在函数指针基础之上。前者常用于重构一堆if-else或switch-case后者是很多库和框架扩展点的底层逻辑。不理解回调就不理解为什么一个库可以把“用户自定义操作”嵌入到自己的主流程中。第三块是二级指针和多级指针。这类内容在日常业务代码里不常写但一旦涉及修改实参指针本身、管理指针数组、处理字符串数组时它就是绕不开的底层机制。很多人觉得二级指针不直观说白了还是“地址的地址”这个概念没被真正内化成直觉。1.2 一个容易忽略的基础共识代码和数据一样也住在内存里想理解函数指针首先要树立一个观念程序运行的时候我们的函数代码并不是飘在空中的抽象逻辑它同样被编译成机器指令存放在内存的代码段占据具体的地址空间。也就是说函数也有自己的“门牌号”。当我们讨论某个函数的地址时其实就是在讨论它第一条指令所在的内存位置。嵌入式开发的朋友对这个应该特别有感触。在单片机上我们经常看到这样的写法void (*JumpToApp)(void) (void (*)(void))0x08008000; JumpToApp();这就是典型的把某个固定地址直接当作函数入口来调用用于从Bootloader跳转应用固件。在PC平台上我们一般不需要关心绝对地址但函数名的确会在编译期被解析成一个地址值。这里有一个常被误解的点func和func这两个写法意义完全相同。在C语言标准中函数名在表达式中会按照需要被隐式转换为指向函数的指针取地址操作符作用于函数名时得到的也是函数指针两者没有区别。这跟数组不同——数组名不能简单等价于数组名但函数名可以。这个差异在面试里属于高频送分题能答清楚的基本说明对函数指针有了正确的底层认知。当你接受了“函数名本质是入口地址”之后再看“函数指针”这个概念就顺理成章了它就是一个专门存放这种入口地址的指针变量类型由函数的返回类型和参数列表共同决定。2. 函数指针的声明、赋值与调用语法细节就是最大的拦路虎2.1 声明语句到底怎么读三个基本类型的逐层拆解函数指针的声明语法可以用“怪异”两个字形容。对于初次接触的人来说它不像普通声明那样直观。比如下面三行int *p[3]; // 指针数组p是数组数组里有3个int* int (*p)[3]; // 数组指针p指向一个含3个int的数组 int (*p)(int); // 函数指针p指向一个返回int、带一个int参数的函数第一行和第二行就已经够让人迷糊的了第三行又加了括号和参数列表。关键在于[]和()在声明中的结合优先级高于解引用*。如果写成int *p[3]p先跟[3]结合说明“p是数组”数组元素是指针。而int (*p)[3]中括号让*p先结合说明“p首先是一个指针”它指向的对象是一个含3个int的数组。同理int (*p)(int)的意思是p是一个指针指向一个“返回int且接受一个int参数”的函数。记这种语法有个土办法看到声明先找最左边的标识符再看它右边有没有括号或方括号然后从内往外逐层剥开。这个习惯在解读复杂声明时特别受用。2.2 赋值的两种姿势和调用的三种姿势有了声明赋值就简单了。写一个普通函数int add(int a, int b) { return a b; }在另一个函数中int (*pFunc)(int, int) add; // 方式一直接使用函数名 int (*pFunc2)(int, int) add; // 方式二加上取地址符与方式一等价调用方式也一样灵活int result pFunc(3, 5); // 方式一指针名直接当函数用 int result2 (*pFunc)(3, 5); // 方式二显式解引用后再调用效果相同两种写法在绝大多数编译器和平台上结果完全一致因为函数指针在解引用的时候编译器并不需要真正做一次“读取内存”而是直接跳转到对应地址。汇编层面看两种情况最终都会生成call指令。不过我个人建议团队协作时统一一个风格定义函数指针类型后调用时直接用指针变量名当作函数名来写不要加*。原因有二第一代码更干净第二很多代码检查工具也更认这种写法。既然标准允许那就别写得花里胡哨。2.3 类型重定义为什么我强烈建议给函数指针起别名void (*)(int)这种类型写一次还好写多了绝对会让人想骂人。尤其是你在头文件里声明一个回调接口时满屏的指针加括号给后期维护带来极大的阅读负担。更优雅的做法是用typedeftypedef void (*Callback)(int event_id, void *user_data);这样原本一长串的类型被压缩成了一个单词Callback。后续声明变量、定义函数参数时都极其清爽void register_callback(Callback cb);用typedef给函数指针取别名并非单纯为了偷懒。更重要的是它把“函数指针类型”提升成了一个可以被命名的抽象概念这能在设计层面帮助团队统一接口表达。我见过一些团队从一开始没有这么做到后面维护代码时每次都要对照头文件里的原始声明去猜参数表效率非常低。我个人的风格是凡是函数指针作为参数类型出现两次以上就定义类型别名只出现一次且逻辑很简单的场景才直接用完整声明。这是一种判断上的平衡也建议你按这个原则形成自己的习惯。3. 函数指针数组与转移表用一张表干掉一堆if-else3.1 转移表的长相一个数组装的全是函数入口函数指针本身只是基础能力真正让它在工程里发光的是把它放进数组。这种数组叫作“函数指针数组”也叫转移表。它最典型的作用是用数组下标取代条件分支。举个例子一个命令行菜单程序传统写法很可能长这样switch (cmd_id) { case CMD_OPEN: open_file(); break; case CMD_SAVE: save_file(); break; case CMD_CLOSE: close_file(); break; default: printf(unknown command\n); break; }这个写法的问题在于每增加一个命令就要在switch里加一个case代码会越来越长可读性逐渐下降而且case分支很难被数据驱动。如果命令有几十个switch就成了一个又臭又长的怪物。用函数指针数组可以改成这样typedef void (*CommandHandler)(void); void open_file(void); void save_file(void); void close_file(void); CommandHandler handlers[] { [CMD_OPEN] open_file, [CMD_SAVE] save_file, [CMD_CLOSE] close_file, };调用侧只需要一行if (cmd_id 0 cmd_id CMD_MAX handlers[cmd_id] ! NULL) { handlers[cmd_id](); } else { printf(unknown command\n); }这个“一行调用”的价值在于业务逻辑和数据定义分离了。增加一个命令你只需要实现一个函数然后在数组里多登记一项主分发逻辑完全不用动。这就是转移表所谓的“以数据替代码”。3.2 数组初始化时的指定器写法可读性靠这个细节撑起来上面的代码示例里用到了C99标准引入的指定初始化器designated initializer[CMD_OPEN] open_file。这个写法可以显式地把下标和函数名对应起来特别适合命令编号可能不连续的场景。如果不用指定器代码可能会变成CommandHandler handlers[] {open_file, save_file, close_file};这写法有一个隐患当命令比较多时你得自己数顺序一旦中间漏了一个或写反一个编译器不会给任何提醒运行时的灾难却立刻显现。我在实际项目里就遇过类似事故那一次排查良久最终发现只是初始化顺序与枚举定义不一致。指定器写法还有一个额外好处当你要增加一个新命令但新命令在老命令后面时数组会自动判断大小不会因为初始化列表长度变了而遗漏。很多初入行的开发者不知道这个特性所以我觉得值得强调。3.3 实际应用扩展状态机、消息映射和插件注册转移表不止能用在做菜单上。它的核心场景是“某种离散的输入映射到一段确定的处理逻辑”。我可以列出三个常见典型场景状态机把“当前状态”当作数组下标数组里存的是该状态对应的处理函数。状态事件到来时通过查表找到处理函数执行后返回下一个状态。网络协议栈里的连接状态管理常按这个思路实现。消息映射GUI框架或者服务器框架里消息ID与处理函数之间的关系用一张表维护。收到消息后按消息ID直接找到处理入口省去大量if-else判断。插件注册程序启动时把插件提供的各个能力函数注册进一张全局表需要调用时按能力编号索引。这种方式让插件化设计的扩展点变得清晰可控。我自己做的模拟项目X里有个配置解析器内部就维护了一个“指令字到解析函数”的转移表。当初重构这段代码时把二十几个else-if分支压缩成了两三个查询函数代码行数下降了三百多行最明显的是后来查bug的时间也短了因为“发号规则”和“处理逻辑”彻底分开了。3.4 边界检查是转移表应用中的头号安全习惯用转移表有一个必须养成的习惯数组访问前一定要做边界检查和空指针检查。数组越界本身就够可怕了而如果越界后取到一个垃圾值再把它当作函数指针去调用那程序直接崩溃还是小事更严重的是可能被利用来执行恶意代码。所以每次调用前都必须检查if (cmd_id 0 || cmd_id CMD_MAX || handlers[cmd_id] NULL) { log_error(invalid command handler for id: %d, cmd_id); return; }在嵌入式项目中这个检查不是“建议”而是“强制”。毕竟跑在设备上的程序可没有宿主操作系统帮你阻止非法内存访问函数指针跳飞了大概率造成硬件异常。除检查之外另一个小技巧是在初始化表末尾加一个占位项比如[CMD_LAST] NULL并把它当作数组长度的标志同时还能作为“未实现命令”的兜底符。这在增量维护时特别好用新增命令编号只要在CMD_LAST之前定义表长自动扩展排查问题也直观。4. 回调函数把“行为”当参数传递是大型程序的分层关键4.1 回调和普通函数调用到底有什么区别很多初学者会把“通过函数指针调用函数”和“回调函数”混为一谈。其实两者的区别藏在控制流的反向上。普通函数调用是“我调用它”执行完它把结果还给我而回调是“我把自己的函数告诉对方让对方在合适的时机调用我”。更准确地说当你在一个函数A中通过参数接受另一个函数B的指针并在某个时机调用B那么B就是回调函数。这时候函数B的具体实现在写A的时候并不存在A只约定“具有某类函数签名”。这种约定在软件设计里就是“接口约束”。我用一个生活化的例子来解释你去餐厅吃饭服务员问你要不要加辣。如果你回答“要”那么厨房做菜时就会遵循你这个偏好来执行“加辣”这个动作。在这个过程里你并没有亲自去厨房炒菜而是把自己的口味偏好“登记”给了厨房厨房在出菜前“回调”了你的偏好。函数指针在C里干的就是这个活儿。4.2 标准库qsort一个能背下来就理解回调和排序隔离的经典案例C语言标准库的qsort是理解回调的入门标配。它的原型是void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));qsort负责实现通用排序逻辑分治、比较、交换。但它并不知道你要排序的到底是什么类型的数据所以“怎么比较”必须由你提供。这个“怎么比较”就是回调函数。如果要对一个结构体数组按某个字段排序typedef struct { int id; int score; } Student; int compare_by_score(const void *a, const void *b) { const Student *sa (const Student *)a; const Student *sb (const Student *)b; return (sa-score sb-score) - (sa-score sb-score); } Student arr[10]; // ... arr填充 qsort(arr, 10, sizeof(Student), compare_by_score);里面有几个关键点值得注意。compare_by_score接收的为什么是const void *因为qsort不知道你的具体数据类型它只能给你内存地址类型信息要靠你在比较函数里自己做恢复。这是C语言的一种“裸内存”视角。比较函数的返回值为什么通常用差比较return sa-score - sb-score;这种写法虽然简洁但在极端值下可能溢出。比如两个int差值超过INT_MAX结果是未定义行为。稳妥写法是上面示例中的两两相减比较再转成-1/0/1或者用if分支。排序算法从qsort的角度看不需要知道数据是学生、订单还是坐标点它只需要通过回调比较两个元素的先后顺序就能完成整个排序。这就是“通用算法与具体数据类型解耦”最标准的示范。你把这个原理吃透后面看bsearch、看各种框架里的遍历器都会有似曾相识的感觉。4.3 用回调实现观察者效果一个模块怎么通知外部“我变天了”真实项目里回调最常见的应用场景是事件通知或观察者模式。某公司内部框架的定时器模块就常用这个设计外部模块注册一个回调定时器超时后触发它。假设我们做一个温度提醒模块温度超过阈值就要通知上层// 模块头文件 typedef void (*TemperatureAlertCallback)(float temp, void *user_data); void temperature_monitor_set_callback(TemperatureAlertCallback cb, void *user_data); // 模块内部 static TemperatureAlertCallback s_cb; static void *s_user_data; void temperature_monitor_set_callback(TemperatureAlertCallback cb, void *user_data) { s_cb cb; s_user_data user_data; } static void check_temperature(void) { static float current_temp 25.0f; current_temp 1.0f; if (current_temp 30.0f) { if (s_cb) { s_cb(current_temp, s_user_data); } } }上层使用方这样注册void on_temperature_alert(float temp, void *user_data) { (void)user_data; printf(警告温度已达 %.1f°C\n, temp); } temperature_monitor_set_callback(on_temperature_alert, NULL);注意这里的user_data参数。很多初学者会问既然回调函数可以接收参数为什么还要专门搞一个void *user_data把上下文传进去原因在于回调函数被调用的那一刻外部系统可能并没有其他途径把“我是谁”或“我需要操作哪个对象”传递进来。user_data就是一个通用的后门让你可以把任意结构体指针、对象指针、上下文信息塞进去。这算是我在项目里见过的最常用的设计模式之一一个模块永远不知道业务层的需求它只通过回调把“事件发生”通知出去再由业务去做具体响应。这个边界一旦划清楚模块的复用性会大幅度提升。5. 二级指针与多级指针不是炫技是真实需求催生出的层级递进5.1 什么时候必须用二级指针修改实参指针的场景首当其冲C语言参数传递默认是值传递这一点在指针身上同样成立。如果你在A函数中把指针变量传入B函数B对指针本身做了重新赋值那么在A函数里这个变量的值并不会改变。举个最常见的例子void alloc_memory(int *p) { p (int *)malloc(sizeof(int)); *p 100; } int main(void) { int *ptr NULL; alloc_memory(ptr); *ptr 200; // 程序崩溃 }这段代码是教科书级的错误。为什么崩溃因为alloc_memory内部修改的只是ptr的一份拷贝函数返回后ptr仍然是NULL。这就好比你让邻居帮你收快递结果快递被放到了邻居家里没告诉你你却回自己家敲门当然拿不到。修复这种问题有两种思路。第一种是让函数返回分配好的指针int *alloc_memory(void) { return (int *)malloc(sizeof(int)); }第二种就是二级指针void alloc_memory(int **p) { *p (int *)malloc(sizeof(int)); *(*p) 100; }二级指针版本里p是一级指针ptr的地址通过*p malloc(...)可以直接修改ptr的值。如果你习惯用“*表示一层解引用”来理解那么*p在等号左边的时候其实就是在指定“外层的那个指针变量本身应该存储什么地址”。5.2 指针数组与二维数组名字像本质完全不一样二级指针还经常和“字符串数组”或“指针数组”联系在一起。先看两个容易混淆的声明char *names[5]; // 指针数组5个char*指针 char names[5][20]; // 二维数组5行20列的char存储块char *names[5]中每个元素是一个指针指针可以指向不同的字符串字面量每个字符串长度也可以不同char *names[] { Alice, Bob, Charlie };这里的names本身在函数参数传递时会退化成指向指针的指针char **。遍历时可以这样写void print_all(char **arr, int n) { for (int i 0; i n; i) { printf(%s\n, arr[i]); } }print_all(names, 3);传入的names就是一个char **类型。这个退化的过程和“一维数组在参数里退化成指针”是同一套规则数组名遇到了“数组下标”之外的使用场景就变成首元素的指针。二维数组char names[5][20]则完全不同它在内存里是一片连续的5 * 20字节区域。传参时退化成char (*)[20]即指向一行20个字符的数组指针跟char **完全不兼容。很多人把“二维数组”和“指针数组”混用编译器会报不兼容警告说实话这是新手最容易栽的坑之一。5.3 再往里走一层三级指针到底什么时候才需要三级指针在普通代码里很少出现。如果哪个初学者在自己的业务代码里写了一个char ***我大概率会觉得他在炫技或者没想清楚。但在某些特定场景三级指针是必要的比如有些字符串分割函数往往需要同时输出多个子字符串指针集合。假设一个函数需要把输入字符串按分隔符拆成一组子串int split_string(const char *input, char ***out_result);out_result为什么是三级指针因为函数在内部需要创建并返回一个char **数组而“返回一个二级指针”在通过参数传递时就需要“参数类型是二级指针的地址”自然就是char ***。这种代码在实际工作中不算多但框架级别的通用处理库里面并不罕见。理解它的关键是第几级指针代表你在第几层间接关系上做修改。你只要记住“修改一层指针需要传入它的地址地址的类型就是下一级指针”。5.4 我踩过的一个多级指针坑忘了初始化导致程序异常有一回我调一个开源库库的函数原型长这样int parse_config(const char *path, char ***result);我按文档调用但没注意result在传入前必须被初始化为NULL。结果库内部判断*result ! NULL时发现它指向一个垃圾值直接往那个垃圾地址里写指针导致段错误。后来翻源码才发现库作者默认调用者会把结果指针清零。从那以后我养成一个习惯所有输出型指针参数在调用前显式置空。就算调用的库文档里没写这一步也不会造成副作用还能掩盖部分未初始化问题。这个习惯救了我不少次推荐你也养成。6. 复杂声明拆解右左法则实战再难的类型也不是天书6.1 声明和表达式都遵循同一个优先规则理解你的语法困境在C语言中不管是声明还是表达式去掉括号后总有一套固定的优先级规则后缀操作符()和[]高于前缀操作符*。声明时这个规则体现得尤其明显。看这段声明int *f(int);f先跟(int)结合说明f是一个函数函数的返回值是int *。在看表达式中这等同于*f(int)是int。而int (*f)(int);由于括号的存在*f先结合所以f先是一个指针它指向的对象是一个函数函数接受int参数并返回int。很多教材把这个关系讲得比较复杂我习惯引导别人用“先右后左由内向外”的方式读后面会展开。6.2 右左法则的完整实操从int (*(*p)[5])(int *)说起右左法则Right-Left Rule是快速拆解复杂声明的实用工具步骤如下找到最左侧的标识符开始读。先看标识符的右边如果有()或[]先解释为“函数”或“数组”。再看标识符的左边如果遇到*解释为“指针”。继续往外扩展直到所有符号都被消费完。注意右左法则要和优先级规则结合需要先用优先级确定哪些部分先结合。拿一个复杂声明举例int (*(*p)[5])(int *);我们按法则拆解找到标识符p。p的左边是*右边是[5]。根据优先级[]优先结合所以p首先是一个包含5个元素的“数组”等一下这里p左侧有*但[5]优先级高所以先解释p是数组不仔细看(*p)[5]是被括号包起来的因此p先和*结合p是一个指针再和[5]结合p指向一个长度为5的数组。数组元素类型是什么往外看(*(*p)[5])前面的int右边的(*)(int *)表示每个元素是一个函数指针所指向的函数接受int*参数并返回int。整个声明连起来读p是一个指针它指向一个数组数组里有5个元素每个元素是一个函数指针这些函数返回int并接受一个int*参数。就算一星期没见过这种写法用右左法则也能一步步拆出来。虽然这种声明在真正的业务代码里不常见但面试题和部分底层代码比如信号处理器数组会用。掌握拆解方法以后至少不会看到一段声明就对着屏幕上火。6.3 用typedef层层简化两种路线对比复杂的声明不仅难写还难读最好的办法是用typedef把它拆成多层可读的“积木”。还是以上面的声明为例typedef int (*Handler)(int *); // Handler 是一个函数指针类型 typedef Handler HandlerArray[5]; // HandlerArray 是包含5个Handler的数组 HandlerArray *p; // p 是指向 HandlerArray 的指针这样一重写整个声明瞬间从“天书”变成了“可读的工程代码”。注意第二行的HandlerArray是一个数组类型并非指向数组的指针第三行才用*p去指向它。这就是“先构造类型别名再构造指针”的拆解思路。很多刚毕业的同事刚看到typedef化繁为简的写法时惊讶于C语言还能这样组织类型。其实抽象类型的层层定义恰恰是工程上驾驭复杂C代码的关键技能。在Linux内核源码里你会看到大量类似手法先定义基础操作类型再组合成更上层的描述结构最后通过指针串联起来。6.4 一个复杂声明的现实场景信号处理函数表复杂声明不只是面试题它在真实系统里是有位置的。比如某些系统里要维护一个信号处理函数表每个信号对应一个处理函数函数的签名是void handler(int signo)。这种情况下表的类型可能写成typedef void (*SignalHandler)(int); SignalHandler signal_handlers[NSIG];这也是一种“函数指针数组”和转移表本质相同只是把命令ID换成了信号编号。看看是不是比直接写void (*signal_handlers[NSIG])(int)舒服得多如果你在别人的代码里看到一大串复杂的声明先不要烦躁也别急着骂作者故意装。按右左法则拆拆完觉得实在难维护就自己用typedef重构。这个动作做多了之后你会发现复杂声明在C生态里真的不是用来吓唬人的它只是语言底层机制自然推演出来的结果。7. 从第三篇往后看指针能力的真正分水岭在哪里连续写完三篇“深入理解指针”我想结合自己的实战经验聊几句指针的学习路径。第一阶段是把指针当作“地址的别名”你学会了声明、赋值、解引用会通过指针访问普通变量和数组元素。第二阶段是理解“指针参与了类型系统”你意识到int*、int**、int (*)[5]、int (*)(int)这些类型之间存在着千丝万缕的关系能够自己解析复杂声明。第三阶段是“指针成为设计工具”你开始主动地用函数指针数组简化分支逻辑用回调函数解耦模块用二级指针管理动态字符串集合用指针与结构体的组合构建链表、二叉树等灵活数据结构。如果让我给一个学习建议实践一定要跟上。指针语法虽然可以在纸面上推导但内存布局、参数传递、回调触发的时机都只有真正在调试器里看过才能形成直觉。我个人推荐把一个个小例子在本地编译运行起来配合打印地址、查看变量内存、单步跟踪来验证推导结果。调试器里看到的东西比一百行文字解释都管用。第三篇讲到的内容——函数指针、转移表、回调、多级指针、复杂声明——每一项都有大量的工程应用。理解它们你就等于拿到了阅读开源代码的钥匙。往后当你再看到Linux内核、某个嵌入式RTOS或者网络库里的“奇怪指针”第一反应将不再是绕道走而是心里默念我知道你在干什么我也知道你为什么这么写。最后分享一个小技巧我梳理指针相关的知识点时习惯在纸上画出“内存格子”把变量、指针、指向对象之间的箭头关系画清楚。遇到想不通的指针问题就把这张图在脑子里过一遍。绝大多数时候问题根源要么是类型不匹配要么是操作对象和指针层级不匹配两者都能从图上看出来。画图是我在深入学习指针过程中最有价值的习惯没有之一。
阅读完成 · 觉得有帮助?
咨询建站