【C语言进阶学习】二、指针的进阶(2)

本篇是继续上篇内容进行学习【C语言进阶学习】二、指针的进阶(1)


函数指针

我们在前篇讲数组指针的时候,是这样引出数组指针的概念的:

整型指针: 存放整型的地址 - - - 是指向整型的指针
字符指针: 存放字符的地址 - - - 是指向字符的指针
所以 - - >
数组指针:存放数组的地址 - - - 是指向数组的指针

那么,我们就此可以引出函数指针的概念了:
函数指针:存放函数的地址 - - - 是指向函数的指针

我们可能有些疑问,函数也有地址的吗?是的,函数是有地址的
我们创建函数之后,调用它的时候,就会在内存中开辟一块空间,既然开辟了内存空间,那就有对应的内存空间地址。

接下来我们来了解一下以下内容:
&数组名 - - - 取出的是数组的地址
数组名 - - - 数组首元素地址
这我们在前篇已经讲过,那么 &函数名 与 函数名 所代表的意义是否也不一样呢?
请看下面代码:

int Add(int x, int y)
{
	return x + y;
}
int main()
{
	printf("%p\n", &Add);
	printf("%p\n", Add);
	return 0;
}

在这里插入图片描述

总结:&函数名与函数名都是指函数的地址
数组名 != &数组名
函数名 == &函数名


既然函数有地址,那函数的地址该如何存放呢?
如图:在这里插入图片描述
在这里插入图片描述
了解函数指针是如何定义之后,那函数指针可以如何运用呢?

假如我们要实现一个加法运算,在我们没学过函数指针之前是这样调用函数的,如图:
在这里插入图片描述
当我们使用函数指针,我们就可以调用函数了:
.在这里插入图片描述
而我们一开始就讲了,函数名与&函数名都是表示函数的地址,所以我们不可并不用&,而当我们将数组名传给pf后,pf就是存放Add函数的地址,所以也可以不用 * 操作符, 其实 * 号在这里就是一个摆设,无论放几个 * 都无所谓,是不会报错的,这个地方放 * 是为了方便理解,如图:

在这里插入图片描述


了解了这些后,我们来看下面代码:

//代码1
(*(void (*) ())0)();

//代码2
void(*signal(int, void(*)(int)))(int);

这两个代码分别表示什么意思呢?
首先我们看代码1:

	( * ( void(*)() )0 )();
	代码分析:
	这个代码是一次函数调用
	1.首先代码中把0强制类型转换为类型为void(*)()的一个函数的地址。
	2.然后解引用0的地址,就是取0地址处的这个函数,被调用的函数是无参的,返回类型是void

代码2:

void(* signal(int, void(*)(int) ) )(int);
代码分析:
这个代码是一次函数声明
声明的函数名是signal
signal函数有2个参数,第一个参数是int类型,第二个是void(*)(int)的函数指针类型
signal函数的返回类型依然是:void(*)(int)的函数指针类型

或许代码2我们看起来有点复杂,那我们是否可以将其简化一下呢?如下:

typedef void(*pfun_t)(int);
//typedef类型重定义关键字,将void(*)(int)类型的函数指针,重新命名为pfun_t
//所以代码2可以写为:
pfun_t signal(int,pfun_t);

注:void ( * )(int) signal(int, void(*)(int));
这种写法虽然很容易理解,但语法是错误的,所以一定不能这样写!!


函数指针数组

什么叫函数指针数组呢?
我们来类比一下:

整型指针数组:存放整型指针的数组

int* arr[10];
//整型指针数组---存放整型指针的数组
//该数组有10个元素,每个元素都是int*类型

那我们是不是可以将函数指针数组这样定义,如下:

函数指针数组:存放函数指针的数组
那函数指针数组该如何定义呢?如下

#include <stdio.h>
//加法
int Add(int x, int y)//函数指针类型:int (*)(int, int)
{
	return x + y;
}
//减法
int Sub(int x, int y)//函数指针类型:int (*)(int, int)
{
	return x - y;
}
//乘法
int Mul(int x, int y)//函数指针类型:int (*)(int, int)
{
	return x * y;
}
//除法
int Div(int x, int y)//函数指针类型:int (*)(int, int)
{
	return x / y;
}

int main()
{	
	//pfArr就是一个函数指针的数组
	int (* pfArr[4])(int, int) = {Add, Sub, Mul, Div};
	//去掉pfArr[4]剩下的int(*)(int,int)就是函数指针数组的元素的类型
	return 0;
}

在这里插入图片描述

注:数组是一个存放相同类型数据的存储空间,所以我们要注意函数指针数组里的元素类型是否一致(返回类型,参数)

了解到这,那我们该如何去使用函数指针数组呢?请看下面例子:
假如我要实现一个计算器:

最简单的写法:

#include <stdio.h>
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)//函数指针类型:int (*)(int, int)
{
	return x + y;
}
//减法
int Sub(int x, int y)//函数指针类型:int (*)(int, int)
{
	return x - y;
}
//乘法
int Mul(int x, int y)//函数指针类型:int (*)(int, int)
{
	return x * y;
}
//除法
int Div(int x, int y)//函数指针类型:int (*)(int, int)
{
	return x / y;
}

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("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;
}

运行结果:
在这里插入图片描述

我们可以看到这种写法,switch语句里的代码有点冗余,有很多重复的代码,所以有没有更好的写法呢?如下:

使用函数指针简化:

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)//int (*)(int, int)
{
	return x + y;
}

int Sub(int x, int y)//int (*)(int, int)
{
	return x - y;
}

int Mul(int x, int y)//int (*)(int, int)
{
	return x * y;
}

int Div(int x, int y)//int (*)(int, int)
{
	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函数,在Calc函数里再通过函数指针去调用这些函数,而这种通过函数指针去调用的函数,就叫回调函数,在本片最后一个内容我们会详细讲解。

注:回调函数的这种机制,必须用函数指针来实现,否则做不到

使用函数指针数组的实现:

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)//函数指针类型:int (*)(int, int)
{
	return x + y;
}
//减法
int Sub(int x, int y)//函数指针类型:int (*)(int, int)
{
	return x - y;
}
//乘法
int Mul(int x, int y)//函数指针类型:int (*)(int, int)
{
	return x * y;
}
//除法
int Div(int x, int y)//函数指针类型:int (*)(int, int)
{
	return x / y;
}

int main()
{
	int input = 0;
	int x = 0;
	int y = 0;
	int ret = 0;//接收结果
	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		//pfArr就是函数指针数组
		int(*pfArr[5])(int, int) = { 0, Add, Sub, Mul, Div };
							  //下标  0   1    2    3    4
		if (input == 0)
		{
			printf("退出计算器\n");
		}
		else if (input >= 1 && input <= 4)
		{
			printf("请输入两个操作数:");
			scanf("%d %d", &x, &y);
			ret = pfArr[input](x, y);
			printf("ret = %d\n", ret);
		}
		else
		{
			printf("选择错误\n");
		}
	} while (input);
	return 0;
}

运行结果:
在这里插入图片描述
这种写法就可以防止主函数中的代码,会随着其他算法的增加,而导致代码增多的现象,它增加的只有函数指针数组的元素个数,以及else if里的判断条件的范围。


指向函数指针数组的指针

指向函数指针数组的指针,意思是一个指针,指针指向一个数组,数组的元素是函数指针类型。
那函数指针数组的指针怎么定义呢?

#include <stdio.h>
int Add(int x, int y)
{
	return x + y;
}
int main()
{
	int arr1[10];//整型数组
	int(*p1)[10] = &arr1;
	//p1是指向一个整型数组的指针

	int* arr2[10];//整型指针数组
	int*(*p2)[10] = &arr2;
	//p2是一个指向整型指针数组的指针

	int(* pf)(int, int) = Add;
	//pf是一个函数指针
	int(* pfArr[5])(int, int);
	//pfArr是一个函数指针数组,该数组有五个元素,每个元素都是int(*)(int,int)类型的函数指针
	
	int(* (*ppfArr)[5] )(int,int) = &pfArr;
	//ppfArr就是一个函数指针数组的指针
	//它指向一个函数指针数组,该数组有五个元素,每个元素都是函数指针,类型为int(*)(int,int)
	return 0;
}

在这里插入图片描述


回调函数

概念:

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应

总结:回调函数就是一个通过函数指针来调用的函数。

举例:比如上面计算器的实现,使用函数指针简化的方法,其中就是运用回调函数的方法


了解回调函数的概念之后,我们来进行对其熟悉练习。

我们在前面的学习中,已经学习过冒泡法排序一个整型数组,这里我们进行一下复习。
在这里插入图片描述
我们可以发现:

每一趟只解决一个数字的排序问题,所以需将10个数字全部排序完成,需要进行9躺冒泡排序,
因为第9趟排序完成后,最后剩下的一个数字,已经会在它应该在的位置上。
所以n个数字,只需n-1趟。

而每一趟排序的内部:

第一趟:10个数字待排序,9对比较
第二趟:9个数字待排序,8对比较
第三趟:8个数字待排序,7对比较
……
第九趟:2个数字待排序,1对比较

代码实现:

#include <stdio.h>
void print(int* arr, int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(arr + i));
	}
	printf("\n");
}
void bubble_sort(int* arr, int sz)
{
	int i = 0;
	for (i = 0; i < sz - 1; i++)//确定冒泡排序的趟数
	{
		int j = 0;
		int flag = 1;//假设本趟已经有序了

		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;
				flag = 0;//本趟无序
			}
		}
		if (flag == 1)
		{
			break;
		}
	}
}
int main()
{
	int arr[10] = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
	int sz = sizeof(arr) / sizeof(arr[0]);

	printf("排序前:\n");
	print(arr, sz);

	//通过冒泡法,进行升序排列
	bubble_sort(arr, sz);

	printf("排序后:\n");
	print(arr, sz);
	return 0;
}

运行结果:
在这里插入图片描述
这里我们只学了使用冒泡法排序一个整型的数组内容,但它不能实现浮点型、字符型、结构体型等等除整型之外的类型的排序。

那如果出现整型数据之外的数据类型需要进行排列,那我们需要如何做呢?请看下面解答:


在C语言的库函数中有一个用于任意类型排序的函数 qsort
qsort - - - 使用快速排序的方法
qsort库函数的相关信息:
在这里插入图片描述
下面对qsort函数详细解读:

void qsort(
	void* base,//待排序目标的起始位置
	size_t num,//待排序的元素个数
	size_t width,//一个元素的大小,单位是字节
	int(* cmp)(const void* e1, const void* e2)
	//函数指针,排序所使用的比较函数
	//不同类型的数据比较的方法是不一样的,所以需要将比较方法写成函数传递给qsort
   //函数指针,指针指向的参数有两个,参数类型都是const void*,函数返回类型是int
)

那这里的函数指针所指向的函数返回的值是什么呢?如图:
在这里插入图片描述
当参数e1小于e2时,返回一个小于0的数字
当参数e1等于e2时,返回0
当参数e1大于e2时,返回一个大于0的数字

在这个函数中,我们还发现了一个我们不常见的类型:void*
注释: void是无,空的含义,void * 表示的是无类型的指针。 void* 类型的指针可以接收任意类型的地址(void* 也可以被称为万能指针)。

void*类型指针使用注意类项:

  1. void* 类型指针不能进行解引用操作的。
    (原因:指针的类型决定了指针解引用操作时访问字节的个数,如果指针类型是void *,无类型指针,那么就无法得知该指针解引用访问多少个字节数。)
  2. void* 类型指针不能进行++ / ±整数运算的操作。
    (原因:指针类型的意义决定了指针 + -整数运算的步长,如果指针类型是void *无类型指针,那么就无法得知该指针 + -的步长到底是多少个字节。)

在这里插入图片描述
(1)下面我们来试着用qsort排序一下整型数组,熟悉qsort的使用:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//比较2个整型的函数
//为了调用qsort,需要将比较函数传给qsort
//按照qsort函数参数的类型,写出需要使用的比较函数
int cmp_int(const void* e1, const void*e2)
{
	return *(int*)e1 - *(int*)e2;//升序
	//return *(int*)e2 - *(int*)e1;//降序
	
}

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

void test1()
{
	int arr[] = { 10, 8, 9, 7, 5, 3, 4, 2, 1, 6 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, sz, sizeof(arr[0]), cmp_int);
	//打印
	print_arr(arr, sz);
}
int main()
{
	//排序整型数组
	test1();
	return 0;
}

在这里插入图片描述
(2)利用qsort排序结构体数据

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct stu
{
	char name[20];
	int age;
};

int cmp_by_name(const void*e1,const void*e2)
{
	//字符串比较需要用到字符串比较库函数strcmp
	return strcmp(((struct stu*)e1)->name, ((struct stu*)e2)->name);
}

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

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

void test2()
{
	struct stu s[3] = { { "zhangsan", 15 }, { "lisi", 10 }, { "wangwu", 30 } };
	int sz = sizeof(s) / sizeof(s[0]);
	int i = 0;

	qsort(s, sz, sizeof(s[0]), cmp_by_name);//按名字来排

	//打印
	printf("按名字排序:\n");
	print_s(s, sz);

	qsort(s, sz, sizeof(s[0]), cmp_by_age);//按名字来排

	//打印
	printf("按年龄排序:\n");
	print_s(s, sz);
}

int main()
{
	test2();//排序结构体
	return 0;
}

在这里插入图片描述
(3)利用qsort排序浮点型数据:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int cmp_float(const void* e1, const void* e2)
{
	//强制类型转换成int类型---float类型转换为int类型会造成精确度缺失
	return (int)(*(float*)e1 - *(float*)e2);
}

void print_float(float arr2[], int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%.1f ",arr2[i]);
	}
	printf("\n");
}
void test3()
{
	float arr2[] = { 4.0, 5.0, 1.0, 2.0, 3.0, 9.0 };
	int sz = sizeof(arr2) / sizeof(arr2[0]);
	int i = 0;

	qsort(arr2, sz, sizeof(arr2[0]), cmp_float);
	
	//打印
	print_float(arr2, sz);
}

int main()
{
	test3();//排序浮点型数组
	return 0;
}

在这里插入图片描述


经过这三种示例,我们也熟悉了qsort函数的使用了。
那这个库函数究竟是如何实现的呢?接下来我们就模拟实现qsort排序任意类型的排序函数- - - – - BubbleSort

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct stu
{
	char name[20];
	int age;
};

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

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

int cmp_by_age(const void*e1, const void*e2)
{
	return ((struct stu*)e1)->age - ((struct stu*)e2)->age;
}
//交换函数
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 BubbleSort(void* base, size_t num, size_t width, int (*cmp)(const void*e1, const void*e2))
{
	size_t i = 0;
	//趟数
	for (i = 0; i < num - 1; i++)
	{
		//比较的对数
		size_t j = 0;
		for (j = 0; j < num - 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);//只有一个一个字节才可以完成交换
			}
		}
	}
}
//测试自定义的BubbleSort()排序整型数组
void test3()
{
	int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);

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

	//打印
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}
//测试自定义的BubbleSort() 排序结构体
void test4()
{
	struct stu s[3] = { {"zhangsan", 15}, {"lisi", 30},{"wangwu", 10} };
	int sz = sizeof(s) / sizeof(s[0]);
	//按照名字排序
	BubbleSort(s, sz, sizeof(s[0]), cmp_by_name);

	//打印
	int i = 0;
	printf("\n按名字排序:\n");
	for (i = 0; i < sz; i++)
	{
		printf("%s,%d\n", s[i].name, s[i].age);
	}
	printf("\n");

	//按照年龄来排序
	BubbleSort(s, sz, sizeof(s[0]), cmp_by_age);

	//打印
	printf("按年龄排序:\n");
	for (i = 0; i < sz; i++)
	{
		printf("%s,%d\n", s[i].name, s[i].age);
	}
	printf("\n");

}
int main()
{
	//测试自定义的BubbleSort();
	test3();//测试排序整型数组

	test4();//排序结构体数据

	return 0;
}

在这里插入图片描述
从两个测例中我们可以发现,我们是将一个函数名作为参数传给一个函数指针,在由函数指针去调用

  • 1
    点赞
  • 0
    收藏 更改收藏夹
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Clumsy、笨拙

你的鼓励是我最大的动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值