为什么上期被隐藏了
胡曼不是一个人,是一种状态
Day 4 函数指针与回调
1. 函数指针
-
普通指针:存的是变量的地址,比如
int *p,指向一块存int的内存。 -
函数指针:存的是函数的入口地址,指向代码段里的函数,不是数据段。
函数本身有类型:返回值 + 参数列表。函数指针必须和它完全对上。
// 声明 fp 是一个函数指针
// 它指向:返回 int、吃两个 int 的函数
int (*fp)(int, int);
typedef 用来给类型起别名
typedef 返回值 (*类型名)(参数列表);
2. 函数回调
回调:把一个函数指针当参数传给另一个函数,由那个函数在内部调用它。
接收方不写死逻辑,逻辑由外部传进来的回调决定。
#include <stdio.h>
typedef int (*OpFunc)(int, int);
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; }
// calc:通用调度函数,op 是回调
int calc(int x, int y, OpFunc op)
{
if (op == NULL) return 0; // 防御:万一传了空指针
return op(x, y);
}
int main(void)
{
printf("add: %d\n", calc(4, 5, add));
printf("sub: %d\n", calc(4, 5, sub));
printf("mul: %d\n", calc(4, 5, mul));
return 0;
}
qsort 标准库(C 语言最经典的回调案例):
头文件:#include <stdlib.h>
原型:
void qsort(void *base,
size_t nmemb,
size_t size,
int (*compar)(const void *, const void *));
拆解参数:
-
base:数组首地址。void *是无类型指针,任何类型的数组都能传 -
nmemb:元素个数 -
size:单个元素占几字节 -
compar:比较回调
综合练习
#include <stdio.h>
#include <stdlib.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;}
typedef int (*replace)(int,int);
int call(int x,int y,replace op){
if(op == NULL){return 0;}
return op(x,y);
}
int cmp_int(const void *a,const void *b){
const int *pa = (const int*)a;
const int *pb = (const int*)b;
if(*pa < *pb){return -1;}
if(*pa > *pb){return 1;}
if(*pa == *pb){return 0;}
}
int cmp_str(const void *a,const void *b){
const char *const *pa = (const char *const*)a;
const char *const *pb = (const char *const*)b;
return strcmp(*pa,*pb);
}
int main(){
int (*fp)(int,int);
fp = add;
printf("test 6+5 = %d\n",fp(6,5));
replace su;
su = sub;
printf("test 6-5 = %d\n",su(6,5));
replace mu;
mu = mul;
printf("====c测试回调====\n");
printf("78 + 91 = %d\n",call(78,91,fp));
printf("78 - 91 = %d\n",call(78,91,su));
printf("78 * 91 = %d\n",call(78,91,mu));
printf("====qort回调排序int数组====\n");
int arr[] = {9,1,8,2,5};
int n = sizeof(arr) / sizeof(arr[0]);
printf("排序前:");
for (int i = 0; i < 5; i++)
{
printf("%d ",*(arr+i));
}
printf("\n");
qsort(arr,n,sizeof(int),cmp_int);
printf("排序后:");
for (int j = 0; j < 5; j++)
{
printf("%d ",*(arr+j));
}
printf("====字符串排序====\n");
char *str1[] = {"banana","apple","orange","watermelon"};
int nstr = sizeof(str1) / sizeof(str1[0]);
printf("排序前:\n");
for (int i = 0; i < 4; i++)
{
printf("%s ",str1[i]);
}
printf("\n");
qsort(str1,nstr,sizeof(str1[0]),cmp_str);
printf("排序后:\n");
for (int i = 0; i < 4; i++)
{
printf("%s ",str1[i]);
}
printf("\n");
return 0;
}
3.简易分发表(switch的替代品)
思想:用结构体数组存「匹配 key + 函数指针」,循环查表,命中就调用对应函数。
替代冗长的 if-else if / switch case。协议解析、命令分发、opcode 处理到处都是。
typedef int (*CmdFunc)(void);
typedef struct {
int cmd_id; // 命令码 / opcode
CmdFunc handler; // 对应的处理函数
const char *name; // 便于调试和打印
} CmdEntry;
完整练习
#include <stdio.h>
typedef int (*CmpFunc)(void);
typedef struct
{
int cmd_id;
CmpFunc handler;
const char *name;
}Cmdentry;
int cmd_hello(void){
printf("hello command\n");
return 0;
}
int cmd_f8(void){
printf("fei 8 fen qian\n");
return 0;
}
int cmd_test(void){
printf("a test command\n");
return 0;
}
int cmd_exit(void){
printf("exit\n");
return 1;
}
static const Cmdentry commandtest[] = {
{0x01,cmd_hello,"HELLO"},
{0x02,cmd_f8,"F8"},
{0x03,cmd_test,"TEST"},
{0x04,cmd_exit,"EXIT"},
{0x99,NULL, NULL },
};
int dispatch(int cmd_id){
int i;
for(i=0;commandtest[i].handler != NULL;i++){
if (commandtest[i].cmd_id == cmd_id)
{
return commandtest[i].handler();
}
}
printf("unknown cmd_id: 0x%02X\n",cmd_id);
return -1;
}
int main(void)
{
dispatch(0x01);
dispatch(0x02);
dispatch(0x03);
dispatch(0x04);
dispatch(0x99);
return 0;
}
为什么用哨兵(最后一行 handler = NULL):这样遍历时不用另外传一个"数组长度"参数,表加长缩短都不用改 dispatch。真实项目里两种都常见:哨兵,或者额外传 count。
逆向里看到一大片"整数 + 函数地址"交替出现的数组,几乎都是这种分发表。IDA 解析出来就是跳转表。
优点:新增命令不用改
dispatch,只需往数组加一行。
4. GDB验证
编译:gcc fp.c -g -o fp(Windows 上产物是 fp.exe,下面命令里的程序名相应带上)
b main
run
p fp # 打印函数指针里存的地址
p fp(3,4) # 直接在 GDB 里调用
p cmd_table[0].handler # 看表项里存的函数地址
p cmd_table[0].name
disass cmd_hello # 反汇编回调本体
部分调试:
(gdb) b dispatch
Breakpoint 2 at 0x4015d7: file D:\Dxt\c-learning\day4\day4_2.c, line 43.
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: D:\Dxt\c-learning\day4\day4_2.exe
[New Thread 55292.0x987c]
[New Thread 55292.0x142f4]
Thread 1 hit Breakpoint 1, main () at D:\Dxt\c-learning\day4\day4_2.c:57
57 dispatch(0x01);
(gdb) break dispatch
Note: breakpoint 2 also set at pc 0x4015d7.
Breakpoint 3 at 0x4015d7: file D:\Dxt\c-learning\day4\day4_2.c, line 43.
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) n
Program not restarted.
(gdb) p cmd_id
No symbol "cmd_id" in current context.
(gdb) n
Thread 1 hit Breakpoint 2, dispatch (cmd_id=1) at D:\Dxt\c-learning\day4\day4_2.c:43
43 for(i=0;commandtest[i].handler != NULL;i++){
(gdb) p cmd_id
$1 = 1
(gdb) n
44 if (commandtest[i].cmd_id == cmd_id)
(gdb) p cmd_id
$2 = 1
(gdb) n
46 return commandtest[i].handler();
(gdb) n
hello command
53 }
(gdb) n
main () at D:\Dxt\c-learning\day4\day4_2.c:58
58 dispatch(0x02);
(gdb) n
Thread 1 hit Breakpoint 2, dispatch (cmd_id=2) at D:\Dxt\c-learning\day4\day4_2.c:43
43 for(i=0;commandtest[i].handler != NULL;i++){
(gdb) p cmd_id
$3 = 2
(gdb) p commandtest[i]
$4 = {cmd_id = 1, handler = 0x401550 <cmd_hello>, name = 0x404031 "HELLO"}
(gdb)
要多理解

浙公网安备 33010602011771号