【逐步剖C】-第八章-指针进阶-上

前言:在【逐步剖C】-第五章-指针初阶中。我们已经介绍了有关指针的基础知识,本章将分享有关指针的进阶知识,本文较长,内容较多较干,请配合白开水进行食用。

一、字符指针

对于字符指针char*我们需要知道它的两个常用方法:

1. 存储单个字符的地址

如下代码:

int main()
{
	char ch = 'w';
	char *pc = &ch;
	*pc = 'w';
	return 0;
}

指针变量中存储着变量ch的地址,并可对其进行解引用操作。

2. 存储字符串的地址

如下代码:

int main()
{
	const char* pstr = "hello world.";
	printf("%s\n", pstr);
	return 0;
}

需要区分的是,这里存储的不是一整个字符串,而是字符串首字符的地址,在代码中也就是字符h的地址。因为通过字符h的地址,对指针进行加减整数,解引用的操作就可以得到整个字符串的内容。
这也是一般的编译器要求在指针变量前加上关键字const 的原因,即编译器不希望对需要存储的字符串内容进行修改。如果不加const而执意要更改,从调试中我们也可以看到是不允许的,如图:
在这里插入图片描述
所以,不管编译器有没有提醒,在用字符指针存储字符串的地址时最好都加上关键字const,这样代码就显得更加严谨一些。

3. 相关面试题

有了上面的区分后,来看这样一道面试题:
下面程序输出的结果是:

#include <stdio.h>
int main()
{
	char str1[] = "hello bit.";
	char str2[] = "hello bit.";
	const char *str3 = "hello bit.";
	const char *str4 = "hello bit.";
	if(str1 ==str2)
		printf("str1 and str2 are same\n");
	else
		printf("str1 and str2 are not same\n");
	if(str3 ==str4)
		printf("str3 and str4 are same\n");
	else
		printf("str3 and str4 are not same\n");
	return 0;
}

输出结果:
在这里插入图片描述
解释str1str2数组名,表示的是数组首元素的地址,而创建数组会在内存中开辟两块不同的空间,故虽然存储的字符串内容相同,但首元素的地址肯定不同str3str4字符指针变量,它们在初始化时指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当几个指针指向同一个字符串的时候,他们实际会指向同一块内存。所以str1和str2不同,str3和str4相同。

小结一下字符指针与字符数组存储字符串的区别
字符指针存储字符串,本质是存储字符串中首字符的地址,且编译器会将整个字符串“视为”常量字符串,把字符串存储到单独的一个内存区域,不能通过解引用操作对其内容进行修改;
字符数组存储字符串,本质是给字符串中的所有字符都分配了内存空间(包括'\0'),其内容可以进行修改

二、指针数组

这一部分知识在【逐步剖C】-第五章-指针初阶中也有详细介绍,这里就不再过多地介绍啦。指针数组,就是一个数组,数组的每个元素都是一个指针。

int* arr1[10]; //整形指针的数组
char *arr2[4]; //一级字符指针的数组
char **arr3[5];//二级字符指针的数组

三、数组指针

1. 数组指针的定义

首先我们明确一下,数组指针是数组,还是指针?
答案是:指针。我们可由以下“命名”规则推出:

字符指针-->存放字符地址的指针-->指向字符的指针--> char*
整型指针-->存放整型地址的指针-->指向整型的指针--> int*
浮点型指针-->存放浮点型地址的指针-->指向浮点型的指针--> float*,double*
数组指针-->存放数组地址的指针-->指向数组的指针

那么数组指针的类型是什么样的呢?即该如何定义呢?下面介绍
我们知道指针数组定义为:

int* arr[10];
//有10个整型指针元素的数组

数组指针的定义和其很像:

int(*p)[10];
//一个指向有10个整型元素的数组的指针

解释:p先和 * 结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫数组指针。
这里要注意:[]的优先级要高于 * 号的,所以必须加上()来保证p先和*结合
由上面的定义可得,p为指针变量的变量名,去掉变量名后我们就得到了它的类型,故如上数组指针的类型为int(*)[10]
(PS:这里关于类型的理解在后文中会大量用到,且个人觉得也很重要,关于此种理解方式在【逐步剖C】第三章-数组中有简单说明,感兴趣的朋友们可以看看)

2. &+数组名 VS 数组名

先说结论:&数组名 指的数“整个”数组的地址,也就上面数组指针定义所说的 “数组地址”,故&数组名 取出来的地址就应该存放在对应类型的数组指针变量中,如:

int arr[10];
int(*p)[10] = &arr;
//数组指针p存放的是整个数组的地址

数组名,是数组首元素的地址,存放在对应类型的一级指针变量中,如:

int arr[10];
int* p = arr;
//一级指针p存放的是数组首元素也就是arr[0]的地址

下面再结合指针类型在加减整数操作上的意义与实际代码进一步说明:
代码:

#include <stdio.h>
int main()
{
	int arr[10] = { 0 };
	printf("arr = %p\n", arr);
	printf("&arr= %p\n", &arr);
	
	printf("arr+1 = %p\n", arr+1);
	printf("&arr+1= %p\n", &arr+1);
	return 0;
}

运行结果:
在这里插入图片描述
解释:可以看到,&数组名数组名的地址值虽然一样,但由于其地址本质的意义的不同,导致它们执行+1后的结果不同。由指针初阶的知识(PS:文章在这:【逐步剖C】-第五章-指针初阶,感兴趣的朋友可以看看),我们知道,指针类型的意义决定了其向前(减整数)或向后(加整数)走一步有多大。

  • 对于&arr,其指针类型是int[10],即一个有10个整型元素的数组,故其在+1后往后走了10个整型的大小,即跳过了整个数组的大小(10个整型大小为40字节,40转换为16进制就是28,60+28=88,如运行结果所示)
  • 对于数组名,其指针类型是int,即一个整型,故其在+1后往后走了1个整型的大小,(1个整型的大小为4字节,60+4=64,如运行结果所示)

示意图参考:
在这里插入图片描述
如上就是&数组名数组名的区别。

3. 数组指针的使用:

数组指针在实际使用中其实很少有写成上面所说int arr[10]; int(*p)[10] = &arr;这种形式,更多的是用来作为函数的参数来。看下面这段代码:

#include <stdio.h>
void print_arr1(int arr[3][5], int row, int col)
{
	int i = 0;
	for (i = 0; i < row; i++)
	{
		for (j = 0; j < col; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
}

void print_arr2(int(*arr)[5], int row, int col)
{
	int i = 0;
	for (i = 0; i < row; i++)
	{
		for (j = 0; j < col; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
}

int main()
{
	int arr[3][5] = { 1,2,3,4,5,6,7,8,9,10 };
	print_arr1(arr, 3, 5);
	//可以数组指针来接收
	print_arr2(arr, 3, 5);
	return 0;
}
  • 若想通过函数打印一个二维数组,我们需要将二维数组进行传参,那在参数设计上应该设计为什么类型来接收呢?

一般是将其设计为与实参形式相同的二维数组,如打印函数print_arr1中所示;在学习数组指针之后我们也可以用对应类型的数组指针作为参数来接收,如打印函数print_arr2中所示。
在函数print_arr2中,arr[i]就相当于对传进来的数组指针进行解引用操作(即*(arr+i))而得到了整个下标为i的一行的一维数组,而后再通过[j]来访问该一维数组中对应下标的元素

还有一点需注意,我们在传参时写的是arr,即二维数组的数组名,我们知道,数组名arr,表示首元素的地址,但是二维数组的首元素是二维数组的第一行,所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址,故可以用数组指针来收,通过解引用数组指针即可访问这个一维数组及其中的所有元素。

4. 小练习

看下面代码分别是什么意思?

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

第一行定义了一个有5个整型元素的数组
第二行定义了一个有10个整型指针的数组
第三行定义了一个指向有10个整型元素的数组的指针
第四行定义了一个数组,该数组有10个元素,每个元素是一个指向有5个整型元素的数组的指针。

四、数组参数与指针参数

1. 一维数组传参

如下代码,那个函数能正确完成参数接收?

#include <stdio.h>
void test(int arr[])
{}
void test(int arr[10])
{}
void test(int* arr)
{}
//哪一个能接收主函数中的一维整型数组?

void test2(int* arr[20])
{}
void test2(int** arr)
{}
//哪一个能接收主函数中的一维整型指针数组?

int main()
{
	int arr[10] = { 0 };
	int* arr2[20] = { 0 };

	test(arr);
	test2(arr2);
}

上面所展现的所有函数都能正确完成任务,接下来进行说明:

  • 对于参数int arr[]和参数int arr[10]实际上是一回事,一维数组作为参数,数组大小可省略,用一维数组接收传过来的一维数组,没问题;
  • 对于参数int* arr,因为传参时写的时arr,也就是一维数组的数组名,也就是首元素的地址,用一个int*类型的指针来接受int型变量的地址,没问题。

这里先插一句我个人觉得比较好用的理解方式,这个理解方式在上文也提到过。就是,我们在自己设计函数参数或者判断一个函数的参数设计是否正确时重点要看出它的类型,而不用过于关注参数名。如上面五个函数的类型就分别为:

void test(int arr[])------------类型:int[]
void test(int arr[10])----------类型:int[10]
void test(int* arr)-------------类型:int*
void test2(int* arr[20])--------类型:int*[20]
void test2(int** arr)-----------类型:int**

其中的arr就知识参数名而已,并不是我们判断的重点。这个理解方式在后文的说明中还会用到,所以在后文就不再赘述啦。回到问题中

  • 对于参数int* arr[20],其类型为指针数组,用来接收传过来的指针数组,没问题。
  • 对于参数int** arr,其类型为二级指针,又因为传参本质上传的是指针数组首元素的地址,也就是一级指针的地址,用二级指针来接收,没问题。

2. 二维数组传参

如下代码,那个函数能正确完成参数接收?

void test(int arr[3][5])
{}
void test(int arr[][])
{}
void test(int arr[][5])
{}

void test(int *arr)
{}
void test(int* arr[5])
{}
void test(int (*arr)[5])
{}
void test(int **arr)
{}
int main()
{
	int arr[3][5] = {0};
	test(arr);
}

解释

void test(int arr[3][5])-------->int[3][5]
void test(int arr[][])---------->int[][]
void test(int arr[][5])--------->int[][5]

void test(int *arr)------------->int*
void test(int* arr[5])---------->int* [5]
void test(int (*arr)[5])-------->int(*)[5]
void test(int **arr)------------>int**
  • 对于前三个,只有第二个不能正确完成接收,因为二维数组传参,函数形参若设计为二维数组的形式只能省略第一个[]的数字。因为对一个二维数组,可以不知道有多少行,但是必须知道一行有多少个元素(即列数)。这样计算机才能进行运算。
  • 对于后四个,只有第三个能完成接收,上面说过,二维数组的数组名代表的是二维数组中首元素,也就是二维数组中整个第一行的一维数组的地址,故只能用对应类型的数组指针来接收。而后四个中只有第三个int(*)[5]是数组指针,第一个int*是一级指针;第二个int* [5]是指针数组;第四个int**是二级指针。

小结:数组在传参时一般直接以数组名作为实参传递,即test(arr),在此基础上设计的函数形参大体分为两种:形参可设计成和实参数组类型相同的数组,如int arr[3][5];或设计成能存储实参数组名所代表的地址类型的指针,如:int (*arr)[5]

3. 一级指针传参

当一个函数的参数部分为一级指针时,函数能接收:
(1)对应类型的变量的地址;
(2)对应类型的一维数组的数组名;
(3)对应类型的一级指针变量

如:

void test(int* p){;}
int main()
{
	int i = 0;
	int arr[10] = {0};
	int* pi = &i;
	test(&i);	//(1)
	test(arr);	//(2)
	test(pi);	//(3)
	return 0;
}

4. 二级指针传参

当一个函数的参数部分为二级指针时,函数能接收:
(1)对应类型的一级指针变量的地址;
(2)对应类型的一维一级指针数组的数组名;
(3)对应类型的二级指针变量

如:

void test(int** p){;}
int main()
{
	int i = 0;
	int* arr[10] = {NULL};
	int* pi = &i;
	int** ppi = &pi;
	test(&pi);	//(1)
	test(arr);	//(2)
	test(ppi);	//(3)
	return 0;
}

五、函数指针

1. 函数指针的定义

由上文数组指针部分提到的理解方式与“命名规则”可知:

函数指针-->存放函数地址的指针-->指向函数的指针

学习这部分知识时,我才第一次听说函数也有地址,看下面这段代码:

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

输出结果:
在这里插入图片描述
输出的两个地址就是代码中test函数的地址。那么想保存一个函数的地址,自然就需要一个函数指针。这里继续以test函数为例,能保存test函数的指针定义就写为:void(*pfun)(),其类型为:void(*)()
解释pfun先和*结合,说明pfun是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void。

再看一个例子:
一个执行加法的函数:

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

那么能存储其地址的函数指针就写为:int(*pf)(int,int),其类型就为:int(*)(int,int)
可以看出,在定义函数指针时,其类型一定要和其所将要指向的函数的返回类型与参数类型一 一对应。

补充
定义函数指针后,将函数地址赋给指针的语句为(以上面Add函数为例):

int(*pf)(int,int) = Add;int(*pf)(int,int) = &Add;
二者都对,没有本质区别

2. 函数指针的使用

和之前其他类型的指针变量的使用相同,先进行解引用操作,然后像正常函数调用那样使用就行,仍以Add函数为例:

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

但这里也有一个与其他类型的指针变量的使用不同的点:*的解引用操作本质上只是个修饰可有可无,不会影响函数的调用,即:

(*pf)(2,3) 等价于 (***...*pf)(2,3) 等价于 pf(2,3) 等价于 Add(2,3)

3. 两段有趣的代码:

看下面两段代码分别是什么意思:

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

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

这两段代码看上去多少让人不寒而栗,但实际分析起来其实难度并不算很大。

  • 代码1,最内层括号中的void (*)()为函数指针的类型,接着往外的(void (*)())0表示将0强制转换void (*)()型的函数指针,最后通过*解引用来进行该函数的调用。综上,代码1是一次函数调用,调用的是0地址处的函数
  • 代码2,是一次函数声明,声明的函数叫做signal
    该函数的参数类型int型和void(*)(int)型;
    该函数的返回类型void(*)(int)型,即signal函数会返回一个指向参数类型为int,返回类型为void的函数的指针

对于代码2,写成如下形式可能会更好理解些:

void(*)(int) signal(int , void(*)(int));
	|			     |			|
	|				 |			|
返回类型		   参数1类型     参数2类型

但语法不允许,所以只能写成原来的形式,即:

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

对于代码2,还可通过typedef关键字进行简化,如下:

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

以上两段代码来自书籍《C陷阱和缺陷》,有兴趣的朋友可以看看。

六、函数指针数组

由前面指针数组的知识可知,字符指针数组是每个元素为字符指针变量的数组,整型指针数组是每个元素为整型指针的数组,那么函数指针数组就是每个元素为函数指针的数组

1. 函数指针数组的定义

这里需要和之前其他类型的指针数组做一个小区别,看下面三行代码,哪一行是正确的函数指针数组的定义方式呢?

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

看上去是parr3,实际上是parr1,这和上面两段有趣代码中的代码2有个相似之处是:parr3看起来更清晰,更好理解一些,但语法不支持。所以,parr1才是正确的定义方式。
解释[]要在()里与parr1先结合,说明它是数组;而后说明数组的每一个元素都是int (*)()类型的函数指针。

2. 函数指针数组的用途

函数指针数组通常用于转移表的编写。
如下面这个例子:
如果我们要实现一个有加减乘除功能的计算器,我们可以这么写:

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

利用case语句及do while循环即可实现,但这么些我们发现有一个问题:
将来若想增添一些别双目的运算,如按位与'&'、按位或'|'等,代码需要更改的地方太多,可维护性不高,那么我们可以通过函数指针数组编写转移表进行优化,请看:

#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 (*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;
}

可以看出,所谓转移表其实就是用函数指针数组存储函数的地址,等到需要调用函数时,直接从函数指针数组中访问对应下标的函数,然后正常进行传参调用即可
这样将来再添加更多运算时就只需要写函数、加入数组、更改判断条件即可。不用像之前一样可能还需要写很多case语句,而导致代码写得有些冗长。

七、指向函数指针数组的指针

我第一次学到这时脑瓜子嗡嗡的,感觉数组和指针可以无限套娃,就比如上面标题,那我是不是还能有一个数组,数组的每个元素都是指向函数指针数组的指针?…套娃是能套,但实际意义并不大。这个即将要介绍的指针其实也不太常用,所以下面只简单介绍一下其定义,大家了解一下即可。

1. 指向函数指针数组的指针定义

那么由上面所介绍的“命名”规则与语法要求,指向函数指针数组的指针的定义就可由函数指针一步步推出,请看下面这段代码:

void test(const char* str)
{
printf("%s\n", str);
}
int main()
{
//函数指针pfun
void (*pfun)(const char*) = test;-------------------------->void(*)(const char*)

//函数指针的数组pfunArr
void (*pfunArr[5])(const char* str);----------------------->void(*[5])(const char*)
pfunArr[0] = test;

//指向函数指针数组pfunArr的指针ppfunArr
void (*(*ppfunArr)[5])(const char*) = &pfunArr;------------>void(*(*)[5])(const char*)

return 0;
}

八、 回调函数

前言:前面介绍有关函数指针的内容其实都在为这一部分铺垫,这一部分知识很重要。

1. 回调函数的概念

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

2. qsort函数的使用

系统库函数qsort函数就是一个很好的回调函数的例子,我们可以从相关手册查看它的参数,及相关使用方法等:
在这里插入图片描述
(1)下面对这个函数进行一个简单的说明。
这个函数本质上的作用是将一组数据进行快速排序。
下面是它的参数说明
在这里插入图片描述
看上去有6个参数,但本质上只有4个,因为最后一个参数为函数指针。那么下面逐一对其参数进行一下简单的介绍:

  • 参数1void* base,这里需要传给qsort函数的是待排序数组的首元素的地址
    这里有个巧妙的点:参数的类型为void*,因为编写这个函数的设计者并不确定你将来会给什么样的类型进行排序,而void*类型的指针可以接收任何类型的地址,但同时也有个缺点:其不能直接进行*解引用和++等操作。这个巧妙的点待会在参数4也有体现。
  • 参数2size_t num,这里需要传给qsort函数的是待排序数组的大小(元素个数)。其中的size_t型本质上是无符号整型unsigned int
  • 参数3size_t width,这里需要传给qsort函数的是待排序数组的每个元素所占字节数,也就是每个元素的大小,关于这一参数的巧妙意义,会在后面讲述模拟qsort函数时说明。
  • 参数4int (*cmp)(const void* elem1, const void* elem2)。可以忽略cmp前面的_cdecl(给编译器用的),那么可以看出参数4是一个函数指针,指针名为cmp,指向的函数的返回类型为int型,指向函数的两个参数的类型都为const void*型。这个指向的函数也叫比较函数,需要我们自己设计(后文会继续说明),然后把设计完成的函数的地址作为参数4传给qsort函数

接下来演示一下怎么使用qsort函数。
(2)函数的使用
请看下面这段代码:

#include<stdio.h>
#include<stdlib.h>

int int_cmp(const void* elem1, const void* elem2)
{
	return ( (*(int*)elem1) - (*(int*)elem2));
	//升序排列;实现降序排列,用elem2的值减elem1即可
}

int main()
{
	int arr[10] = { 9,3,2,4,6,7,5,8,1,0 };

	int arr_size = sizeof(arr) / sizeof(arr[0]);
	int elem_wid = sizeof(arr[0]);
	//通过sizeof来计算数组的大小,及数组元素的大小

	qsort(arr, arr_size, elem_wid, int_cmp);

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

	return 0;
}

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

说明:其实就是按函数参数设计的要求进行传参:参数1传的就是数组名,因为数组名代表了首元素的地址;参数2和参数3都可以通过关键字sizeof算出来。关键就在于参数4比较函数的设计。
那么关于比较函数的设计有这么几个需要注意的地方

  • 比较函数的返回类型与参数类型需与qsort函数参数4中的一致,及返回类型为int型,两个参数类型为const void*型;
  • 这里const void*的原因是不希望改变函数的参数的同时,使其能接收任何类型的地址,但不能直接进行解引用等操作,所以在编写比较函数时,要根据需求进行强制类型转换,如上面的代码就是在已知是整型元素之间的比较的情况下把参数强制转换为int*型,进而能进行解引用等操作;
  • 比较函数的返回值之所以被设计为整型,是因为比较函数的返回值其实是有要求的,要求如下:
    在这里插入图片描述
    若elem1小于elem2,则返回一个小于0的数;若相等,则返回一个等于0的数;若大于,则返回一个大于0的数。其实这个返回值的要求的意图很明显了,就是让你把需要比较的两个元素做差,差值情况就是它们的大小情况了。

如上是整型类型元素排序的例子,下面再举一个结构体类型排序的例子。
假设这个结构体类型名为Stu,有nameage两个成员,即:


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

这里我们需要以age为基准进行一次排序;以name为基准再进行一次排序。
请看代码:


//比较函数
#include<stdio.h>
#include<stdlib.h>
#include<string.h>

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

int cmp_stu_by_age(const void* elem1, const void* elem2)
{
	return ( ((struct Stu*)elem1)->age - ((struct Stu*)elem2)->age );
}

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

int main()
{
	struct Stu s[3] = { {18,"zhangsan"}, {19,"lihua"}, {17,"wanggang"} };
	//结构体数组初始化
	
	int s_size = sizeof(s) / sizeof(s[0]);
	int s_wid = sizeof(s[0]);
	//通过sizeof来计算数组的大小,及数组元素的大小
	
	qsort(s, s_size, s_wid, cmp_stu_by_name);
	qsort(s, s_size, s_wid, cmp_stu_by_age);

	return 0;
}

这里我们通过调试窗口查看排序的结果:
结构体数组的初始情况
在这里插入图片描述

执行语句qsort(s, s_size, s_wid, cmp_stu_by_name);

在这里插入图片描述

执行语句qsort(s, s_size, s_wid, cmp_stu_by_age);

在这里插入图片描述
说明:大体上和整型数据的排序相同,但需要留意的一点是关于以name为基准的比较函数的编写,比较两个字符串的大小,我们一般用库函数strcmp来实现,且strcmp的返回值刚好与比较函数返回值的要求一致,如下图:
在这里插入图片描述

3. qsort函数的模拟

上面介绍了qsort函数的使用,接下来介绍qsort函数的模拟。之所以说是模拟,是因为这里采用冒泡排序的方式实现,而qsort函数本质是快速排序。
设计思路:
(1)既然是模拟,那么设计的参数就应该与qsort保持一致,即有:

void bsort(void* base, size_t num, size_t width, int(*cmp)(const void* elem1, const void* elem2))

(2)接下来就是函数体内部的实现了。因为是以冒泡排序为基础,所以元素之间比较的次数没有变化;因为需要应对于所有可能类型元素的比较,所以需要改变的就是相邻两元素的比较方式以及符合条件后的交换方式
对于比较方式的问题,可以通过传进来的比较函数解决;
对于交换相邻两素的问题,需要我们自己再设计一个交换函数;
至此,整个模拟函数的框架就为:

void bsort(void* base, size_t num, size_t width, int(*cmp)(const void* elem1, const void* elem2))
{
	int i = 0;
	int j = 0;
	for (i = 0; i < num - 1; i++)
	{
		for (j = 0; j < num - i - 1; j++)
		{
			if(//比较函数)
				{
					//交换函数
				}
		}
	}
}

下面介绍比较函数的使用以及交换函数的编写 (PS知识点:char*型指针的妙用)

  • 比较函数的使用
    首先我们能确定的是比较函数的返回值,假设我们想要升序排列,则if语句的判断部分就写为:if(cmp(参数1,参数2) > 0);接下来是参数部分,由冒泡排序的工作原理可知,需要进行比较的两个参数是数组中的相邻元素。
    但这里还有个问题:我们在设计这个函数时,并不确定其将来会给什么类型的数据进行排序,所以将第一个参数base设置为了void*型,那么如何将相邻两个元素的地址表示出来呢?
    这里就体现了一个 char*型指针的妙用技巧,以及要求传每个元素所占字节大小的意义了。
    具体的操作过程是:将参数base强制转化为char*型,那么内层循环中,当j = 0时,base + j * width就是数组中第一个元素的地址 (注:这里不能解引用,因为比较函数要求传的就是地址)base + (j + 1) * width就是其相邻元素的地址,由此一来,if语句的判断部分就完成了:
if( cmp( (char*)base + j * width , (char*)base +  (j+1) * width) > 0 )
  • 交换函数
    这里的思路和比较函数相似,首先我们确定返回类型为void型;参数需要三个,分别是:数组每个元素所占字节数size_t width, 需要交换的两个数const void* elem1, const void* elem2 (PS:这里需要交换两个数的类型也可以是char*,因为在比较阶段就已将参数base强制转换了) 这里需要传每个元素所占字节数的原因和比较函数一样,我们虽然不确定要交换两个什么类型的数据,但我们可以通过char*型的指针一个字节一个字节地进行交换,直至交换次数达到交换的元素的大小。我们可以通过循环实现。由此,交换函数部分也完成了:
void swap_in_bsort(size_t width, const void* elem1, const void* elem2)
{
	size_t i = 0;
	char tmp = 0;
	for (i = 0; i < width; i++)
	{
		char tmp = *((char*)elem1 + i);
		*((char*)elem1 + i) = *((char*)elem2 + i);
		*((char*)elem2 + i) = tmp;
	}
}

至此,整个函数的模拟我们就完成了,函数的使用和qsort完全一样,下面是完整代码,请看:

void swap_in_bsort(size_t width, const void* elem1, const void* elem2)
{
	size_t i = 0;
	char tmp = 0;
	for (i = 0; i < width; i++)
	{
		char tmp = *((char*)elem1 + i);
		*((char*)elem1 + i) = *((char*)elem2 + i);
		*((char*)elem2 + i) = tmp;
	}
}

void bsort(void* base, size_t num, size_t width, int(*cmp)(const void* elem1, const void* elem2))
{
	size_t i = 0;
	size_t j = 0;
	for (i = 0; i < num - 1; i++)
	{
		for (j = 0; j < num - i - 1; j++)
		{
			if( cmp( (char*)base + j * width , (char*)base +  (j+1) * width) > 0 )
				{
				swap_in_bsort(width, (char*)base + j * width, (char*)base + (j + 1) * width);
				}
		}
	}
}

本章完。

看完觉得有觉得帮助的话不妨点赞收藏鼓励一下,有疑问或看不懂的地方或有可优化的部分还恳请朋友们留个评论,多多指点,谢谢朋友们!🌹🌹🌹

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值