-- 二级指针与函数指针)
文章目录本篇先引入两个易混淆的问题1、为什么一级指针有时不够函数想改的是“指针变量”本身2、二级指针先找到指针变量再找到最终对象2.1 让函数真正改变调用者的指针2.2 二级指针作为输出参数2.3 多级指针不是“层数越多越强”3、函数也有入口地址但先看清它的签名4、最容易读错的声明函数指针变量4.1 赋值和调用分三步写最清楚4.2 用 typedef 让代码不再被复杂声明淹没4.3 注意类型匹配4.4 函数指针也可能为空5、回调函数把“怎么做”交给调用者6、函数指针数组把菜单选择和函数地址放进同一张表6.1 为什么它比一长串 if...else(分支) 更适合固定菜单7、对比二级指针与函数指针8、练习8.1 把一个指针改为指向更大的数8.2 用函数指针选择操作总结本篇先引入两个易混淆的问题前两篇中int *p的含义已经明确p保存一个int对象的地址*p能访问那个整数。但C中还有两类看起来更陌生的声明int**p;int(*function)(int,int);它们都带有多个符号却不是同一种的复杂int **pp关注的是数据地址有几层它适合让函数修改一个指针变量的内容。int (*function)(int, int)关注的是函数的参数和返回值它保存的是可调用函数的入口地址。阅读时不要只数星号读声明时始终从变量名向外读先判断它是“数据指针”还是“函数指针”再看每一层指向的内容。1、为什么一级指针有时不够函数想改的是“指针变量”本身先回顾一级指针能修改什么voidSetValue(int*p){if(p!NULL){*p100;}}SetValue(number)能修改number因为p保存的是number的地址*p就是number。但如果我们想让一个函数把调用者的p改为指向另一个整数下面写法为什么没有效果voidPointToWrong(int*p,int*target){ptarget;}原因仍然是 C 的值传递。调用PointToWrong(p, second)时函数得到的是指针值的副本。p target只改变了函数内部那份副本调用者手里的p没变。如果要修改调用者的指针变量就应把“指针变量的地址”传入。一个int *变量的地址类型正是int **。2、二级指针先找到指针变量再找到最终对象intvalue10;int*pvalue;int**ppp;可以逐层展开表达式类型当前含义valueint最终保存的整数 10valueint *value的地址pint *保存valuepint **指针变量p自己的地址ppint **保存p*ppint *找到p**ppint再沿着p找到value#includestdio.hintmain(){intvalue10;int*pvalue;int**ppp;printf(%d\n,**pp);// 10**pp20;printf(%d\n,value);// 20return0;}此处**pp 20最终修改的是value。但二级指针的主要用途通常不是“多绕一层改整数”而是使用*pp改写p的指向。2.1 让函数真正改变调用者的指针#includestdio.hvoidPointTo(int**pp,int*target){if(pp!NULL){*pptarget;}}intmain(){intfirst10;intsecond20;int*pfirst;PointTo(p,second);printf(%d\n,*p);// 20return0;}调用时传入的是p。进入函数后pp指向调用者的指针变量p*pp就是那个指针变量*pp target把p的内容从first改成second回到main后p因此指向second。不要写成**pp *target。那会把second的数值复制给first而不是修改p的指向。二级指针最容易错的地方就是没有分清“这一层要改地址”还是“下一层要改数据”。2.2 二级指针作为输出参数函数常常需要同时表达“操作是否成功”和“找到的地址是什么”。返回值适合表达成功或失败二级指针参数适合把一个指针结果带回调用者。#includestdio.hintFindFirstPositive(constint*arr,intsize,constint**result){if(arrNULL||resultNULL){return0;}*resultNULL;for(inti0;isize;i){if(arr[i]0){*resultarr[i];return1;}}return0;}一个使用方式如下constintdata[]{-3,-1,5,7};constint*first_positiveNULL;intsizesizeof(data)/sizeof(data[0]);if(FindFirstPositive(data,size,first_positive)first_positive!NULL){printf(找到第一个正数为%d\n,*first_positive);}运行结果2.3 多级指针不是“层数越多越强”int ***只是多了一层间接访问它不代表更高级的写法。每增加一层都增加一次“这个地址指向什么”的理解负担。实际代码中如果用结构体、清晰返回值或拆分接口能表达需求就不要为了炫技叠加星号。只有确实要修改某一层指针变量时二级或更多级指针才有意义。3、函数也有入口地址但先看清它的签名先看两个普通函数intAdd(inta,intb){returnab;}intMultiply(inta,intb){returna*b;}它们都接收两个int返回一个int。这份“参数列表 返回类型”叫函数签名。函数指针不能只知道“要调用 Add”还必须知道怎样传参数、如何接收结果因此函数指针类型必须与目标函数签名匹配。4、最容易读错的声明函数指针变量int(*operation)(int,int);从operation向外读(*operation)operation是一个指针(int, int)它指向一个接收两个int参数的函数最左侧int该函数返回int。因此它可以保存Add或Multiply的地址。括号不能省略int*operation(int,int);// 这是函数声明函数返回 int *int(*operation)(int,int);// 这才是函数指针变量第一句中operation(...)先与函数调用结合所以operation是函数第二句用括号让*operation先结合才让它成为指针变量。4.1 赋值和调用分三步写最清楚#includestdio.hintAdd(inta,intb){returnab;}intMultiply(inta,intb){returna*b;}intmain(){int(*operation)(int,int);intresult;operationAdd;resultoperation(2,3);printf(加法结果%d\n,result);operationMultiply;result(*operation)(2,3);printf(乘法结果%d\n,result);return0;}运行结果operation Add和operation Add都可以调用时operation(2, 3)与(*operation)(2, 3)也都可以。初学阶段可以把带星号的形式理解为“先从指针找到函数再调用”熟悉后使用较简洁的operation(2, 3)即可。4.2 用typedef让代码不再被复杂声明淹没typedefint(*Operation)(int,int);Operation operationAdd;Operation是“接收两个int并返回int的函数指针”的类型别名。它没有创造新规则只是把复杂声明起了一个可读的名字。4.3 注意类型匹配doubleDivide(doublea,doubleb){returna/b;}typedefint(*Operation)(int,int);Operation opDivide;// 错误签名不匹配Operation规定参数和返回值都是int而Divide使用double。不要用强制类型转换绕过这个问题调用方会按错误的方式准备参数或读取结果行为可能不可预测。要么定义另一个匹配的函数指针类型要么统一所有操作的接口。4.4 函数指针也可能为空typedefint(*Operation)(int,int);Operation operationNULL;if(operation!NULL){printf(%d\n,operation(1,2));}函数指针和数据指针一样空指针不能解引用这里的“解引用”表现为调用。若某个回调参数允许为空就要明确约定为空时应该跳过、使用默认行为还是返回错误。5、回调函数把“怎么做”交给调用者函数指针的实用的地方不只是“换一种方式调用 Add”而是让一个通用函数把具体行为交给调用者选择。typedefint(*BinaryOperation)(int,int);intCalculate(inta,intb,BinaryOperation operation,int*result){if(operationNULL||resultNULL){return0;}*resultoperation(a,b);return1;}Calculate不需要知道“加法”或“乘法”具体怎么写它只要求传入的函数遵守统一接口。调用者可以这样选择行为intresult;if(Calculate(4,5,Add,result)){printf(%d\n,result);// 9}if(Calculate(4,5,Multiply,result)){printf(%d\n,result);// 20}这个模式叫回调当前函数在合适的时机“回头调用”调用者提供的函数。它让通用流程和具体策略分离开。6、函数指针数组把菜单选择和函数地址放进同一张表当若干函数签名完全相同就可以把它们的地址放进数组typedefvoid(*Handler)();Handler menu[]{ShowHelp,StartGame,QuitGame};这仍然是“指针数组”只不过数组元素从数据地址变成了函数地址。下面是完整的最小菜单分发程序#includestdio.hvoidShowHelp(){puts(帮助输入选项编号后按回车);}voidStartGame(){puts(开始游戏);}voidQuitGame(){puts(准备退出);}intmain(){typedefvoid(*Handler)();Handler menu[]{ShowHelp,StartGame,QuitGame};size_tchoice;scanf(%zu,choice);size_tcountsizeof(menu)/sizeof(menu[0]);if(choicecountmenu[choice]!NULL){menu[choice]();}else{puts(无效选项);}return0;}运行示例6.1 为什么它比一长串if...else(分支) 更适合固定菜单下面写法在只有两个选项时很直观if(choice0){ShowHelp();}elseif(choice1){StartGame();}当选项很多、每个选项的参数和返回类型也一致时函数指针数组把“编号对应什么行为”集中成一张表分发代码保持不变。但它不一定适合所有场景如果每个操作需要不同数量或不同类型的参数就不应强行塞进同一个函数指针数组应重新设计统一接口或使用不同分发表。7、对比二级指针与函数指针写法保存的内容最常见用途调用或访问方式int *pint对象地址修改数据*pint **ppint *变量地址修改调用者的指针*pp targetint (*op)(int, int)函数入口地址选择一种计算op(a, b)void (*handlers[])(void)多个函数入口地址菜单、命令分发handlers[index]()看到复杂声明时可以先问“最里面的变量名是数组、函数还是指针”再根据括号逐层向外解释。能画出它保存的是什么地址代码就不会显得神秘。8、练习8.1 把一个指针改为指向更大的数要求编写PointToLarger。若两个地址都有效让*result指向较大的那个整数否则令*result为NULL。voidPointToLarger(constint*a,constint*b,constint**result);参考答案voidPointToLarger(constint*a,constint*b,constint**result){if(resultNULL){return;}*resultNULL;if(aNULL||bNULL){return;}if(*a*b){*resulta;}else{*resultb;}}这里用的是const int **因为函数改的是调用者的“结果指针”但不会修改a、b指向的整数。8.2 用函数指针选择操作要求为加法和减法定义统一函数指针类型写一个Calculate函数空回调时返回 0。参考答案typedefint(*Operation)(int,int);intAdd(inta,intb){returnab;}intSubtract(inta,intb){returna-b;}intCalculate(inta,intb,Operation operation,int*result){if(operationNULL||resultNULL){return0;}*resultoperation(a,b);return1;}检查点Operation的返回类型、两个参数类型必须同时与Add、Subtract匹配不能写成int *Operation(int, int)那会声明一个返回int *的函数总结地址不仅可以指向普通的数据变量也能够指向指针变量本身也就是可以存储指针变量的地址形成多级指针除此之外函数在内存中同样拥有专属的入口地址这类函数地址可以像普通数据一样存放到指针变量、数组等载体中程序运行时可以读取、保存该地址并根据需求动态选择调用对应的函数以此实现回调、多分支分发等灵活的程序逻辑。