函数指针与其进阶——C语言

1.函数指针

上一篇的指针博客中,我们讲了数组指针——存放数组地址的指针

那函数在内存中是否有地址呢?

我们来看一组代码:

#include <stdio.h>

void test()
{
	printf("函数指针\n");
}
int main()
{
	test();
	printf("%p\n", test);
	printf("%p\n", &test);
	return 0;
}

我们可以直观的看到,函数是有地址的。并且,无论是否有&符号,地址总是一样的

按照惯例,我们现在要将地址存储在指针变量当中。

#include <stdio.h>
void test()
{
	printf("函数指针\n");
}
int main()
{
	test();

	void (*p1)() = test;
	void (*p2)() = &test;
	
	if (p1 == p2)
		printf("相同\n");
	return 0;
}

 可以看到,我们将单独的 test 的地址存储在 p1 指针变量中;将单独的 &test 的地址存储在 p2 指针变量中。为了验证 p1 与 p2 内存储的地址相同,我们添加了一条 if 语句。

由此进一步证明 test 与 &test 是相同的,无差别的。

现在需要着重分析:为什么函数指针要这样写

 我们可以发现,函数指针与数组指针非常类似,这里就不再赘述。

我们需要注意的是,在 main 函数里面的定义的函数指针,是一个指针,它是指向具有 void 类型的返回值(无返回值),并且无参数的 test 函数。

一个问题: void * p ()  正确吗

了解了函数指针的创建,接下来我们聊聊如何使用。

阅读这组代码:

#include <stdio.h>

//定义test函数
void test()
{
	printf("函数指针\n");
}
int main()
{
	//将test函数的地址存放在p指针中
	void (*p) () = &test;

	//???
	(*p) ();
	return 0;
}

其实不难理解,注释 “???” 部分的那条语句正是函数指针的使用

我们分析一下:我们将 test 函数的地址存放在函数指针p中,p解引用 (*p)得到函数,函数需要参数,所以就写成了  (*p) () ;

我们将 (*p) 的括号去掉行吗?不行,还是结合性问题,去掉了括号,意义就变了

1.1函数指针拓展

1)阅读这串代码:(* (void (*) () ) 0 ) () ;

2)  阅读这串代码:void (* signal(int ,void (*) (int) ) ) (int);

 这样实际上我们还不是很好理解,我们运用一个关键字——typedef

#include <stdio.h>

typedef void(*pf)(int);//将 void(*)(int) 这个函数指针类型重命名为 pf
int main()
{
	void(*signal(int, void(*)(int)))(int);

	//重命名之后可以写成:
	pf signal(int, pf);

	//可以看到这是一次函数声明,void(*)(int) 作为signal函数的返回值
	return 0;
}

事实上我们重命名之后我们会发现一个矛盾:在重命名之前我们为什么要把 signal函数写在里面?直接写在外面不就好了吗?

#include <stdio.h>

//typedef void(*pf)(int);//将 void(*)(int) 这个函数指针类型重命名为 pf
int main()
{

	void(*)(int) signal(int, void(*)(int));
	//void(*signal(int, void(*)(int)))(int);


	//重命名之后可以写成:
	//pf signal(int, pf);

	//可以看到这是一次函数声明,void(*)(int) 作为signal函数的返回值
	return 0;
}

这不是更好理解吗?事实上编译器会报错。

所以说,我们可以那么理解,但是因为语法不支持,要用一种“不科学”的表示方法描述。

以上两个例子均出自——《C陷阱和缺陷》

2.函数指针数组

这个就与上一篇的博客十分类似啦,数组的定义我也不赘述了,指针数组我也不赘述了。

我们先来做一个判断题:

int (*parr1[10])();
int *parr2[10]();
int (*)() parr3[10];
哪个是定义正确的函数指针数组?
答案是第一个。parr1 会和 [ ] 先结合形成数组,数组的每个元素都是 int (*) () 类型的函数指针
我们举一个简单的例子来熟悉一下函数指针数组。
#include <stdio.h>

//加
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("************************\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+%d=%d\n", x, y, ret);
			break;
		case 2:
			printf("请输入两个操作数:>");
			scanf("%d%d", &x, &y);
			ret = Sub(x, y);
			printf("%d-%d=%d\n", x, y, ret);
			break;
		case 3:
			printf("请输入两个操作数:>");
			scanf("%d%d", &x, &y);
			ret = Mul(x, y);
			printf("%d*%d=%d\n", x, y, ret);
			break;
		case 4:
			printf("请输入两个操作数:>");
			scanf("%d%d", &x, &y);
			ret = Div(x, y);
			printf("%d/%d=%d\n", x, y, ret);
			break;
		default:
			printf("输入有误\n");
			break;
		}
	} while (input);
	return 0;
}

我们模拟实现一个简单的计算器。

但是大家可以看到,这组代码非常多,虽然逻辑简单,但是看起来就是很low,不能拿出去装逼。

我们就要在这个基础上,通过函数指针数组实现

原理是什么?我们可以看到独立的加、减、乘、除这四个函数的返回值,参数都一样,我们可以把这些函数存放在数组中,通过数组下标找到对应的函数。 

我们改进的代码:

#include <stdio.h>

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 input = 0;
	int x = 0;
	int y = 0;
	int ret = 0;
	int (*parr[5])(int , int ) = { 0,Add,Sub,Mul,Div };//转移表
	do
	{
		printf("************************\n");
		printf("***1.Add    2.Sub*******\n");
		printf("***3.Mul    4.Div*******\n");
		printf("************************\n");
		printf("请选择:>");
		scanf("%d", &input);
		if (input >= 1 && input <= 4)
		{
			printf("请输入两个操作数:>");
			scanf("%d%d", &x, &y);
			ret = (*parr[input])(x, y);
		}
		else
			printf("输入有误\n");
		printf("ret=%d\n", ret);
		
	} while (input);
	return 0;
}

大家可以对比一下前后的代码。第二组代码明显更加简洁,并且阅读起来非常也并不困难。实质操作只是把加、减、乘、除这几个独立的函数都存入函数指针数组中 parr[5] ,除了0以外(我们规定0是退出程序),剩余四个函数我们都可以通过数组下标找到, 这也是函数指针数组的高端操作——转移表。

运行起来的效果除了输出没有运算符号了,其他都一摸一样。 

 2.2指向函数指针数组的指针

这个概念非常绕口, 但事实上并不复杂。我们看一组代码。

#include <stdio.h>
void test()
{
	printf("指向函数指针数组的指针\n");
}
int main()
{
	//把test函数地址存放到函数指针变量当中
	void (*pf)() = &test;

	//创建一个大小为1的函数指针数组,并把test函数放进去
	void (*pfarr[1])() = { test };

	//把函数指针数组的地址存放到指向函数指针数组的指针变量当中
	void (*(*ppfarr)[1])() = &pfarr;
	return 0;
}

实际上,这种方式是很少使用的,我们只需要知道有这个东西就好了。

3.回调函数

我们解释一下什么是回调函数:

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

qsort 函数的作用是对数组元素进行排序,不管是什么类型的数据。可以用通过 qsort 函数的参数使用了多个 void 确定。

我们来分析一下各个参数的意义。

 

那么参数里面的函数指针指向谁呢?

cplusplus给出解释:

我们进一步补充:

当 p1 p2 两个指针指向的元素相减,大于0返回1;等于0返回0;小于0返回-1; 

p1 p2 的类型都是 void* 类型的,在比较前需要强制类型转换

我们举几个 qsort 函数对不同数据进行排序的例子:

1)对整型数据排序

#include <stdio.h>

int cmp(const void* p1, const void* p2)
{
	return *(int*)p2 - *(int*)p1;//后一个元素大于第一个元素,返回1,qsort内部进行交换
}
int main()
{
	int arr[10] = { 1,2,6,5,4,7,3,8,9,0 };
	qsort(arr, 10, sizeof(int), cmp);
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

2)对字符型数据排序

#include <stdio.h>

int cmp(const void* p1, const void* p2)
{
	return *(char*)p2 - *(char*)p1;//后一个元素大于第一个元素,返回1,qsort内部进行交换
}
int main()
{
	char arr[10] = "abcdefghij";
	qsort(arr, 10, sizeof(char), cmp);
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%c ", arr[i]);
	}
	return 0;
}

 

3)对结构体数据排序

#include <stdio.h>

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

int cmp(const void* p1, const void* p2)
{
	return ((struct Stu*)p2)->age - ((struct Stu*)p1)->age;
}
int main()
{
	struct Stu arr[3] = { {"zhangsan",20},{"lisi",17},{"wangwu",33} };
	qsort(arr, 3, sizeof(struct Stu), cmp);//按年龄排序
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%d ", arr[i].age);
	}
	return 0;
}

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


int cmp(const void* p1, const void* p2)
{
	return strcmp(((struct Stu*)p2)->name , ((struct Stu*)p1)->name);
}
int main()
{
	struct Stu arr[3] = { {"zhangsan",20},{"lisi",17},{"wangwu",33} };
	qsort(arr, 3, sizeof(struct Stu), cmp);//按名字比较
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%s,%d ", arr[i].name, arr[i].age);
	}
	return 0;
}

 

那我们为了进一步理解回调函数,我们自己来模拟一个qosrt对整型排序的例子。用冒泡排序的思维。

#include <stdio.h>

int cmp(const void* p1, const void* p2)
{
	return *(int*)p2 - *(int*)p1;
}


void Swap(char* p1, char* p2,int width)
{
	//因为传进来的char*类型,对应的元素只有一个字节
	//我们需要知道外部元素的真正大小,所以需要把width也传进来
	//实现元素之间一个字节一个字节的交换
	int i = 0;
	for (i = 0; i < width; i++)
	{
		char tmp = *p1;
		*p1 = *p2;
		*p2 = tmp;
		p1++;
		p2++;
	}
}
void my_qsort(void* base, int num, int width, int (*cmp)(const void*, const void*))
{
	int i = 0;
	int j = 0;
	for (i = 0; i < num - 1; i++)
	{
		for (j = 0; j < num - 1 - i; j++)
		{
			//先将元素强转char*类型(指向的对象只有1字节)再乘以元素大小
			//当main函数的数组不是int类型的时候,我们不需要进到qsort来进行修改
			//增强了函数的可移植性
			if (  (cmp ( (char*)base + j * width  , (char*)base + (j + 1) * width ) ) >0)
			{
				//定义一个交换函数实现元素的交换
				Swap((char*)base + j * width, (char*)base + (j + 1) * width,width);
			}
		}
	}
}
int main()
{
	//创建需要操作的数组
	int arr[10] = { 0,9,8,6,5,4,2,1,3,7 };

	//模拟实现qsort,用冒泡排序的思维
	my_qsort(arr, 10, sizeof(int), cmp);

	//打印
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

 4.结束语

以上就是对函数指针及其进阶的一小部分的使用与分析。

个人水平有限可能会解释不清、定义模糊、语言逻辑牛头不对马嘴的现象。

以上代码的思想不是很深,我本人对此指针的理解也不是很深,基本上是面向初学者的。

希望进阶学者不要贬低。

有问题欢迎大家积极评论区留言!

猜你喜欢

转载自blog.csdn.net/weixin_59913110/article/details/125120596