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

在这里插入图片描述


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 旁边放一颗 *,说明它是指针,指针指向的是函数,函数的参数类型是 intint,返回类型也是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 函数里,我们传了 AddSubMulDiv,它们都是不同的函数地址,

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

4. 回调函数

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

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

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

在上面的 计算器 小程序中,我们没有直接的去调用 AddSubMulDiv 这四个函数,而且传给了 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 排序

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

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

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

其实在 C 语言中有一个库函数叫 qsort,它是用快速排序的方法实现的。
在这里插入图片描述
里面有些参数是不需要用到的,整理一下,如下
在这里插入图片描述
第一个参数是 void 类型的指针;

第二个参数是 整型

第三个参数是 整型

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

下面我们对参数进行分析
在这里插入图片描述

  • base

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

  • num

元素个数(数组大小)

  • width

元素的宽度,也就是一个元素占几个字节
比如 int4 个字节、char1 个字节

  • compare

比较函数

  • elem1elem2

传给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 是创建的 比较函数 ,它的作用就是:比较 e1e2 指向的元素

*(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();
	return 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 函数里,调用 e1e2 去了;

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

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

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

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

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

这里拿结构体代码来举例👇
在这里插入图片描述
首先我创建的结构体类型一共是 28 个字节;

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

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

是不是还需要 base 指向的下一个元素的地址
在这里插入图片描述
其实 问号 处的地址很简单,我们来看 bubble_sort 函数内部的 if 语句:

1、当 j = 0 时,也就是 0 ∗ w i d t h 0 * width 0width,宽度就是 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* 的指针就可以理解为 1base 加多少,就跳过多少个字节的大小👇
在这里插入图片描述

  • 27
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 15
    评论
完整版:https://download.csdn.net/download/qq_27595745/89522468 【课程大纲】 1-1 什么是java 1-2 认识java语言 1-3 java平台的体系结构 1-4 java SE环境安装和配置 2-1 java程序简介 2-2 计算机中的程序 2-3 java程序 2-4 java类库组织结构和文档 2-5 java虚拟机简介 2-6 java的垃圾回收器 2-7 java上机练习 3-1 java语言基础入门 3-2 数据的分类 3-3 标识符、关键字和常量 3-4 运算符 3-5 表达式 3-6 顺序结构和选择结构 3-7 循环语句 3-8 跳转语句 3-9 MyEclipse工具介绍 3-10 java基础知识章节练习 4-1 一维数组 4-2 数组应用 4-3 多维数组 4-4 排序算法 4-5 增强for循环 4-6 数组和排序算法章节练习 5-0 抽象和封装 5-1 面向过程的设计思想 5-2 面向对象的设计思想 5-3 抽象 5-4 封装 5-5 属性 5-6 方法的定义 5-7 this关键字 5-8 javaBean 5-9 包 package 5-10 抽象和封装章节练习 6-0 继承和多态 6-1 继承 6-2 object类 6-3 多态 6-4 访问修饰符 6-5 static修饰符 6-6 final修饰符 6-7 abstract修饰符 6-8 接口 6-9 继承和多态 章节练习 7-1 面向对象的分析与设计简介 7-2 对象模型建立 7-3 类之间的关系 7-4 软件的可维护与复用设计原则 7-5 面向对象的设计与分析 章节练习 8-1 内部类与包装器 8-2 对象包装器 8-3 装箱和拆箱 8-4 练习题 9-1 常用类介绍 9-2 StringBuffer和String Builder类 9-3 Rintime类的使用 9-4 日期类简介 9-5 java程序国际化的实现 9-6 Random类和Math类 9-7 枚举 9-8 练习题 10-1 java异常处理 10-2 认识异常 10-3 使用try和catch捕获异常 10-4 使用throw和throws引发异常 10-5 finally关键字 10-6 getMessage和printStackTrace方法 10-7 异常分类 10-8 自定义异常类 10-9 练习题 11-1 Java集合框架和泛型机制 11-2 Collection接口 11-3 Set接口实现类 11-4 List接口实现类 11-5 Map接口 11-6 Collections类 11-7 泛型概述 11-8 练习题 12-1 多线程 12-2 线程的生命周期 12-3 线程的调度和优先级 12-4 线程的同步 12-5 集合类的同步问题 12-6 用Timer类调度任务 12-7 练习题 13-1 Java IO 13-2 Java IO原理 13-3 流类的结构 13-4 文件流 13-5 缓冲流 13-6 转换流 13-7 数据流 13-8 打印流 13-9 对象流 13-10 随机存取文件流 13-11 zip文件流 13-12 练习题 14-1 图形用户界面设计 14-2 事件处理机制 14-3 AWT常用组件 14-4 swing简介 14-5 可视化开发swing组件 14-6 声音的播放和处理 14-7 2D图形的绘制 14-8 练习题 15-1 反射 15-2 使用Java反射机制 15-3 反射与动态代理 15-4 练习题 16-1 Java标注 16-2 JDK内置的基本标注类型 16-3 自定义标注类型 16-4 对标注进行标注 16-5 利用反射获取标注信息 16-6 练习题 17-1 顶目实战1-单机版五子棋游戏 17-2 总体设计 17-3 代码实现 17-4 程序的运行与发布 17-5 手动生成可执行JAR文件 17-6 练习题 18-1 Java数据库编程 18-2 JDBC类和接口 18-3 JDBC操作SQL 18-4 JDBC基本示例 18-5 JDBC应用示例 18-6 练习题 19-1 。。。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Albert Edison

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值