【C进阶】回调函数(指针进阶2,详解,小白必看)

目录

6. 函数指针数组

6.1简单计算器

6.2函数指针数组实现计算器

7. 指向函数指针数组的指针(仅作了解即可)

8.回调函数

8.1关于回调函数的理解​编辑

8.1.1用回调函数改良简单计算器

8.2qsort库函数的使用

8.2.1冒泡排序

8.2.2qsort的概念

8.3冒泡排序思想实现qsort


         这篇文章包括但不限于函数指针数组指向函数指针数组的指针,回调函数等知识点的总结。承接着上文指针进阶(1)知识点总结,传送门-- > http://t.csdn.cn/mgVGJ

指针进阶(3):指针和数组笔试题解析总结,传送门--> http://t.csdn.cn/aKVsj 

        如有错误,欢迎大家指点与纠正,感谢你的来访!

6. 函数指针数组

数组是一个存放相同类型数据的存储空间,那我们已经学习了指针数组,
比如:
int * arr [ 10 ];
//数组的每个元素是int*

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

int ( * parr1 [ 10 ])();//1
int * parr2 [ 10 ]();//2
int ( * )() parr3 [ 10 ];//3

 答案是:parr1 //1

parr1 先和 [] 结合,说明 parr1 是数组,数组的内容是什么呢?
int (*)() 类型的函数指针。-->对于一个变量来说去掉它的名字,就是它的类型。
int my_strlen(const char* str)
{
	return 0;
}
int main()
{
  //指针数组
	char* arr[10];
  //数组指针
	int arr2[5] = { 0 };
	int(*p)[5] = &arr2;//p是一个指向数组的指针变量

	//函数指针
    int (*pf)(const char*) = my_strlen;//pf是一个指向函数的函数指针变量
	
	//函数指针数组-存放函数指针的数组
	int (*pfArr[10])(const char*);
    //1.pf先与[10]结合,代表这是一个数组,数组有10个元素;
    //2.去掉pf[10],剩余int (*)(int,int)为数组每个元素的类型--函数指针类型

	return 0;
}

以下三种写法均等价
1.   *(*pf)("abcdef");
2.   pf ("abcdef");
3.   my_strlen("abcdef");

我们通过实现一个计算器的函数来说明,函数指针数组的用途用在哪里:

6.1简单计算器

按照咱们正常的思路,写一个简单的计算器实现整数的加减乘除的功能,应该大致写成这种代码:

【C进阶】回调函数(指针进阶2,详解,小白必看)_第1张图片

那么执行一下,看一下状况如何:

【C进阶】回调函数(指针进阶2,详解,小白必看)_第2张图片

以上出现的问题是,输入0之后,没有立即打印"退出计算器"然后结束程序,而是要继续输入两个操作数之后才打印信息,还顺便打印了个6,这样的代码肯定存在问题,咱们稍微改进一下。

//写一个计算器能完成整数的+ - * /
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;
	do {
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			printf("请输入两个操作数:>");
			scanf("%d %d", &x, &y);
			ret = Add(x, y);
			printf("%d\n", ret);
			break;
		case 2:
			printf("请输入两个操作数:>");
			scanf("%d %d", &x, &y);
			ret = Sub(x, y);
			printf("%d\n", ret);
			break;
		case 3:
			printf("请输入两个操作数:>");
			scanf("%d %d",&x,&y);
			ret = Mul(x, y);
			printf("%d\n", ret);
			break;
		case 4:
			printf("请输入两个操作数:>");
			scanf("%d %d", &x, &y);
			ret = Div(x, y);
			printf("%d\n", ret);
			break;
		case 0:
			printf("退出计算器\n");
			break;
		default :
			printf("选择错误\n");
			break;
		}
	}while(input);
	return 0;
}

执行代码,继续测试:

【C进阶】回调函数(指针进阶2,详解,小白必看)_第3张图片

 问题是解决了,可是又发现新的问题:

【C进阶】回调函数(指针进阶2,详解,小白必看)_第4张图片

 以上的代码的switch case语句太多了,当我想要在这个计数器里面增加: << >> & | ^ && ||等功能,case语句的就会越来越多,那应该怎么去纠正呢?

这时候就要用到函数指针数组了。

例子:

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;
}
int main()
{
	//存放函数指针的数组 - 函数指针数组
	int (*pf[4])(int, int) = {Add,Sub,Mul,Div };
	                        // 0    1   2   3
	int i = 0;
	for (i = 0; i < 4; i++)
	{
		int ret = pf[i](8, 4);
		printf("%d\n", ret);
	}

	return 0;
}

【C进阶】回调函数(指针进阶2,详解,小白必看)_第5张图片

这段代码使用了函数指针数组,将四个运算函数的地址存储在数组中,然后通过循环遍历数组,依次调用四个运算函数进行计算并输出结果。这种方式可以减少代码的重复量,提高代码的可维护性。

 6.2函数指针数组实现计算器

//函数指针数组实现计算器
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) = { NULL,Add,Sub,Mul,Div };
							 //   0    1   2   3    4
	//NULL的作用是为了占用0下标,然后让函数指针数组的元素下标对应上菜单的功能
	do {
		//菜单
		menu();
		//进行选择
		printf("请选择:>");
		scanf("%d", &input);
		//用函数指针数组代替switch
		if (input == 0)
		{
			printf("退出计算器\n");
			return 0;
		}
		else if (input >= 1 && input <= 4)
		{
			printf("请输入两个操作数:>");
			scanf("%d %d",&x,&y);
			ret = (***pfArr[input])(x, y);//其实比函数指针的用法就多了一个[input]
			 //跟函数指针一样,调用函数的时候 * 是没有任何意义的
			printf("%d\n",ret);
		}
		else
		{
			printf("选择错误\n");
		}
	} while (input);
	return 0;
}

【C进阶】回调函数(指针进阶2,详解,小白必看)_第6张图片

函数指针数组是一个数组,其中的元素是函数指针。每个函数指针指向一个特定的函数,可以通过函数指针调用相应的函数。 函数指针数组可以用来解决代码冗余的问题,可以方便地遍历、调用不同的函数,从而简化代码的结构和逻辑,提高代码的可维护性和可扩展性。

函数指针数组的用途:转移表

7. 指向函数指针数组的指针(仅作了解即可)

//指向函数指针数组的指针
int Add(int a, int b)
{
	return a + b;
}
int Sub(int a, int b)
{
	return a - b;
}
int main()
{   //函数指针
	int(*pf)(int, int) = Add;
	//函数指针数组
	int (*pfArr[4])(int, int) = { Add,Sub };
	//指向函数指针数组的指针
	 int (*(*ppfArr)[4])(int,int) = &pfArr;

	 return 0;
}

如何理解指向函数指针数组的指针:

【C进阶】回调函数(指针进阶2,详解,小白必看)_第7张图片

8.回调函数

概念

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。
回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
代码冗余问题:第一版简单的计算器存在的问题
【C进阶】回调函数(指针进阶2,详解,小白必看)_第8张图片

存在着较多逻辑相同,代码冗余的部分。

 这时候就需要用到回调函数解决问题了:

8.1关于回调函数的理解【C进阶】回调函数(指针进阶2,详解,小白必看)_第9张图片

8.1.1用回调函数改良简单计算器

#include
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;
}
//将冗余重复部分用Calc函数封装(只用了一个函数),通过这个函数里面传入的函数指针调用计算器函数
void Calc(int(*pf)(int, int))//pf函数指针,传过来哪个函数地址就用哪种类型计算
{
	int x = 0;
	int y = 0;
	int ret = 0;
	printf("请输入两个操作数:>");
	scanf("%d %d", &x, &y);
    ret = pf(x, y);
	printf("%d\n", ret);
}
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 ret = 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;
}

总结:

回调函数:被作为参数传递的函数,Add、Sub、Mul、Div四个函数就是回调函数,

而Calc函数则是工具人

8.2qsort库函数的使用

8.2.1冒泡排序

回顾一下冒泡排序的过程:

冒泡排序的思想:两两相邻的元素进行比较,假设要排成升序【C进阶】回调函数(指针进阶2,详解,小白必看)_第10张图片

怎么去写冒泡排序的代码:
①由元素个数确定趟数

②由趟数确定比较对数         

③两个元素两两交换排成升序

//冒泡排序 
void bubble_sort(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz - 1; i++)//趟数
	{
		int j = 0;
		for (j = 0; j < sz - 1 - i; j++)//比较对数
		{
			if (arr[j] > arr[j + 1])
			{
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
		}
	}
}
void print_arr(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}
int main()
{
	int arr[] = { 9,8,7,6,5,4,3,2,1,0};
	//排序
	//使用
	int sz = sizeof(arr) / sizeof(arr[0]);

	bubble_sort(arr, sz);

	print_arr(arr, sz);
	return 0;
}

【C进阶】回调函数(指针进阶2,详解,小白必看)_第11张图片

不足:

但是这个冒泡排序的缺点也很明显,就是只能排整型int

如果遇到浮点型、结构体等类型的数据,就排不了,那么怎么可以解决呢?

这时候就要用到qsort了。

8.2.2qsort的概念

qsort-- quicksort

是一个库函数,是用来排序的库函数使用快速排序的方法

qsort的好处是

1.现成的

2.可以排序任意类型的数据

【C进阶】回调函数(指针进阶2,详解,小白必看)_第12张图片

 【C进阶】回调函数(指针进阶2,详解,小白必看)_第13张图片

qsort是可以排序任意类型的数据

1.比较2个整数的大小,> < ==

//qsort函数的使用者提供这个函数
//qsort 默认是升序
int cmp_int(const void* p1, const void* p2)
{
	return *(int*)p1 - *(int*)p2;
    //排成倒序的话,什么都不用动,只需要return *(int*)p2 - *(int*)p1;//调换顺序即可
}
void print_arr(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}
test1()
{
	int arr[] = { 3,1,5,2,4,9,8,6,5,7 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	//使用qsort来排序整型数组,这里就要提供一个比较函数,这个比较函数能够比较2个整数的大小
	qsort(arr, sz, sizeof(arr[0]), cmp_int);
	print_arr(arr, sz);
}
int main()
{
   test1();
}

 【C进阶】回调函数(指针进阶2,详解,小白必看)_第14张图片

2.比较2个字符串,strcmp -->回顾字符串知识:http://t.csdn.cn/V7E9a

3.比较2个结构体数据(学生:张三、李四)指定比较的标准

回顾结构体对象访问成员的知识:http://t.csdn.cn/DVEVj

//测试qsort 排序结构体数据
struct Stu {
	char name[20];
	int age;
};
void print_arr1(struct Stu* arr, int sz)//打印年龄
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		//printf("%d ", (*(arr + i)).age);
		//printf("%d ", (arr+i)->age);
		printf("%d ", arr[i].age);

	}
	printf("\n");
}
void print_arr2(struct Stu*arr, int sz)//打印姓名
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%s ", (arr+i)->name);
		//printf("%s ", (*(arr+i)).name);
		//printf("%s ", arr[i].name);

	}
	printf("\n");
}
//按照年龄来比较
int cmp_stu_by_age(const void*p1,const void* p2)
{
	return ((struct Stu*)p1)->age - ((struct Stu*)p2)->age;

}
int cmp_stu_by_name(const void* p1, const void* p2)
{
	return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);
}
void test2()
{
	struct Stu s[] = { {"zhangsan",30},{"lisi",25},{"wangwu",50} };
	int sz = sizeof(s) / sizeof(s[0]);
	//测试按照年龄来排序
	print_arr1(s, sz);
	qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);
	print_arr1(s, sz);
	//测试按照名字来排序
	/*print_arr2(s,  sz);
	qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);
	print_arr2(s, sz);*/
}

打印年龄:

【C进阶】回调函数(指针进阶2,详解,小白必看)_第15张图片

打印性别:

【C进阶】回调函数(指针进阶2,详解,小白必看)_第16张图片

8.3冒泡排序思想实现qsort

使用冒泡排序的思想来实现一个类似于qsort这个功能的冒泡排序函数bubble_sort()

各参数组成:
base,num,width,cmp函数,elem1和elem2

接下来说明一下qsort各参数的设计思路:

①为什么base定义为void*类型?

void*是 C 语言中的一种通用指针类型,可以指向任意类型的数据。在 qsort 函数中,由于需要对不同类型的数据进行排序,因此需要使用void*类型的指针,以便能够接受各种类型的数据,base参数是一个指向要排序数组第一个元素的指针。由于它要指向任意类型的数据,所以使用void*是最通用的。

举例:

int a = 10;

void* p = &a;

优点:

无具体类型的指针,所以它可以接收任何类型的地址

缺点:

*p  //-->  void*的指针不能解引用操作符

p++  //-->  因为不知道是什么数据类型,不能直接++

正确用法:

*(int*)p;//也可以转化成其它类型

②为什么num要设计成size_t?

  • size_t 在 C 语言中是一种无符号整数类型,用于表示大小、长度和索引值,使用它表示数组元素个数很合适。
  • 数组元素个数是一个非负的值,使用无符号类型size_t可以避免出现负数,更加合理。

  • 使用专门的 size_t 类型比简单的 unsigned int 更能表明这个参数的语义 - 它表示一个大小或长度,而不是一个整数。

③为什么width要设计成size_t

在qsort函数中,width参数表示每个数组元素的大小(以字节为单位)

  1. width表示一个"大小"的概念,使用size_t类型可以更清楚地表达这个参数的语义。

  2. width的单位是字节,size_t通常是无符号整数类型,不会有负数,所以更适合表示正的值。

  3. width需要足够大的范围来表示任意数据类型的大小。size_t类型依赖平台,但通常是机器字大小,能满足大多数数据元素的大小需求。

  4. 在访问数组元素时,索引位置需要乘以元素大小才能获得地址偏移量。既然索引是size_t类型,那么两者相乘的结果也应该是size_t类型,以避免溢出问题。

④为什么要设计一个函数指针cmp?

  1. qsort 本身是一个通用的排序函数,不能知道要排序的元素类型,也就无法直接比较两个元素。采用函数指针可以将比较操作的实现留给用户。

  2. 通过函数指针,用户可以自行实现针对自己数据类型的比较函数,将具体的比较逻辑封装起来。这提高了qsort的通用性和灵活性。

  3. 对于不同的数据类型,比较操作的逻辑可能不同。使用函数指针实现可以避免在qsort中写大量的条件分支逻辑。

  4. 函数指针提供了一种扩展机制。如果用户需要改变比较操作的逻辑,只需要传入一个新的函数指针就可以,而不需要改动qsort函数本身。

⑤为什么elem1和elem2类型是const void*类型?

  1. qsort要对任意类型的数据进行排序,所以比较函数需要能处理任意类型的元素。使用void*可以指向任意类型的数据,const用于表示不应修改指针指向的内容。

  2. qsort要对任意类型的数据进行排序,所以比较函数需要能处理任意类型的元素。使用void*可以指向任意类型的数据,const用于表示不应修改指针指向的内容。

  3. 在调用qsort时可以直接将数组元素的地址强制转换为const void* 类型传给比较函数,简化调用。

  4. qsort函数本身不需要了解元素具体类型,只要把void*指针传给比较函数,由比较函数转换并解释指针内容即可。

开始模拟实现:冒泡排序大题思路不需要改变,只需要对排序方式进行即可

int cmp_int(const void* p1, const void* p2)
{
	return *(int*)p1 - *(int*)p2;
}
//假设排序为升序
//希望这个bubble_sort函数可以排序任意类型的数据
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++;
	}
}

//假设排序为升序
//希望这个bubble_sort函数可以排序任意类型的数据
void bubble_sort(void* base, size_t num, size_t width,
	int (*cmp)(const void* p1, const void* p2))
{
    //冒泡排序大题思路不需要改变,只需要对排序方式进行即可
	//要确定趟数
	size_t i = 0;
	for (i = 0; i < num - 1; i++)
	{
		int flag = 0;//假设已经有序了
		//一趟冒泡排序的过程
		size_t j = 0;
		for (j = 0; j < num - 1 - i; j++)
		{
			//两个相邻的元素比较
			//arr[j] arr[j+1]
			if (cmp((char*)base + j * width, (char*)base+(j+1)*width)>0)
			{
				//交换
				flag = 0;
				Swap((char*)base+j*width,(char*)base+(j+1)*width,width);
			}
		}
	}


}

void print_arr(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}
void test3()
{
	int arr[] = { 3,1,5,2,4,9,8,6,5,7 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	printf("排序前:");
	print_arr(arr, sz);
	bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);
	printf("排序后:");
	print_arr(arr, sz);
}

qsort对于代码的解析:

【C进阶】回调函数(指针进阶2,详解,小白必看)_第17张图片

如何交换数据:

为什么这个地方写成char*?

在bubble_sort函数中,由于不知道base指向的数据类型,因此需要将其强制转换为char*类型,从而让指针的步长为1。这样,在进行指针的加减运算时,就可以根据width参数来控制指针的步长,从而实现对任意数据类型的排序。

注意:width对于char来说是1个字节的意思,但是对于其它类型来说就不是了,width是根据参数不同来设置不同的数据类型,控制指针步长的。

交换的流程:1,2,3,4表示顺序 

【C进阶】回调函数(指针进阶2,详解,小白必看)_第18张图片

 代码的整体流程:

【C进阶】回调函数(指针进阶2,详解,小白必看)_第19张图片

排序:

【C进阶】回调函数(指针进阶2,详解,小白必看)_第20张图片

通过新定义的冒泡排序排序年龄以及姓名

struct Stu {
	char name[20];
	int age;
};
int cmp_stu_by_age(const void*p1,const void* p2)
{
	return ((struct Stu*)p1)->age - ((struct Stu*)p2)->age;

}
int cmp_stu_by_name(const void* p1, const void* p2)
{
	return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);
}
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++;
	}
}
//假设排序为升序
//希望这个bubble_sort函数可以排序任意类型的数据
void bubble_sort(void* base, size_t num, size_t width,
	int (*cmp)(const void* p1, const void* p2))
{
	//要确定趟数
	size_t i = 0;
	for (i = 0; i < num - 1; i++)
	{
		int flag = 0;//假设已经有序了
		//一趟冒泡排序的过程
		size_t j = 0;
		for (j = 0; j < num - 1 - i; j++)
		{
			//两个相邻的元素比较
			//arr[j] arr[j+1]
			if (cmp((char*)base + j * width, (char*)base+(j+1)*width)>0)
			{
				//交换
				flag = 0;
				Swap((char*)base+j*width,(char*)base+(j+1)*width,width);
			}
		}
	}


}
void print_arr3(struct Stu* s, int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", (s + i)->age);
		//printf("%s ", (*(arr+i)).name);
		//printf("%s ", arr[i].name);

	}
	printf("\n");

}
void print_arr4(struct Stu* s, int sz)
{
	int i = 0;
		for (i = 0; i < sz; i++)
		{
			printf("%s ", (s+i)->name);
			//printf("%s ", (*(arr+i)).name);
			//printf("%s ", arr[i].name);
	
		}
		printf("\n");

}
void test4()
{
	struct Stu s[] = { {"zhangsan",30} ,{"lisi",25},{"wangwu",50}};
	int sz = sizeof(s) / sizeof(s[0]);
	//测试按年龄来排序
	/*print_arr3(s, sz);
	bubble_sort(s, sz, sizeof(s[0]), cmp_stu_by_age);
	print_arr3(s, sz);*/
	//测试按照名字来排序
	print_arr4(s, sz);
	bubble_sort(s, sz, sizeof(s[0]), cmp_stu_by_name);
	print_arr4(s, sz);

}

按姓名排序:

【C进阶】回调函数(指针进阶2,详解,小白必看)_第21张图片

 按年龄排序

【C进阶】回调函数(指针进阶2,详解,小白必看)_第22张图片

这篇文章到这里就结束了,如有错误欢迎大家指正,然后下来就是这篇关于sizeof和strlen的详细总结:,指针和数组笔试题解析总结,传送门--> http://t.csdn.cn/aKVsj

欢迎大家来访。

你可能感兴趣的:(C进阶,c语言,开发语言,笔记,vscode)