【C语言进阶】带你由浅入深了解指针【第五期】:函数指针数组、函数指针数组指针

#代码星辉·七月创作之星挑战赛#

前言

本期内容是指针的最后一期,主要讲的是函数指针数组,总体不难,感谢各位支持。

目录

1.函数指针数组的使用

2. 函数指针数组指针

3.回调函数

3.1 qsort的举例

3.2 分析qsort

3.3 通用类型的冒泡排序 


1.函数指针数组的使用

        在上一期内容中,我们使用了函数指针完成了代码的简化,我们可以把函数指针作为一个数据类型构造出相应的数组,通过遍历数组,我们也能对函数进行调用。

        需要注意的是声明函数指针数组的时候只需要在变量名后面增加【】,【】里面必须标注数组的大小

// 函数指针数组
int (*arr[])(int,int) = {add,sub,mul,div};// []数字可加可不加

提出问题:当我们的功能越加越多,那么switch case语句就会越来越长,代码就会非常冗余,

        我们可以使用函数指针数组,将所有方法存起来,这里第一个元素可以设置为0,因为数组的下标要和主菜单的下标一一对应(主菜单输入1就是add方法)。

        如此以来,可以完全把switch case全部删除,如果想要增加功能,除了单独写函数以外,剩下的只需要在函数指针数组里面添加相应功能,以及控制输入的范围即可

2. 函数指针数组指针

    顾名思义,指向函数指针数组的指针,我们只需要在函数指针数组的基础上修改。

// 函数指针数组
int (*arr[5])(int,int) = {};

// 函数指针数组指针
int (*(*ptr)[5])(int,int) = &arr;

这个指针指向的是一个五个元素的数组,这个数组的类型是int (*)(int, int) ,即函数指针。如果你掌握了这些知识,其实还能再套一层,把这个指针放到指针数组里面......无限套娃。

3.回调函数

        通过一个函数指针调用的函数。如果你把函数指针作为参数传递给另外一个函数,这个指针用来调用所指向的函数时,我们就说这是回调函数。

3.1 qsort的举例

        我们之前写的冒泡排序只能排序某一类(int)的数组,不能做到通用,我们如何来解决这个问题呢?

        首先来看一下c语言的库函数qsort,能够做到排序不同种类的数组元素,这是如何做到的呢,其实真正的玄机是在第四个形参,这是一个函数指针,需要用户自己提供一个方法来诠释如何比较这类元素的大小,比如传入一个比较结构体大小的函数指针,就能比较两个结构体的大小

        为了理解这个,需要认识一下void*指针,万能指针,具体作用如下:

        对于qsort的最后一个形参而言,这是一个函数指针,这个函数的形参有两个元素,返回的值是正数说明第一个元素大于第二个元素,返回值为负数说明,第一个元素小于第二个元素,直接让两个元素相减,得到的返回值就可以完成升序,如果需要降序那么就把这两个元素的名称交换即可,但是这仅限于整型,对于其他类型来说,以下情况是一种普遍的写法:

int compare_generic(const void *a, const void *b) {
    // 假设比较整数
    int num1 = *(int*)a;
    int num2 = *(int*)b;
    
    if (num1 < num2) return -1;  // a 更小,排在前面
    if (num1 > num2) return 1;   // a 更大,排在后面
    return 0;                    // 相等
}

        所以我们需要自行定义一个比较函数用来比较指定类型的元素大小,这里需要注意的一点是,由于void*不能直接解引用,所以需要进行强制类型转换再进行解引用。 

        如果还是不过瘾,我们可以来比较一下结构体的大小,创建一个结构体,按照结构体内的字符串进行比较大小。具体逻辑基本类似,需要注意的一点是,这里字符串比较返回值刚好就是正数0或负数,所以直接返回即可。

       比较结构体的age也是一样,只需要增加一个比较方法即可。 

3.2 分析qsort

① 第一个参数:起始指针的值为什么是void*,这是因为让方法能够通用,无论是什么类型的数据都能使用这个方法。

②第二个参数:确定整体数组的元素的个数。

③第三个参数:确定单个元素的字节数。这样一来就能找到一个范围内的所有元素了。

④第四个参数:比较方法,根据不同的类型可以自行定义比较方法。

void qsort(void *base, size_t nmemb, size_t size, 
           int (*compar)(const void *, const void *));

3.3 通用类型的冒泡排序 

        根据qsort的排序方法所传入的形参,我们可以尝试模仿一下完成bubble_sort的通用方法。

原版的关键代码进行解读

这是冒泡排序的内部算法,我们需要注意以下几点:

①如何判断两个元素谁大谁小:使用比较函数。

②如何确定两个相邻元素的首地址:

        1)先将void*类型强制转换为char*,将粒度最细分。

        2)通过传入的参数width(即一个元素的字节数)、base(首元素地址)计算得出:

        3)最终得到:(char*)base +(j * width)就是j下标元素的首地址。

③知道两个元素的首地址之后,可以传入cmp对比函数以及Swap交换函数。

for (int i = 0; i < sz - 1; i++)
{
	int flag = 1; // 表示已经是有序数组
	for (int j = 0; j < sz - i - 1; j++)
	{
		// 将void*强转成char*,使其粒度最细,j的地址就是:base + (j * width)
		if (cmp((char*)base + (j * width), (char*)base + (j + 1) * width) > 0)
		{
			// 如果返回的是1,那么就说明第一个元素大于第二个元素,需要交换
			Swap((char*)base + (j * width), (char*)base + (j + 1) * width, width);
			flag = 0;
		}
	}
	if (flag)
	{
		break; // 如果一轮过后数组没有发生调换说明数组有序,直接退出循环
	}
}

④关于cmp函数,可以根据不同的数据类型来判断,上面已经赘述过一遍,这里不再演示。

// 比较函数
// void*万能指针
// 前者大于后者返回大于0的整数,0 ,小于....
int cmp_int(const void* e1, const void* e2)
{
	return (*(int*)e1 - *(int*)e2); // void*不能直接解引用,需要强转为具体类型才能解引用
}

⑤关于Swap函数如何根据两个元素的首地址交换两个首元素:这里还需要给Swap函数传入一个参数width,也就是一个元素的字节数,根据这个元素的字节数进行交换,具体的交换如下所示,传入两个char*指针,根据width循环,一个字节一个字节进行顺序交换即可。

// 仅仅知道这两个元素首地址还不够,还需要知道宽度
void Swap(char* e1, char* e2, int width)
{
	int i = 0;
	// 每个元素对应的字节进行交换
	for (int i = 0; i < width; i++) 
	{
		char tmp = *e1;
		*e1 = *e2;
		*e2 = tmp;
		e1++;
		e2++;
	}
}

完整代码

#include<stdio.h>
// 仅仅知道这两个元素首地址还不够,还需要知道宽度
void Swap(char* e1, char* e2, int width)
{
	int i = 0;
	// 每个元素对应的字节进行交换
	for (int i = 0; i < width; i++) 
	{
		char tmp = *e1;
		*e1 = *e2;
		*e2 = tmp;
		e1++;
		e2++;
	}
}
// 比较函数
// void*万能指针
// 前者大于后者返回大于0的整数,0 ,小于....
int cmp_int(const void* e1, const void* e2)
{
	return (*(int*)e1 - *(int*)e2); // void*不能直接解引用,需要强转为具体类型才能解引用
}
// 首元素的地址,数组元素个数,单个元素字节大小,比较方法
void bubble_sort(const void* base, int sz, int width, int (*cmp)(const void* e1, const void* e2))
{
	for (int i = 0; i < sz - 1; i++)
	{
		int flag = 1; // 表示已经是有序数组
		for (int j = 0; j < sz - i - 1; j++)
		{
			// 将void*强转成char*,使其粒度最细,j的地址就是:base + (j * width)
			if (cmp((char*)base + (j * width), (char*)base + (j + 1) * width) > 0)
			{
				// 如果返回的是1,那么就说明第一个元素大于第二个元素,需要交换
				Swap((char*)base + (j * width), (char*)base + (j + 1) * width, width);
				flag = 0;
			}
		}
		if (flag)
		{
			break; // 如果一轮过后数组没有发生调换说明数组有序,直接退出循环
		}
	}
}


// test1冒泡
void test1()
{

	int arr[] = { 9,7,12,1,6,5,3,8,2 };
	int sz = sizeof(arr) / sizeof(arr[0]);

	bubble_sort(arr, sz,sizeof(arr[0]),cmp_int);
	//qsort(arr, sz, sizeof(arr[0]), cmp_int);
	// 打印
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
}


int main()
{
    test1();
    return 0;
}

        关于指针的内容就全部结束了,如果这个系列对你有帮助,欢迎点赞评论收藏,你的支持是我最大的动力,后期会更新一个面试题系列,有需要的可以关注我博主~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值