函数指针 + 函数指针数组 + 回调函数

【指针内功修炼】函数指针 + 函数指针数组 + 回调函数

1. 函数指针

函数指针,顾名思义,就是指向函数的指针,函数 有没有地址呢?

#include <stdio.h>

void test()
{
  printf("hehe\n");
}

int main()
{
  printf("%p\n", test);
  printf("%p\n", &test);
  return 0;
}

运行结果👇

在这里插入图片描述

在数组里面,数组名 拿到首元素的地址;&数组名 拿到整个数组的地址。

在函数里面,函数名 和 &函数名 拿到的都是函数的地址,函数是没有首元素概念的。

函数的地址,就是函数存放的位置

那么函数的地址用什么来接收呢?代码如下👇

int Add(int x, int y)
{
  return x + y;
}

void test(char* str)
{}

int main()
{
  int arr[5];
  int (*pa)[5] = &arr; //pa是数组指针
  
  int (* pf)(int, int) = &Add; // pf是函数指针
  int (* pf)(int, int) = Add; // 也可以直接用函数名

  void (*pt)(char*) = test;

  return 0;
}

pf 旁边放一颗 *,说明它是指针,指针指向的是函数,函数的参数类型是 int、int,返回类型也是int

当然函数也有无返回类型,pt 先和 * 结合,说明 pt 是指针,指针指向的是一个函数,指向的函数无
参数,返回值类型为void。

那么我们如何调用这个函数呢?我们以 pf 为例👇

在这里插入图片描述

阅读两段有趣的代码:

在这里插入图片描述

代码一:

1、把 0 强制类型转换为 void (*)() 类型的函数指针

2、再去调用 0 地址处这个参数为 无参,返回类型是 void 的函数

这是依次函数调用,调用 0 地址处的函数

代码二:

signal 是一个函数声明;

这个函数的参数有 2 个,第一个是 int 类型,第二个是函数指针,该指针指向的函数参数 int,返回类型是 void;

signal 函数的返回类型也是函数指针,该指针指向的函数参数 int,返回类型是 void

是不是觉得代码二很复杂?那能不能把它简化呢?

我们可以这样写

在这里插入图片描述

2. 函数指针数组

数组是一个存放相同类型数据的存储空间,那我们已经学习了指针数组,如下

int* arr[10];
// 数组的每个元素是int*

那要把函数的地址存到一个数组中,那这个数组就叫 函数指针数组,那函数指针的数组如何定义呢?

int (*parr1[10])();
int *parr2[10]();
int (*)() parr3[10];

上面给出了 3 中定义方式,正确的是:parr1

parr1 先和 [ ] 结合,说明 parr1 是数组,数组的内容是什么呢?

是 int (*)() 类型的函数指针。

函数指针数组的用途

我们学习了 函数指针 和 函数指针数组,那么在实际场景如何运用呢?

我们这里以一个 计算器 小程序为例

示例一

写一个计算机小程序,能够实现基本的四则运算

📝代码实现

int Add(int x, int y)
{
  return x + y;
}
int Sub(int x, int y)
{
  return x - y;
}
int Mul(int x, int y)
{
  return x * y;
}
int Div(int x, int y)
{
  return x / y;
}

void menu()
{
  printf("**********************************\n");
  printf("*****  1. add     2. sub     *****\n");
  printf("*****  3. mul     4. div     *****\n");
  printf("*****  0. exit               *****\n");
  printf("**********************************\n");
}

int main()
{
  int input = 0;
  int x, y;
  int ret = 0;
  do
  {
    menu();
    printf("请选择:> ");
    scanf("%d", &input);
    switch (input)
    {
    case 1:
      printf("请输入两个整数:> ");
      scanf("%d %d", &x, &y);
      ret = Add(x, y);
      printf("ret = %d\n", ret);
      break;
    case 2:
      printf("请输入两个整数:> ");
      scanf("%d %d", &x, &y);
      ret = Sub(x, y);
      printf("ret = %d\n", ret);
      break;
    case 3:
      printf("请输入两个整数:> ");
      scanf("%d %d", &x, &y);
      ret = Mul(x, y);
      printf("ret = %d\n", ret);
      break;
    case 4:
      printf("请输入两个整数:> ");
      scanf("%d %d", &x, &y);
      ret = Div(x, y);
      printf("ret = %d\n", ret);
      break;
    case 0:
      printf("退出计算器\n");
      break;
    default:
      printf("输入错误,请重新输入\n");
    }
  } while (input);
  return 0;
}

运行结果👇

在这里插入图片描述

这样子写法是完全正确的,但是如果我要实现 按位与、按位或、左移、右移 呢?

那我岂不是又要去 mian 函数里面添加,这样子很麻烦,有没有化简实现呢?

这时候就需要用到使用 函数指针数组 去实现了;

示例二

对示例一的计算器进行化简升级

📝代码实现

int Add(int x, int y)
{
  return x + y;
}
int Sub(int x, int y)
{
  return x - y;
}
int Mul(int x, int y)
{
  return x * y;
}
int Div(int x, int y)
{
  return x / y;
}


void menu()
{
  printf("**********************************\n");
  printf("*****  1. add     2. sub     *****\n");
  printf("*****  3. mul     4. div     *****\n");
  printf("*****  0. exit               *****\n");
  printf("**********************************\n");
}

int main()
{
  int input = 0;
  int x = 0;
  int y = 0;
  int ret = 0;

  int (*pfArr[5])(int, int) = { 0, Add, Sub, Mul, Div };//pfArr是一个函数指针的数组,也叫转移表

  do
  {
    menu();
    printf("请选择:>");
    scanf("%d", &input);
    if (input == 0)
    {
      printf("退出计算器\n");
      break;
    }
    else if (input >= 1 && input <= 4)
    {
      printf("输入2个操作数:>");
      scanf("%d %d", &x, &y);
      ret = pfArr[input](x, y);
      printf("ret = %d\n", ret);
    }
    else
    {
      printf("选择错误\n");
    }
  } while (input);

  return 0;
}

当然运算结果也肯定是一样的,这里就不演示了

3. 指向函数指针数组的指针

指向函数指针数组的指针是一个 指针

指针指向一个 数组 ,数组的元素都是 函数指针 ;

如何定义?

这里给举个例子👇

在这里插入图片描述

可能看到这里还是有点懵😵

别急,我们把上面的 计算器 小程序重新来改进一下

📝代码实现

void menu()
{
  printf("**********************************\n");
  printf("*****  1. add     2. sub     *****\n");
  printf("*****  3. mul     4. div     *****\n");
  printf("*****  0. exit               *****\n");
  printf("**********************************\n");
}

int Add(int x, int y)
{
  return x + y;
}
int Sub(int x, int y)
{
  return x - y;
}
int Mul(int x, int y)
{
  return x * y;
}
int Div(int x, int y)
{
  return x / y;
}

void calc(int (*pf)(int, int))
{
  int x = 0;
  int y = 0;
  int ret = 0;
  printf("输入2个操作数:>");
  scanf("%d %d", &x, &y);
  ret = pf(x, y);
  printf("ret = %d\n", ret);
}

int main()
{
  int input = 0;

  do
  {
    menu();
    printf("请选择:>");
    scanf("%d", &input);
    switch (input)
    {
    case 1:
      calc(Add);
      break;
    case 2:
      calc(Sub);
      break;
    case 3:
      calc(Mul);
      break;
    case 4:
      calc(Div);
      break;
    case 0:
      printf("退出计算器\n");
      break;
    default:
      printf("选择错误\n");
      break;
    }
  } while (input);

  return 0;
}

当然运行结果肯定也和上面一样的

分析一下 calc 函数

在这里插入图片描述

在 main 函数里,我们传了 Add、Sub、Mul、Div,它们都是不同的函数地址,

那么在 calc 函数里面,我们要用指针 pf 来接收,函数类型是 int、int,返回类型是 void

4. 回调函数

回调函数就是一个 通过函数指针调用的函数。

如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。

回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或
条件进行响应。

在上面的 计算器 小程序中,我们没有直接的去调用 Add、Sub、Mul、Div 这四个函数,而且传给了 calc,当在 calc 函数内部时,准备了 2 个数去计算,我们直接通过 pf 指针去计算。

是不是有点难理解?别急,这里再通过几个示例来讲讲 回调函数

🍑 冒泡排序
一说到排序,脑海里面第一时间想到的都是冒泡排序,那么我这里就来写一个冒泡排序

📝代码实现

//冒泡排序

void bubble_sort(int arr[], int sz) {
  int i = 0;
  int j = 0;
  //趟数
  for (i = 0; i < sz - 1; i++) {
    //每一趟冒泡排序的过程
    //确定的一趟排序中比较的对数
    for (j = 0; j < sz - 1 - i; j++) {
      if (arr[j] > arr[j + 1]) {
        int tmp = arr[j + 1];
        arr[j + 1] = arr[j];
        arr[j] = tmp;
      }
    }
  }
}

void print(int arr[], int sz) {
  int i = 0;
  for (i = 0; i < sz; i++) {
    printf("%d ", arr[i]);
  }
}

int main()
{
  int arr[] = { 1,4,2,6,5,3,7,9,0,8 };
  int sz = sizeof(arr) / sizeof(arr[0]);

  bubble_sort(arr, sz);

  print(arr, sz);
  return 0;
}

运行结果

在这里插入图片描述

🍑 qsort 排序
上面的冒泡排序使用起来当然很好,但是会存在一个问题,就是使用起来会很局限?

假设我要对 float、struct 等其它类型的数据进行排序呢?

那么有没有一种现成的排序模板呢?

其实在 C 语言中有一个库函数叫 qsort,它是用快速排序的方法实现的。

在这里插入图片描述

里面有些参数是不需要用到的,整理一下,如下

在这里插入图片描述

第一个参数是 void 类型的指针;

第二个参数是 整型;

第三个参数是 整型;

第四个参数是 函数指针,compare 和 * 结合,说明它是指针,指针外面指向一个函数,该函数有 2 个参数,返回类型是 int

下面我们对参数进行分析

在这里插入图片描述

  • base

目标数组是开始。 你要排序的数据的起始位置就需要传到 base 里面,

  • num

元素个数(数组大小)

  • width

元素的宽度,也就是一个元素占几个字节 比如 int 占 4 个字节、char 占 1 个字节

  • compare

比较函数

  • elem1 和 elem2

传给compare 用来比较的元素

对于 compare,还要遵循几个因素

在这里插入图片描述

如果 elem1 指向的元素 小于 elem2 指向的元素,那么就返回 小于0 的数字;

如果 elem1 指向的元素 大于 elem2 指向的元素,那么就返回 大于0 的数字;

如果 elem1 指向的元素 等于 elem2 指向的元素,那么就返回 0 ;

qsort 排序整型数组
了解 qsort 函数的基本情况以后,我们就来使用它,对上面的 arr 数组重新进行排序;

📝代码实现

#include <stdio.h>
#include <stdlib.h>

void print(int arr[], int sz) {
  int i = 0;
  for (i = 0; i < sz; i++) {
    printf("%d ", arr[i]);
  }
}

void cmp_int(const void* e1, const void* e2) {
  return *(int*)e1 - *(int*)e2;
}

int main()
{
  int arr[] = { 1,4,2,6,5,3,7,9,0,8 };
  int sz = sizeof(arr) / sizeof(arr[0]);

  qsort(arr, sz, sizeof(arr[0]), cmp_int);

  print(arr, sz);
  return 0;
}

cmp_int 是创建的 比较函数 ,它的作用就是:比较 e1 和 e2 指向的元素

(int)e1 :对 e1 强制类型转换为 int*, 然后解引用,访问一个字节;

(int)e2 :对 e2 强制类型转换为 int*, 然后解引用,再向后访问一个字节;

这里要讲一下关于 void* 的知识点

  • void* 是一种无类型的指针,无具体类型的指针
  • void* 的指针变量可以存放任意类型的地址
  • void* 的指针不能直接进行解引用操作
  • void* 的指针不能直接进行 ±整数

qsort 排序结构体
我们定义一个结构体,要求结构体里面有 姓名、年龄、成绩;

要求使用 qsort函数按照 成绩 来对结构体成员排序

📝代码实现

#include <stdio.h>
#include <stdlib.h>

struct Stu
{
  char name[20];
  int age;
  float score;
};

int cmp_stu_by_socre(const void* e1, const void* e2)
{
  if (((struct Stu*)e1)->score > ((struct Stu*)e2)->score)
  {
    return 1;
  }
  else if (((struct Stu*)e1)->score < ((struct Stu*)e2)->score)
  {
    return -1;
  }
  else
  {
    return 0;
  }
}

void print_stu(struct Stu arr[], int sz)
{
  int i = 0;
  for (i = 0; i < sz; i++)
  {
    printf("%s %d %f\n", arr[i].name, arr[i].age, arr[i].score);
  }
  printf("\n");
}

void test1()
{
  struct Stu arr[] = { {"zhangsan",20,87.5f},{"lisi",22,99.0f},{"wangwu", 10, 68.5f} };

  int sz = sizeof(arr) / sizeof(arr[0]);

  // 按照成绩来排序
  qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_socre);
  
  //打印结构体成员
  print_stu(arr, sz);
  return 0;
}

int main()
{
  test1();
  retur

n 0;
}
运行结果👇

在这里插入图片描述

还可以按照 年龄 来排序

📝代码实现

#include <stdio.h>
#include <stdlib.h>

struct Stu
{
  char name[20];
  int age;
  float score;
};

int cmp_stu_by_age(const void* e1, const void* e2)
{
  return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}

void print_stu(struct Stu arr[], int sz)
{
  int i = 0;
  for (i = 0; i < sz; i++)
  {
    printf("%s %d %f\n", arr[i].name, arr[i].age, arr[i].score);
  }
  printf("\n");
}

void test1()
{
  struct Stu arr[] = { {"zhangsan",20,87.5f},{"lisi",22,99.0f},{"wangwu", 10, 68.5f} };

  int sz = sizeof(arr) / sizeof(arr[0]);

  // 按照年龄来排序
  qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_age);

  print_stu(arr, sz);
  return 0;
}

int main()
{
  test1();
  return 0;
}

运行结果👇

在这里插入图片描述

当然,肯定还可以按照 姓名 来排序

📝代码实现

#include <stdio.h>
#include <stdlib.h>

struct Stu
{
  char name[20];
  int age;
  float score;
};

int cmp_stu_by_name(const void* e1, const void* e2)
{
  return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}

void print_stu(struct Stu arr[], int sz)
{
  int i = 0;
  for (i = 0; i < sz; i++)
  {
    printf("%s %d %f\n", arr[i].name, arr[i].age, arr[i].score);
  }
  printf("\n");
}

void test1()
{
  struct Stu arr[] = { {"zhangsan",20,87.5f},{"lisi",22,99.0f},{"wangwu", 10, 68.5f} };

  int sz = sizeof(arr) / sizeof(arr[0]);
  
  qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_name);
  
  print_stu(arr, sz);
  
  return 0;
}

int main()
{
  test1();
  return 0;

}
运行结果👇

在这里插入图片描述

注意:名字 排序是根据首字母的 ASCII 码值来比较的;

模拟实现 qsort
既然学会了 qsort 函数的使用,那么我们来模仿 qsort 的功能实现一个通用的冒泡排序;

📝代码实现

#include <stdio.h>

int cmp_int(const void* e1, const void* e2)
{
  return *(int*)e1 - *(int*)e2;
}

void print_arr(int arr[], int sz)
{
  int i = 0;
  for (i = 0; i < sz; i++)
  {
    printf("%d ", arr[i]);
  }
  printf("\n");
}

void Swap(char* buf1, char* buf2, int width)
{
  int i = 0;
  for (i = 0; i < width; i++)
  {
    char tmp = *buf1;
    *buf1 = *buf2;
    *buf2 = tmp;
    buf1++;
    buf2++;
  }
}

void bubble_sort(void* base, int sz, int width, int(*cmp)(const void* e1, const void* e2))
{
  int i = 0;
  for (i = 0; i < sz - 1; i++)
  {
    int j = 0;
    for (j = 0; j < sz - 1 - i; j++)
    {
      if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
      {
        //两个元素的交换
        Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
      }
    }
  }
}

void test2()
{
  int arr[] = { 9,8,7,6,5,4,3,2,1,0 };

  int sz = sizeof(arr) / sizeof(arr[0]);

  bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);

  print_arr(arr, sz);
}

int main()
{
  test2();
  return 0;
}

运行结果👇

在这里插入图片描述

其实这段代码很简单,但是理解起来的话,还是有点难度,别急,下面我们一起来看一看

代码解析:

我们先来梳理一下调用逻辑:

在这里插入图片描述

首先我们从 test2 函数出发,在函数内部,调用 bubble_sort 函数

在 bubble_sort 函数的参数部分:

1、arr 传的就是数组第 1 个元素 9 的地址,形参部分用 base 接收

2、sz 就是元素个数 10,形参部分用 sz 接收

3、sizeof(arr[0]) 就是一个 int 的大小 4 个字节,形参部分用 width 接收

4、cmp_int 是一个函数,相当于传的就是函数的地址,形参部分用指针 cmp 接收,cmp 就指向创建的 cmp_int 函数

我们再来详细说下 bubble_sort 函数和 cmp_int 函数

在这里插入图片描述

我们在 if 语句里面传的两个值,其实通过 函数指针 跑去 cmp_int 函数里,调用 e1 和 e2 去了;

把 (char*)base + j * width 传给了 e1,(char*)base + (j + 1) * width 传给了 e2,然后 e1 和 e2 进行计算,计算好之后的结果(返回类型是 int)再与 if 语句里面的 0 进行比较;

如果返回结果是 大于0 的,说明 e1 大于 e2,那我们就把 e1 和 e2 进行交换,而交换的时候,又去调用我们的 Swap 函数;

所以核心思路就是:把 cmp_int 函数的地址传给 bubble_sort,用一个 cmp 指针来接收,再通过这个 cmp 指针去调用这个函数的。

关于 Swap 函数,这里就不过多解释,其实就是个交换的实现。

❗ 这里在重点说一下:(char*)base + j * width, (char*)base + (j + 1) * width 这段代码

这里拿结构体代码来举例👇

在这里插入图片描述

首先我创建的结构体类型一共是 28 个字节;

在 test1 函数里,定义的 arr 一共有 3 个结构体,那么在内存中应该是下面这样👇

在这里插入图片描述

在 test1 函数里:

把 arr 传给了 base,base 相当于指向了第一个 28 的地址,但是如果我们要比较第一个 28 和第二个 28 呢?

是不是还需要 base 指向的下一个元素的地址

在这里插入图片描述

其实 问号 处的地址很简单,我们来看 bubble_sort 函数内部的 if 语句:

1、当 j = 0 时,也就是 0 ∗ w i d t h 0 * width0∗width,宽度就是 sizeof(arr[0]) 的计算结果,也就是 28;

所以就是:(char*)base + 0 和 (char*)base + 28

因为我是 char* 强转之后的 base,所以 (char*) base + 0 还是首元素的地址;

因为我是 char* 类型的指针,所以 (char*) base + 28 跳过 28 个字节,因为这个结构体正好是 28 个字节👇

在这里插入图片描述

2、当 j = 1 时

就是:(char*) base + 1 * 28 和 (char*) base + (1 + 1) * 28;

因为我们是把 base 强制类型转换成了 char* 的指针,而 char* 的指针就可以理解为 1,base 加多少,就跳过多少个字节的大小👇

在这里插入图片描述

原文链接:《【指针内功修炼】函数指针 + 函数指针数组 + 回调函数(二)》

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值