从0开始学c语言-28-qsort函数、 数组和指针参数、函数指针数组(转移表)、回调函数

上一篇:

从0开始学c语言-27-字符指针,指针数组和数组指针_阿秋的阿秋不是阿秋的博客-CSDN博客

如若没有上边这篇链接内容的基础,可能会看不懂。

目录

数组参数、指针参数

一维数组传参

二维数组传参

一级指针传参

二级指针传参

函数指针

有趣的代码

函数指针数组

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

指向函数指针数组的指针

回调函数

qsort函数的使用

模仿qsort函数进行冒泡排序

结构体调用qsort函数


数组参数、指针参数

在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?

接下来这些代码展示了什么样的可以,什么样的不可以。

一维数组传参

void test(int arr[])//ok
{}
void test(int arr[10])//ok
{}
void test(int* arr)//ok
{}
void test2(int* arr[20])//ok
{}
void test2(int** arr)//ok
{}
int main()
{
	int arr[10] = { 0 };
	int* arr2[20] = { 0 };
	test(arr);
	test2(arr2);
}

二维数组传参

void test(int arr[3][5])//ok
{}
void test(int arr[][])//err
{}
void test(int arr[][5])//ok
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
void test(int* arr)//err
{}
void test(int* arr[5])//err
{}
void test(int(*arr)[5])//ok
{}
void test(int** arr)//err
{}
int main()
{
	int arr[3][5] = { 0 };
	test(arr);
}

一级指针传参

void print(int *p, int sz) {
 int i = 0;
 for(i=0; i<sz; i++)
 {
 printf("%d\n", *(p+i));
 }
}
int main()
{
 int arr[10] = {1,2,3,4,5,6,7,8,9};
 int *p = arr;
 int sz = sizeof(arr)/sizeof(arr[0]);
 //一级指针p,传给函数
 print(p, sz);
 return 0; }
当一个函数的参数部分为一级指针
的时候,函数能接收什么参数?

答案:同类型的一级指针,比如char*可以接收char[ ]数组的数组名。

(不知道答案为什么是这样的,翻到文章顶部,有链接解释)

二级指针传参

void test(int** ptr) {
 printf("num = %d\n", **ptr); 
}
int main()
{
 int n = 10;
 int*p = &n;
 int **pp = &p;
//以下这两种传参方式都可以
 test(pp);
 test(&p);
 return 0; }

思考以下代码的传参对不对?

void test(char **p) {
 
}
int main()
{
 char* arr[10];
 test(arr);//Ok?
 return 0; }

答案:OK。

arr的类型是char*[10],指向10个char*,本质上是char**指针。

函数指针

void test()
{
	printf("hehe\n");
}
int main()
{
	printf("%p\n", test);
	printf("%p\n", &test);
	return 0;
}

 他们的输出结果(地址)是一样的,和数组名不一样的是,

函数名 == &函数名,而 数组名!= &数组名 。

我们监视看一下类型。

根据我们监视的结果,请看看接下来这段代码,哪个会有能力存放函数的地址呢?

void (*pfun1)();
void *pfun2();

是第一个哦,它的类型是void(*)(),是一个函数指针

意思是说这个函数指针指向的函数,函数返回类型是void,参数没有。

而第二个,   它的类型是void*(),是一个返回类型是void*的函数。

        这段代码也告诉我们,如果不加()把*和指针变量括起来,那么就会先和()结合,解读成一个函数。

有趣的代码

(*(void (*)())0)();

猜猜这段代码的意思是?

如图:

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

看看这个代码的意思是?

所以 

void (*signal(int , void(*)(int)))(int);
也可以写成
void(*)(int) signal(int,void(*)(int));

简化一下代码,

typedef void(*pfun_t)(int);
//把void(*)(int)函数指针重定义为pfun_t
那么
void(*)(int) signal(int,void(*)(int));
就可以写成
pfun_t signal(int, pfun_t);

函数指针数组

数组是一个存放相同类型数据的存储空间,

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

int (*parr1[10])();
int *parr2[10]();
int (*)() parr3[10];
答案是: parr1
其实最明显的,数组的标志是[ ],函数的标志是 ()

根据这两个标志能很快判断出来类型。

parr1 先和 [ ] 结合,说明 parr1是数组名,数组指向 int (*)() 类型的函数指针,数组名
本质是int (*)() *指针,因为指向10个nt (*)() 类型的函数指针,
故parr1的类型是int (*)() [10]

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

        这是一个简答的计算器代码,分别写了加减乘除四种算法的函数,主函数用do……while循环配合switch分支来选择不同的计算方法。

#include <stdio.h>
int add(int a, int b) {
	return a + b;
}
int sub(int a, int b) {
	return a - b;
}
int mul(int a, int b) {
	return a * b;
}
int div(int a, int b) {
	return a / b;
}
int main()
{
	int x, y;
	int input = 1;
	int ret = 0;
	do
	{
		printf("*************************\n");
		printf(" 1:add           2:sub \n");
		printf(" 3:mul           4:div \n");
		printf("*************************\n");
		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");
			break;
		}
	} while (input);
	return 0;
}

但是太长,且有很多重复代码,这时候我们就可以用函数指针数组来完成。

最关键的一句是这个

 int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表

*p[5]其实就相当函数名,类型是int(int x,int y),函数返回类型是int,函数参数是int x和int y,这是一个函数

p先和[5]结合,*p[5]就相当于*(p[5]),

p[5]的类型就是在*p[5]类型中间加(*),就像这样int(*)(int x,int y),这是一个函数指针

p的类型就是在p[5]的类型后面加[5],就是这样int(*)(int x,int y)[5],这是一个函数指针数组。(实际上p是指向int(*)(int x,int y)的 int(*)(int x,int y)* 指针)

当我们调用函数指针数组中的函数指针的时候,首先需要找到对应的数组下标,因为我们设置的菜单分别是1 2 3 4  来选择功能,便加了个0来让下标对上。

比如我们现在要用加法,下标是1,那么就是p[1],p[1]是一个int(*)(int x,int y)的函数指针,要达到我们调用函数的目的还需要加上*来解引用才能完成调用,*p[1],然后加上后面的函数参数,就像下面这样,注意一定要用()来把*p[1]整体括起来

(*p[1])(x, y)

但是你有没有想过一个问题,观察一下这个表达式

 int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表

我们说p的类型是int(*)(int x,int y)[5],指向5个int(*)(int x,int y),那后面初始化的类型也应该是int(*)(int x,int y)这个函数指针才对,为什么却直接写了函数呢?

实际上我们前面提到过 函数名就等于&函数名 ,所以函数指针当中的(*)实际上就是个摆设而已,没有实际作用,为的就是让我们好区分而已。

紧接着,我们把switch语句换成了if语句

if ((input <= 4 && input >= 1))
         {
          printf( "输入操作数:" );
              scanf( "%d %d", &x, &y);
              ret = (*p[input])(x, y); //调用函数指针
//input相当于函数指针数组中的下标
         }
          else
               printf( "输入有误\n" );
          printf( "ret = %d\n", ret);

完整的代码就是这样

#include <stdio.h>
int add(int a, int b) {
	return a + b;
}
int sub(int a, int b) {
	return a - b;
}
int mul(int a, int b) {
	return a * b;
}
int div(int a, int b) {
	return a / b;
}
int main()
{
	int x, y;
	int input = 1;
	int ret = 0;
	int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
	while (input)
	{
		printf("*************************\n");
		printf(" 1:add           2:sub \n");
		printf(" 3:mul           4:div \n");
		printf("*************************\n");
		printf("请选择:");
		scanf("%d", &input);
		if ((input <= 4 && input >= 1))
		{
			printf("输入操作数:");
			scanf("%d %d", &x, &y);
			ret = (*p[input])(x, y);
		}
		else
			printf("输入有误\n");
		printf("ret = %d\n", ret);
	}
	return 0;
}

指向函数指针数组的指针

指向函数指针数组指针是一个 指针
指针指向一个 数组 数组元素都是 函数指针
void test(const char* str) {
 printf("%s\n", str);
}
int main()
{
 //函数指针pfun
 void (*pfun)(const char*) = test;
 //函数指针的数组pfunArr
 void (*pfunArr[5])(const char* str);
 pfunArr[0] = test;
 //指向函数指针数组pfunArr的指针ppfunArr
 void (*(*ppfunArr)[5])(const char*) = &pfunArr;
 return 0; }

分析一下怎么写出来的 ,我们要写 指向 函数指针 数组 的 指针

也就是说有一个指向 函数指针数组 的指针,这个函数指针数组中的元素都是函数指针

那就先需要写一个函数指针,函数指针指向的是函数,就需要先写个函数,

test是函数名,它的类型是void(const char*),返回类型是void,函数参数是const char*。

void test(const char* str) {
 printf("%s\n", str);
}

        现在写一个函数指针指向test函数,我们可以把*pfun看做函数名,而*pfun中的*代表的意思就是pfun是一个函数指针变量,而这个pfun函数指针指向的函数返回类型是void,函数参数是const char*。

 void (*pfun)(const char*) = test;

        然后写一个函数指针数组(存放)指向5个同一类型的函数指针。我们直接在*pfunArr后面加上[5]来表示数组,因为*pfunArr[5]会相当于*(pfunArr[5]),而

*pfunArr[5]的类型是void(const char*)函数,那么

去掉*后的pfunArr[5]类型是void(*)(const char*)函数指针,再

去掉[5]后的pfunArr类型是void(*)(const char*)[5]类型,是指向 5个void(*)(const char*)函数指针 的函数指针数组。

最后把pfunArr中的第一个元素初始化,这里想想为什么不是=pfun函数指针,而是=test函数本身呢?

 void (*pfunArr[5])(const char* str);
 pfunArr[0] = test;

因为函数名就等于&函数名,这就说明其实&函数名类型中的(*)其实是个摆设,没有实际作用,只是为了方便我们人为区分而已。

实际上你选择=pfun函数指针 来初始化也是一样的效果。

(或者更加离谱一些,函数本身就是指针,指向了函数返回类型和函数参数。)

终于到了我们的目的地,要写一个 指向 函数指针数组 的指针 ,而被指向的 函数指针数组  已经写出来了,现在只需要一个指向这个对象的指针。

那当然是把(*pfunArr[5])中的 pfunArr函数指针数组名 换个名字加上*再整体括起来后,ppfunArr就代表指向void(*)(const char*)[5]了。

此时 *ppfunArr 的类型是void(*)(const char*)[5]

去掉*后的ppfunArr类型是void(*)(const char*)[5] *,指向void(*)(const char*)[5] 的指针。

 void (*(*ppfunArr)[5])(const char*) = &pfunArr;

最后进行初始化等于&pfunArr。

回调函数

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

 相当于高中学的复合函数,f(u(x)),将u(x)替换x。

qsort函数的使用

https://cplusplus.com/reference/cstdlib/qsort/

在这个网址中,有qsort函数的介绍。
我简单讲一下。

qsort函数是用来给数组排序的,返回类型为void(也就是说没有返回值),函数参数共有4个。

base就是需要排序的数组,

num是需要排序的元素个数(因为传递过来的数组是首元素的地址,必须要有个数才能确定排序几个元素)

size是指一个元素占几个字节的意思,(因为每个数组存放数据类型不同,char是一个字节,int是4个字节)

compare是指向返回类型为int,函数参数为两个const void*的函数指针。

qsort函数主要是通过compare进行的数组排序。
int compar (const void* p1, const void* p2);

 如上图,如果compare函数的返回值<0,那么p1元素会放到p2元素后, >0则相反,=0说明p1=p2

现在演示它的使用方式

#include <stdlib.h>
int int_cmp(const void * p1, const void * p2) {
  return (*( int *)p1 - *(int *) p2);
//强制类型转换,把指针能够访问的字节变为4byte
}
int main()
{
    int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
    int i = 0;
    qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);
    for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
   {
       printf( "%d ", arr[i]);
   }
    printf("\n");
    return 0; }
可以看到的是,这个cmp函数是我们自己写的。

输出结果也达到了我们的目的,如果想升序的话,思考一下cmp函数中应该怎么写呢?

我的答案的是乘-1。

模仿qsort函数进行冒泡排序

不知道冒泡排序的看这篇文章
我先写了这个
int main()
{
	test();
	return 0;
}

然后在test函数中写好我们需要的大体框架。

test()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	//调用冒泡排序函数
	bubble(arr,sz,sizeof(arr[0]),cmp_arr);
	//打印arr数组
	print(arr,sz);
}

把bubble函数需要的四个参数都确定好,然后写个print函数来打印排序后的arr数组。

我先写了print函数

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

对比 使用bubble函数 和 接收函数参数 

为了具有通用性,所以我们选择了空指针来接收,其他的没啥好解释了。

bubble(arr,sz,sizeof(arr[0]),cmp_arr);
void bubble( void* arr,  int sz,int width, int(*cmp)(const void* e1, const void* e2) )

然后在bubble函数中写好冒泡排序的大体框架

void bubble( void* arr,  //需要排序的数组中第一个元素的地址
			 int sz,     //需要排序的元素个数
			 int width,  //一个元素的大小,单位:字节
			 int(*cmp)(const void* e1, const void* e2) 
			 //函数指针,指向了 (比较2个元素) 的函数
			)
{
	int i = 0;
	int j = 0;
	//趟
	for (i = 0; i < sz; i++)
	{
		//一趟比较
		for (j = 0; j < sz - 1 - i; j++)
		{	

		}
	}

}

现在需要确定的是,我们该如何确定一趟比较中应该 交换元素的条件

这就需要我们的 cmp函数 了,专门比较两个元素大小并返回值。

前面我们知道的 qsort函数 利用 cmp函数 传回来的三种不同范围的值来确定是否进行交换,怎么交换。

现在我们模仿 qsort函数 来写一下我们需要 用cmp函数 传回来的值干些什么。

先写一下 cmp函数,其实和 qsort函数 中的 cmp函数 差不多,返回两个元素相减的值(也就是判断谁大谁小)就可。

int cmp_arr(const void* e1, const void* e2)
{
	return *(int*)e1 - *(int*)e2;
	//空指针不能进行解引用,所以
	//要进行强制类型转换
}

因为我们打算排序成一个升序的数组,那么,如果cmp返回一个<0的值,便需要进行交换,也就是说,需要在 确定一趟比较中应该 交换元素的条件 中调用cmp函数把返回值带回来,那便需要确定传递过去什么参数。

我们在学习qsort函数的时候知道cmp函数是比较两个元素的,而冒泡排序中是根据arr[ j ]和arr[ j+1]来比较进行数字的交换,同样的道理我们可以把这两个参数传到cmp函数中进行比较。

但是需要知道的是,我们bubble接收数组的指针是空指针,不能进行向后挪动的操作来访问下一个元素,前面我们设置的四个参数中有表示 一个元素大小 的参数——width,它的单位是字节,为了方便移动,我们需要把空指针接收的数组首元素地址强制类型转换为(char*),因为char*的步长是一个字节,那么在 j变量乘上width变量后便可以表示 我们需要排序的数组的 一个元素大小的步长了。当然,随着j变量每次循环的增长,移动的步数也是紧挨着的,符合我们的需求。

总之:1· char*强制类型转换 2·确认好步长

if ( cmp ( (char*)arr + j * width , (char*)arr + (j + 1) * width )  < 0)

那么满足条件后便需要进行交换,这个交换函数很简单,直接上代码了

void swap( int width, char*a,char*b)
{
	//一个元素大小的交换
	int i = 0; //挪动几个字节(width
	for (i = 0; i < width; i++)
	{
		char tmp = *a;
		*a = *b;
		*b = tmp;
		a++;
		b++;
	}
}

需要传递的函数参数就交给你自己思考了哦!

是步长和两个需要交换的元素。(属于是字节为单位的交换

现在给上完整的代码

//模仿qsort函数使用冒泡排序
int cmp_arr(const void* e1, const void* e2)
{
	return *(int*)e1 - *(int*)e2;
	//空指针不能进行解引用,所以
	//要进行强制类型转换
}

void swap( int width, char*a,char*b)
{
	//一个元素大小的交换
	int i = 0; //挪动几个字节(width
	for (i = 0; i < width; i++)
	{
		char tmp = *a;
		*a = *b;
		*b = tmp;
		a++;
		b++;
	}
}
void bubble(void* arr, //需要排序的数组中第一个元素的地址
			int sz,    //需要排序的元素个数
			int width, //一个元素的大小,单位:字节
			int(*cmp)(const void* e1, const void* e2) 
			//函数指针,指向了 (比较2个元素) 的函数
			)
{
	int i = 0;
	int j = 0;
	//趟
	for (i = 0; i < sz; i++)
	{
		//一趟比较
		for (j = 0; j < sz - 1 - i; j++)
		{
			//void*是空指针,不能解引用 也不能 向前或者向后走
			//因为你不知道要访问几个字节,但是你可以往里面放任意类型的地址
			if ( cmp ( (char*)arr + j * width , (char*)arr + (j + 1) * width )  < 0)
			{
				//交换
				swap( width, ((char*)arr + j * width), ((char*)arr + (j + 1) * width));
				//传地址,让函数内外产生联系
			}
		}
	}

}
void print(int* arr,int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(arr + i));
	}
}
test()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	//调用冒泡排序函数
	bubble(arr,sz,sizeof(arr[0]),cmp_arr);
	//打印arr数组
	print(arr,sz);
}
int main()
{
	test();
	return 0;
}

如果还想优化,就考虑我们之前讲的assert和const来修饰一下吧。

(头文件我没给,不知道的自己查一下,https://cplusplus.com/reference/

现在你可以看看和我们之前写的冒泡排序代码有何不同。给上链接。

从0开始学c语言-16-数组以及数组传参应用:冒泡排序_阿秋的阿秋不是阿秋的博客-CSDN博客

结构体调用qsort函数

同样的道理,我们试试结构体。

首先创建一个结构体

struct Stu
{
	char name[20];
	int age;
	//结构体大小=20个char + 一个int
};

这是一个tag为Stu的结构体,结构体大小为24byte。

接下来写主函数和test函数

void test()
{
	struct Stu s [3]= { { "aqiu",9 } ,{"aka",56},{"as",4} };
	int sz = sizeof(s) / sizeof(s[0]);
}
int main()
{
	test();
	return 0;
}

在test函数中我们进行了结构体数组的初始化,并且计算了结构体数组的元素个数,这些都是为了使用qsort函数。

qsort(s, sz, sizeof(s[0]), cmp_age);

s代表结构体,sz代表结构体数组的元素个数,sizeof(s[0])代表一个结构体的大小,cmp_age是我们需要调用的函数,用来比较两个元素的大小并带回来一个返回值。

cmp_age函数的返回类型和函数参数类型都没变化,和我们上面写的不太一样的地方是,访问两个元素的方式不太一样了,因为我们是结构体元素的比较,便需要转换为结构体指针来访问结构体成员并且比较。

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

好了,调用结束。

现在我们试试比较名字来排序的cmp函数怎么写,其实道理一样的,只不过我们比较名字就是在比较字符串,比较字符串又需要使用比较字符串的函数strmp。

int cmp_name(const void* e1, const void* e2)
{
	return strcmp( ( (struct Stu*)e1 )->name, ( (struct Stu*)e2 )->name);
	//很容易括号出问题
}

同样的道理,我们把自己上面写的bubble函数中的cmp函数换成cmp_age或者cmp_name函数也可以进行结构体排序。 

完整的代码太长了,bubble函数的代码就在上面,这里只给上结构体的完整代码。

#include <string.h>
#include <stdlib.h>
struct Stu
{
	char name[20];
	int age;
	//结构体大小=20个char + 一个int
};
int cmp_age(const void* e1, const void* e2)
{
	return (((struct Stu*)e1)->age) - (((struct Stu*)e2)->age);
}
int cmp_name(const void* e1, const void* e2)
{
	return strcmp( ( (struct Stu*)e1 )->name, ( (struct Stu*)e2 )->name);
	//很容易括号出问题
}

void test()
{
	struct Stu s [3]= { { "aqiu",9 } ,{"aka",56},{"as",4} };
	int sz = sizeof(s) / sizeof(s[0]);
	qsort(s, sz, sizeof(s[0]), cmp_age);
	/*bubble(s, sz, sizeof(s[0]), cmp_age);*/ 
	//bubble(s, sz, sizeof(s[0]), cmp_name);
}
int main()
{
	test();
	return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值