C进阶_C语言_函数与指针_C语言指针进阶

本文详细介绍了C语言中的函数指针概念,包括函数指针的定义、存储和调用,以及函数指针数组的使用。还探讨了指向函数指针数组的指针,并通过示例展示了回调函数的概念和应用。此外,文章通过冒泡排序模拟实现了qsort函数,强调了qsort的通用性,并给出了自定义比较函数的例子。
摘要由CSDN通过智能技术生成

上一篇博客http://t.csdn.cn/GYCiM

我们了解了指针相关知识,今天来了解函数和指针的关系。

目录

函数指针

函数指针数组

指向函数指针数组的指针

回调函数

qsort

冒泡排序模拟实现qsort


函数指针

我们知道,数组指针是指向数组的指针。

int arr[10]={0};
int (*p)[10]=&arr;

那么函数指针就是指向函数的指针。

假设有函数Add……

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

那么现在取它的地址,看看能不能打印出一个地址:

#include <stdio.h>
int main()
{
	printf("%p\n",&Add);
	return 0;
}

运行程序,果然是地址:

既然是指针就可以存起来,那么函数指针该怎么存呢?

首先应该明确一个指针。所以在定义指针变量时应用括号把变量名括起来,再去明确参数。

如果想得到函数调用后的结果,就要用一个变量来接收调用后的数据:

int (*pf)(int, int) = &Add;
int ret = (*pf)(2, 3);

要注意&Add和Add是一个效果,都是函数的地址。

用以下方式也可以实现,并且在调用函数时可以直接用pf。

int (*pf)(int, int) = Add;
int ret = pf(2, 3);

调用函数时pf前面的*可以说就是摆设,有无都可以。

因为平时在调用函数时,直接以Add()的方式调用即可,而Add又是地址,而函数指针里存放的又是地址,所以我们在用函数指针调用函数时可以不去加*。

但是注意,要写*的话,就一定要把*和pf一起用括号括起来。

阅读两段有趣的代码:

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

怎么读懂代码1?

我们从0下手。0前面的括号里边括起来的是void (*)()这是一个函数指针类型。

既然void (*)()是被放在括号里的,说明有进行类型强制转换,转换的是谁呢?是0。

0本来是int型的,现在把它强制转换成函数指针类型。

0被当成了一个函数的地址。

void (*)()前边加了*,说明在进行解引用操作。

事实上解引用操作可有可无。

要注意这个函数没有参数所以最后边是()。没有传参。

总而言之就是把0以函数的形式调用了。该代码是一次函数调用。

再次解释一遍:

该代码是一次函数调用,

调用0地址处的一个函数,

首先代码中将0强制类型转换为类型为void (*)()的函数指针,

然后去调用0地址处的函数。

那代码2如何解读呢?

从signal下手。很显然signal是一个函数,它的参数分别为int变量和一个参数为int的void型函数指针。

signal在调用后很可能会返回一个参数为int的void型函数指针,然后再调用此函数。

再次解读:

该代码是一次函数的声明,

声明的函数名字叫signal,

signal函数的参数有2个,第一个是int类型,第二个是函数指针类型,该函数指针能够指向的那个函数的参数是int,

返回类型是void,

signal函数的返回类型是一个函数指针,该函数指针能够指向的那个函数的参数是int,返回类型是void。

因为signal内外都有void(*)(int),所以这个代码可以被简化:

typedef void(*pfun_t)(int);
pfun_t signal(int, pfun_t);

函数指针数组

我们已经学了以下知识:

int my_strlen(const char* str)
{
    return 0;
}
int main()
{
	//指针数组
	char* ch[5];
	int arr[10] = {0};
	//pa是数组指针
	int (*pa)[10] = &arr;
	//pf是函数指针
	int (*pf)(const char*) = &my_strlen;
	return 0;
}

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

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

答案是:parr1。
parr1先和[]结合,说明parr是数组,数组的内容是什么呢?
是int (*)()类型的函数指针。

//函数指针数组
int (*pfA[5])(const char*) = { &my_strlen};

下面来实现一个计算器:

#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)
{
	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;
	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;
}

如果想把功能拓展下,增加更多的功能,那么该怎么写代码?

那么我们就要再写多个函数,然后再switch中不断增加case break。

我们发现这些函数的参数、类型一模一样:

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 (*pf[5])(int, int) = { NULL, Add, Sub, Mul, Div };

然后这样改进(函数已省略):

int (*pf[5])(int, int) = { NULL, Add, Sub, Mul, Div };
int main()
{
	int input = 0;
	int x = 0;
	int y = 0;
	int ret = 0;
	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		if (input == 0)
		{
			printf("退出计算器\n");
			break;
		}
		else if (input>=1 &&input<=4)
		{
			printf("请输入两个操作数:>");
			scanf("%d %d", &x, &y);
			ret = pf[input](x, y);
			printf("%d\n", ret);
		}
		else
		{
			printf("选择错误\n");
		}
	} while (input);

	return 0;
}

如果想添加功能,只需要定义函数,然后在int (*pf[5])(int, int) = { NULL, Add, Sub, Mul, Div }中添加函数名、修改成员数即可。

指向函数指针数组的指针

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

int main()
{
	int arr[10];
	int (*pA)[10] = &arr;
	//函数指针数组
	int (* pf[5])(int, int);
	//ppf是指向函数指针数组的指针
	int (*(* ppf)[5])(int, int) = &pf;
	return 0;
}

在int (*(* ppf)[5])(int, int) = &pf里,因为ppf和[5]不结合,那么(*ppf)[5]就是数组。

前边加上了*后,*和(*ppf)[5]结合,那么ppf就是指针,是数组pf的指针。

它们是这样存放的:

p中存放的是函数指针,pf中存放的是多个函数的指针,ppf指向pf。

回调函数

函数指针的最重要的一个应用就是回调函数。

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

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

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

我们回顾下刚刚写的计算器的部分代码:

	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);

我们可以发现每个case后、break之前的代码逻辑是非常相似的。

可以封装一个函数,然后多次调用,并且在调用的时候把相应函数的地址传递过去:

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("请输入两个操作数:>");
	scanf("%d %d", &x, &y);
	ret = pf(x, y);
	printf("%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;
}

qsort

排序算法有很多,今天要用到冒泡排序和C语言库函数自带的qsort函数。

有关冒泡排序的知识,欢迎参考我之前的博客http://t.csdn.cn/zhKV6

在MSDN中查找qsort

void qsort(
void *base, 
size_t num, 
size_t width, 
int (__cdecl *compare )(const void *elem1, const void *elem2 )
);

 这个函数有四个参数,分别是void型指针、两个int型(size_t就是int)变量和一个比较函数指针。

Parameters

base

Start of target array

num

Array size in elements

width

Element size in bytes

compare

Comparison function

elem1

Pointer to the key for the search

elem2

Pointer to the array element to be compared with the key

从MSDN文档可得知base是目标数组的起始位置,num是数组元素个数,width是数组内每个元素的大小(字节),然后是比较函数指针,两个形参分别是要比较的数。

qsort可以对任意类型的数组进行排序。

我们用冒泡排序来模拟实现下qsort,先看冒泡排序的函数:

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

这样的代码只能对整数进行排序!因为第二层for内,if后用的是>,只能比较两个整数的大小。

而两个字符串之间的比较要用strcmp,所以此代码只能对整数进行排序。

那么针对不同的数据类型就要用到不同的函数了,qsort中最后一个参数的作用就显现出来了。

qsort的参数中:

int (__cdecl *compare )(const void *elem1, const void *elem2 )

elem1和elem2分别是要比较的两个元素的地址。

elem1和elem2两个指针是void型的指针。为什么是void?

这是因为qsort作者在设计此函数时,并不知道未来的开发者会将什么样的数据类型进行排序。

void*可以用于接收任何类型的指针,但是不能直接解引用,在使用时要进行类型转换。

void型指针也不能直接进行运算,比如,变量名++是错误的。

下边来实现一个比较整型的数组:

int cmp_int(const void* e1, const void* e2)
{
}
void test1()
{
	
	int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, sz, sizeof(arr[0]), cmp_int);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
}

因为要排序的数据是int型,所以在cmp_int内部就要将e1和e2强制类型转换:

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

在qsort的MSDN文档中,我们看到:

Return ValueDescription
< 0elem1 less than elem2
0elem1 equivalent to elem2
> 0elem1 greater than elem2

当elem1小于elem2时返回值小于0,当elem1和elem2相等时返回值等于0,当elem1大于elem2时返回值大于0。

所以就要用第一个元素的值减去第二个元素的值,和qsort保持一致。

当返回值传递到qsort中后,qsort会根据返回值是大于0还是等于0还是小于0,再结合前三个参数进行排序。

qsort默认正序。也就是说e1-e2升序,e2-e1降序。

要注意使用qsort要包含stdlib头文件。

下面来实现结构体排序:

struct Stu
{
	char name[20];
	int age;
};
int cmp_stu_by_age(const void* e1, const void* e2)
{
	return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
void test2()
{
	struct Stu s[3] = { {"zhangsan",20}, {"lisi", 50}, {"wangwu", 33} };
	int sz = sizeof(s) / sizeof(s[0]);
	qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);
}
int main()
{
    test2();
    return 0;
}

这是对学生按照年龄来排序。

也可以按照名字来排序:

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

要注意,这里用strcmp,并且要引用头文件string.h。

查阅strcmp的文档可知,strcmp的返回值和qsort返回值非常类似:

ValueRelationship of string1 to string2
< 0string1 less than string2
0string1 identical to string2
> 0string1 greater than string2

都是第一个元素大于第二个元素返回值大于0,第一个元素和第二个元素相等返回值为0,第一个返回值小于第二个元素返回值小于0。

所以可以在cmp_stu_by_name中直接返回strcmp的返回值。

对s进行监视,可以看到成功实现了排序。

冒泡排序模拟实现qsort

下面用回调函数,冒泡排序来模拟qsort。

改造冒泡排序,使冒泡排序能对任何类型的数组排序:

思想还是冒泡排序,但与以往不同的是,它可以对任意类型的数组排序。

写一个bubble_sort的函数,因为不知道将来会对什么类型的数据进行排序,那函数的待排序数组的形参就要是void类型,函数指针cmp的参数要用void*类型:

void bubble_sort(void*base, size_t sz, size_t width, int (*cmp)(const void*e1, const void*e2))
{
}

假设我们要排序的是整型数组……

函数传递的是指针,那么相邻两个元素交换时,由于是整型数组,数组的每个元素是int型,那么第一个元素的地址就要+4,第二个元素的地址就要-4

这里假设第一个元素地址是base,元素交换时加四个字节。

请思考下,哪个数据类型是1个字节?

字符型!char!一个char类型的长度1*width就是一个int型的宽度!

所以在设计比较两个元素大小的函数时,强制转换成char*类型

先看bubble_sort的参数,width是一个元素的宽度,那么可以这样写比较两个元素大小的函数的参数:

cmp((char*)base+j*width, (char*)base+(j+1)* width);

应注意cmp只是bubble_sort函数的参数,具体进行比较的函数在先前已演示,这里不再演示。

将它的返回值作为if的条件:

if (cmp((char*)base+j*width, (char*)base+(j+1)* width)>0)
{
}

再封装一个交换函数:

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, size_t sz, size_t width, int (*cmp)(const void*e1, const void*e2))
{
	//趟数
	size_t i = 0;
	for (i = 0; i < sz - 1; i++)
	{
		//一趟冒泡排序的过程
		size_t 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);
			}
		}
	}
}

 使用我们自己写的bubble_sort函数排序整型数组,运行后发现得到了预期的结果:

void test3()
{
	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);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
}
void test3()
{
	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);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
}
int main()
{
    test3();
	return 0;
}

我们也可以用自己实现的bubble_sort对结构体进行排序,运行后同样得到了预期的结果:

void bubble_sort(void*base, size_t sz, size_t width, int (*cmp)(const void*e1, const void*e2))
{
	//趟数
	size_t i = 0;
	for (i = 0; i < sz - 1; i++)
	{
		//一趟冒泡排序的过程
		size_t 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 test4()
{
	struct Stu s[3] = { {"zhangsan",20}, {"lisi", 50}, {"wangwu", 33} };
	int sz = sizeof(s) / sizeof(s[0]);
	bubble_sort(s, sz, sizeof(s[0]), cmp_stu_by_name);
}
int main()
{
	test4();
	return 0;
}

创作不易,码了九千多字,求各位三连支持下!

评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值