关于C语言的知识放在专栏:C
小菜坤日常上传gitee代码:https://gitee.com/qi-dunyan
❤❤❤
个人简介:双一流非科班的一名小白,期待与各位大佬一起努力!

本文承接了上文:初阶指针——全面了解指针,在此基础上对指针进行更深一步的剖析,相信我,也请相信你自己!

指针全解

  • 前言
  • 1、初阶指针大致回顾
  • 2、 字符指针
  • 3、指针数组
  • 4、 数组指针
  • 5、数组传参和指针传参
  • 6、函数指针
  • 7、 函数指针数组
  • 8、指向函数指针数组的指针
  • 9、回调函数
    • qsort 函数实现
    • qsort函数模拟(冒泡思想)

前言

我们都知道,指针是C语言中必不可少的一部分,是C语言的精髓所在,一个学习C语言的人如果不对指针有着深刻的理解,那还不算得上是真正入门,本篇文章整理了对于指针更深一步的了解所学到的知识,以及一些个人的见解,期待能帮到您对此有更深刻的认识。话不多说,直接开搞!

1、初阶指针大致回顾

在之前的指针初阶文章里,主要讲了以下内容:

  1. 指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
  2. 不管是什么类型的指针,它的大小是固定的4/8个字节(32位平台/64位平台)。
  3. 指针是有类型,指针的类型决定了指针的±整数的步长,指针解引用操作的时候的权限。
  4. 指针的运算——两个指针相减后的结果的绝对值,就是两个指针之间的元素个数。
  5. 避免野指针的出现,写出好的代码
  6. 二级指针是存放一级指针地址的指针变量

大概就是这些,接下来,正式进入指针进阶。

2、 字符指针

字符指针是用来存放字符的一种指针类型,我们平常都是这么使用:

#includeint main(){char ch = 's';char* p = &ch;//这里的p就是指针变量,类型为char*,存放字符's'的地址return 0;}

或者这样使用:

#includeint main(){const char* p = "abcdef";//这里要注意一点,就是这里的p存放的实际上是字符串首字符'a'的地址,并不是整个字符串的地址return 0;}

2.1 字符数组与字符指针
大家这里还要注意一点,就是字符数组与字符指针之间的区别,它们两个看上去很像,但是所表达的意思却完全不同,如下代码:

#includeint main(){char str[] = "abcdef";char* str1 = "abcdef";return 0;}

在这里,str表示的是一个数组,既然是数组,那么这里面的元素是可以进行修改的,就比如把a改成z,只需要把z赋值给数组首元素就可以了,但是,下面的str1所指向的是一个字符串常量,而字符串常量是不可以进行修改的。如下:

#includeint main(){char str[] = "abcdef";char* str1 = "abcdef";str[0] = 'z';*str1 = 'z';return 0;}


所以我们一般在用字符指针指向字符串常量时,前面会加一个const,从而增加代码的健壮性,避免我们后面万一修改字符串常量的操作,因为修改的话,编译器是不会报错的,只有通过调试才能发现错误,加上const后,如果进行修改的话,编译器会报错提醒。
另外,我们还可以通过字符串首字符的地址找到整个字符串:

#includeint main(){const char* str1 = "abcdef";printf("%s\n", str1);//abcdefprintf("%c\n", *str1);//areturn 0;}

这里注意一点,str1存放的是首字符a的地址,所以再对其解引用的时候,打印出来的其实是首字符a。

2.3 练习题

#include int main(){char str1[] = "hello";char str2[] = "hello";const char *str3 = "hello";const char *str4 = "hello";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;}

对于这个题,我们要了解一点,就是。C/C++会把常量字符串存储到单独的一个内存区域,当几个指针指向同一个字符串的时候,他们实际会指向同一块内存。但是数组就不同了,就算数组的内容相同,但是str1和str2都会开辟属于自己的内存空间。如下所示:


所以,这里str3与str4是等价的,但str1与str2不等价,所以最终实际结果为:
str1 and str2 are not same
str3 and str4 are same

3、指针数组

在前面的文章里已经讲过了关于指针数组的知识,指针数组是数组,数组里的元素为指针类型,也就是说,指针数组是一个用来存放指针(地址)的数组。

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

举个例子:

我们可以通过指针数组来实现用一维数组模拟二维数组:

#includeint main(){int arr1[] = { 1,2,3,4 };int arr2[] = { 2,3,4,5 };int arr3[] = { 3,4,5,6 };//数组名表示首元素地址,这里把arr1、arr2、arr3的首元素存放在指针数组arr里int* arr[] = { arr1,arr2,arr3 };int i = 0;for (i = 0; i < 3; i++){int j = 0;for (j = 0; j < 4; j++){//先通过arr[i]找到每个数组的首元素,[j]表示找到的那个数组的下标为j的元素arr1[0]、arr1[1]......printf("%d ", arr[i][j]);}printf("\n");}return 0;}

4、 数组指针

4.1定义
数组指针,看起来与指针数组很像,但是表达的意思却完全不同,数组指针,顾名思义它是个指针,我们知道整形指针int*,它指向的对象为整形,浮点型指针float*指向的对象是浮点型,那么,类比一下,我们就可以知道,数组指针指向的对象为整个数组
我们下面来看以下两个代码:

int *p1[10];int (*p2)[10];//我们把变量名称去掉,然后剩下的就表示变量的类型,在这里,[]的优先级要高于*号,所以p1表示的是一个数组,数组具有十个元素,每个元素的类型为int*,而()的优先级要高于[],所以这里p2首先表示的是一个指针,然后指向了一个具有10个元素的数组,数组的每个元素为int

4.2&数组名、数组名
再次复习一下&数组名与数组名
数组名表示首元素地址,除去两个例外。
1、sizeof(数组名),在这里,数组名代表的就是整个数组,所求出来的就是整个数组的大小。
2、&数组名,数组名前面加上&,表示取出的是整个数组的地址。

#includeint main(){int arr[5] = { 1,2,3,4,5 };printf("%p\n",arr);printf("%p\n",&arr[0]);printf("%p\n",&arr);printf("\n");printf("%p\n", arr+1);printf("%p\n", &arr[0]+1);printf("%p\n", &arr+1);return 0;}

这里我们来验证一下:

4.3数组指针的使用
4.3.1一维数组里的应用

#include//用数组指针来接收整个数组的地址,这里注意[]要与数组arr的[]相对应void test(int(*p)[8], int sz){int i = 0;for (i = 0; i < sz; i++){printf("%d ", (*p)[i]);//p=&arr,*p==*&arr==arr//*和&放一起,就相当于抵消//所以(*p)[i]就相当于arr[i]//还可以写为*((*p)+i)//*((*p)+i) == *(arr+i) ==arr[i],而arr表示首元素地址,+i后就可以指向数组的下标为i的元素,再解引用就会访问arr里下标为i的元素}}int main(){int arr[8] = { 1,2,3,4,5,6,7,8 };//这里的arr单独放在sizeof内部,求出来的是整个数组的大小int sz = sizeof(arr) / sizeof(arr[0]);//这里&arr,取出的是整个数组的地址test(&arr, sz);return 0;}

大家可以看一下这段代码,注释中解释了每一个原因,相信会对大家的理解有所帮助。
同样,数组指针的应用还经常应用在二维数组里。
4.3.2二维数组里的应用

#include//由于传过来的是二维数组里的第一行的地址,第一行有四个元素,所以可以把它看作传来的是一个有四个元素的一维数组的地址//所以用数组指针来接收,指针指向数组的四个元素//row,col分别接收行和列void test(int(*p)[4], int row,int col){int i = 0; for (i = 0; i < row; i++){int j = 0;for (j = 0; j < col; j++){printf("%d ", *(*(p + i) + j));//*(p+i)==p[i],也就是每一行的数组名,而每一行把它看成一维数组,数组名又表示数组首元素的地址,所以*(p+i)+j就指向了每一行的下标为j的元素的地址,再进行解引用,就访问到了每一行每一列的元素}printf("\n");}}int main(){int arr[3][4] = { 1,2,3,4,5 };//这里arr表示数组首元素的地址,二维数组的首元素是整个第一行的地址,所以这里本质上传的参数其实是整个第一行,是一维数组test(arr,3,4);return 0;}


在二维数组里,数组名表示数组首元素地址,二维数组的首元素地址就是整个第一行的地址。而每一行,又是一个一维数组。


4.4小练习
以下所代表的意思

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

int arr [5]就是一个数组,有五个元素,每个元素类型为int

int parr1[10],首先parr1是一个数组,数组10个元素,每个元素类型为int*,所以这是一个指针数组

int(*parr2)[10],parr2是一个指针,指针指向一个数组,所指向的数组有10个元素,每个元素类型为int

int (*parr3[10])[5],parr3是一个数组,有10个元素,每个元素类型为数组指针(地址),该指针指向一个数组,有五个元素,每个元素为int
如果还是不理解这个,就参考以下下面的图解,类比一下就明白了

以上就是关于数组指针的内容,下面继续

5、数组传参和指针传参

在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计?
5.1一维数组传参
对于一维数组传参,我们通常用以下几种方式来接收

#include void test(int arr[3])//第一种,我们用数组传参,那我们可以用数组的形式来接收参数,写成arr[]的形式,方括号里的数组可以省略,但必须有方括号{}void test(int* arr)//第二种,数组名表示首元素地址,我们可以用指针来接收地址{}void test2(int* arr[10])//指针数组传参,指针数组接收,没毛病,方括号的10可省略{}void test2(int** arr)//二级指针来接收一级指针的地址,没毛病{}int main(){//arr是整形数组,具有10个元素,每个元素类型为intint arr[10] = { 0 };//arr2为指针数组,具有10个元素,每个元素类型为int*int* arr2[10] = { 0 };test(arr);//数组名表示首元素地址,这里传送的是arr[0](int类型)的地址test2(arr2);//数组名表示首元素地址,这里传的是arr2[0](int*类型)的地址}

5.2二维数组传参
对于二维数组传参,我们通常用以下几种方式来接收

#includevoid test(int arr[3][5])//二维数组传参,二维数组接收,没毛病{}void test(int arr[][5])//二维数组来接收,可以省略行,但绝不能省略列{}void test(int(*arr)[5])//arr表示首元素地址,二维数组首元素是整个第一行的地址,本质上是一个一维数组,用数组指针来接收整个一维数组的地址,没毛病{}int main(){int arr[3][5] = { 0 };test(arr);}

5.3一级指针传参
对于一级指针的传参与接收,有以下几种:

#includevoid test1(int* p){}int main(){int n = 10;int arr[10];int* ps = &n;int* ps1 = arr;//数组名表示首元素地址//可以被形参为一级指针接收的实参部分test1(&n);//&n取出n的地址传过去,指针变量接收地址test1(arr);//arr数组名表示首元素地址,p接受,没毛病test1(ps);//ps类型为int*,存放n的地址,p来接收,没毛病test1(ps1);//ps1存放arr首元素地址,p来接收,ok的return 0;}

5.4二级指针传参
对于二级指针的传参与接收,有以下几种:

#includevoid test1(int** p){}int main(){int n = 10;int* arr[10];int* ps = &n;int** ps1 = &ps;//数组名表示首元素地址//可以被形参为二级指针接收的实参部分test1(ps1);//二级指针传参,二级指针接收,没毛病test1(&ps);//ps类型为int*,&ps表示取出一级指针的地址,用二级指针接收,没得问题test1(arr);//arr表示指针数组首元素的地址,指针数组首元素类型为int*,取出int*的地址,用二级指针接收,没毛病return 0;}

以上为数组以及指针传参的内容,望诸君加油。

6、函数指针

6.1函数指针定义
在上面的内容中,我们讲到了&数组名表示取出整个数组的地址,数组名表示数组首元素的地址,那么,我们不妨大胆猜测,&函数名,以及函数名是不是表示函数的地址呢?带着这个疑问,我们来看以下代码:

#includeint add(int x,int y){return 0;}int main(){printf("add= %p\n", add);printf("&add = %p\n", &add);return 0;}


在这里,我们可以发现,原来&函数名以及函数名所表示出来的地址是相同的,这里由于函数并不像数组那样,具有多个元素,每个元素都有各自所对应的地址,所以,&函数名以及函数名都表示函数的地址,两者之间没有任何区别

因此,我们可以这样来定义函数指针:

#includeint add(int x,int y){return 0;}int main(){int(*p1)(int x, int y) = add;//这里p1先与*结合,说明p1是一个指针,再与后面的()结合,说明指针指向的是一个函数,int x,int y表示所指向的函数的参数为int类型//最外面的int 表示p1指向的函数的返回类型为int//这里可以把x、y省略,只要表示所指向的函数的参数类型即可int(*p2)(int, int)=add;//或者可以写为int(*p3)(int ,int) = &add;int(*p4)(int x, int y) = &add;return 0;}


6.2函数指针使用

#includeint test(int x, int y){return x + y;}int main(){int sum = 0;int sum1 = 0;int sum2 = 0;//这是我们一般情况下的写法,调用函数test,传送参数2,3,返回2+3的值sum = test(2, 3);//我们还可以用函数指针来实现int (*p)(int, int) = test;//p为函数指针变量,指向函数test,指向函数的参数为int,int,返回类型为intsum1 = (*p)(2, 3);//p存放的是test的地址,解引用指向函数test//其实,由于p存放的是test的地址,而test本身就表示函数test的地址,所以p == test//所以test(2,3) == p(2,3)//所以,实际上真正的写法是下面这种,写成(*p)(2,3)只是方便理解罢了sum2 = p(2,3);printf("%d\n", sum); //5printf("%d\n", sum1);//5printf("%d\n", sum2);//5return 0;}

6.3小练习

看如下代码

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

解释如下:

#includeint main(){//代码1(*(void (*)())0)();//这里可以先把(void(*)())先给去掉,此时就剩下(*0)();这表明调用0所指向的函数,但是0只是一个整形,怎么能调用函数呢,只有函数指针才可以调用函数//所以(void(*)()),最外面的()是强制类型转换,void(*)()是一个函数指针的格式//所以这个代码的意思就是把0,强制类型转换为一个函数指针类型,此时的0不再是一个整形,而是一个函数指针,然后调用0所指向的函数//代码2void (*signal(int, void(*)(int)))(int);//()的优先级>*,所以,signal是一个函数//(int,void(*)(int)) 表示该函数的参数,一个为int,一个为函数指针类型,该函数指针类型所指向的函数有一个参数,类型为int,返回类型为void//去掉以上这些,还剩下 void(*)(int),而signal是一个函数,这个就表示该函数的返回类型为void(*)(int),即函数指针类型//而该函数指针指向的函数有一个参数,为int类型,返回类型为void}

7、 函数指针数组

数组去掉数组名[ ]剩下的就表示数组的元素类型,就比如int arr[5],去掉arr[5],剩下的int就表示数组的元素类型。

7.1定义
函数指针数组,顾名思义就是一个数组,用来存放函数指针的数组,即该数组可以存放多个参数相同、返回类型相同的函数地址
如下:

#includeint add(int x, int y){return x + y;}int sub(int x, int y){return x - y;}int main(){int (*p1)(int ,int) = add;int (*p2)(int ,int) = sub;//p1、p2为函数指针,指向的函数的返回类型都是int,参数都为intint//add与sub的函数返回类型以及参数类型都相同//所以就可以这么来写int(*p3[2])(int, int) = { add,sub };int(*p4[2])(int, int) = { p1,p2 };//[]的优先级 > *,所以p3,p4都是一个数组,有两个元素,去掉数组名[]后剩下int(*)(int ,int),这表示的是函数指针类型(函数的地址)//所以,p3、p4数组的元素类型都是函数指针类型,或者可以说p3、p4数组的元素都表示函数的地址//而p3中,add、sub都是函数名,函数名表示函数地址,并且他们的返回类型以及函数参数都相同,所以都可以存放在数组p3中//p4中,p1、p2都是函数指针,并且所指向函数的返回类型及参数类型都相同,所以p1、p2都可以存放在数组p4中//上面的p3、p4都是函数指针数组,return 0;}

7.2函数指针数组的使用
有了函数指针数组,我们就可以通过数组的元素,来访问所对应的函数指针即函数地址了。举个例子:

#includeint add(int x, int y){return x + y;}int sub(int x, int y){return x - y;}int main(){int sum1 = 0;int sub1 = 0;int(*p[2])(int, int) = {add,sub};//p是一个函数指针数组,通过该数组的元素,实现访问函数地址目的sum1 = p[0](1, 2);//p[0]即add,本质即sum=add(1,2),返回1+2,即3sub1 = p[1](1, 2);//p[1]即sub,本质即sub=sub(1,2),返回1-2,即-1printf("%d\n", sum1);//3printf("%d\n", sub1);//-1return 0;}

了解这个后,我们可以通过函数指针数组实现一个具有加减乘除的简易计算器,如下:

#include//菜单打印void menu(){printf("--------------------------------------\n");printf("-------- 1、add2、sub ----------\n");printf("-------- 3、mul4、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;//函数指针数组,下标为1、2、3、4的元素分别对应+-*/int (*psArr[5])(int, int) = { 0,add,sub,mul,div };int ret = 0;do{menu();printf("请选择:->");//选择实现的功能scanf("%d", &input);if (input == 0){printf("退出\n");break;}//控制范围在有效范围内if (input >= 1 && input <= 4){printf("请输入操作数:->");scanf("%d %d", &x, &y);//通过psArr数组的元素,来调用对应的函数,把返回结果存放到ret中ret = psArr[input](x, y);//打印结果printf("%d\n", ret);}else{printf("输入错误\n");}} while (input);return 0;}

具体如下:

QQ录屏20221023011939

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

8.1定义
上面我们讲完了函数指针数组,而我们一开始所了解的整型数组,字符数组等,它们都可以将自己的地址存放到指针变量中,然后通过这个指针就可以访问它们,而作为函数指针数组,它也有自己的地址,我们也可以通过一个指针变量来存放它的地址,然后再通过指针来灵活使用它们。

那么它的形式是怎样的呢?且看下面:

#includeint main(){//函数指针int(*p1)(int, int);//p1先与*结合,说明p1是个指针,然后再与后面的()结合,说明p1指向的是一个函数,函数参数类型为int,int,返回类型int//函数指针数组int(*p2[3])(int, int);//p2先与[]结合,说明p2是一个数组,去掉数组名[],剩下int(*)(int,int)为数组的元素类型,为函数指针类型,所以p2是一个函数指针数组//指向函数指针数组的指针(用来存放函数指针数组地址的指针变量)int(*(*p3)[3])(int, int);//p3先与*结合,说明p3是一个指针,然后再与[]结合,说明p3指向一个数组,该数组的元素类型为int(*)(int,int),即函数指针类型//所以p3是一个指向函数指针数组的指针return 0;}

8.2指向函数指针数组的指针的使用
了解它的含义后,那么该如何使用呢?举个例子:

#includeint add(int x, int y){return x + y;}int sub(int x, int y){return x - y;}int main(){//p是一个函数指针数组,存放add、sub的地址int(*p[2])(int, int) = { add,sub };//也可以写成{&add,&sub}int(*(*p1)[2])(int, int) = &p;//p是数组名,&p表示取出整个数组的地址,即函数指针数组p的地址,存放在指针变量p1中int sum1 = 0;int sub1 = 0;sum1 = (*p1)[0](1, 2);//这里p1 == &p,所以*p1 == *&p,*与&相抵消,所以这段代码的本质即p[0](1,2),即add(1,2)sub1 = (*p1)[1](1, 2);//这里p1 == &p,所以*p1 == *&p,*与&相抵消,所以这段代码的本质即p[1](1,2),即sub(1,2)printf("%d\n", sum1);//3printf("%d\n", sub1);//-1return 0;}

8.3练习题
声明一个指向含有10个元素的数组的指针,其中每个元素是一个函数指针,该函数的返回值是int,参数是int*

int(*(*p)[10])(int*)//p先与*结合,说明p是一个指针,再与[10]结合,说明指向一个含有10个元素的数组。//去掉数组名和[]后剩下数组的元素类型int(*)(int*),即函数指针类型,所指向的函数返回类型int,参数为int*类型

9、回调函数

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

简单来说,就是把函数a的地址作为函数b的参数,然后在函数b的内部调用函数a时,这时的函数a就称为回调函数。

9.2回调函数的使用*
这里我们再举之前计算器的例子,对那段代码进行修改,如下:

#includevoid 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(*p)(int, int)){int x = 0;int y = 0;printf("请输入操作数:->");scanf("%d %d", &x, &y);//这里,被调用的add、sub、mul、div都是回调函数//通过函数指针调用的函数int ret = p(x, y);printf("%d\n", ret);}int main(){int input = 0;do{menu();printf("请选择:->");scanf("%d", &input);switch (input){case 1://函数add的地址作为函数calc的参数calc(add);break;case 2://函数sub的地址作为函数calc的参数calc(sub);break;case 3://函数mul的地址作为函数calc的参数calc(mul);break;case 4://函数div的地址作为函数calc的参数calc(div);break;case 0:printf("退出\n");break;default:printf("输入错误\n");break;}} while (input);return 0;}

详细图解

同理,被通过函数指针p调用sub、mul、div函数也都是回调函数,原理同上。

回调函数是函数指针内容里最为重要的部分,通过回调函数可以为我们节省很多麻烦事,就比如,程序员写了一个复杂的函数a,后面假如再想实现这个代码的作用时,只需要通过函数指针来调用a即可了,就不需要再次写a。

了解什么是回调函数后,接下来我们来讲一下qsort函数,以及它的模拟实现。

qsort 函数实现

到目前为止,我们学过了一种排序方法,即冒泡排序,如下:

#includevoid bubble_sort(int* arr, int sz){//趟数int t = 0;for (t = 0; t < sz - 1; t++){int i = 0;//每一趟的元素排序次数for (i = 0; i < sz - 1 - t; i++){//降序排序if (arr[i] < arr[i + 1]){int tmp = arr[i];arr[i] = arr[i + 1];arr[i + 1] = tmp;}}}}void print(int arr[], int sz){int i = 0;for (i = 0; i < sz; i++){printf("%d ", arr[i]);}}int main(){int arr[] = { 1,8,1,6,8,6,4,8,9,8,2,5,7,3 };int sz = sizeof(arr) / sizeof(arr[0]);bubble_sort(arr, sz);//核心思想:两两相邻元素进行比较//打印排序后的元素print(arr,sz);//9 8 8 8 8 7 6 6 5 4 3 2 1 1return 0;}

但是,冒泡排序有一个缺陷,就是冒泡排序只能排序整形,对于char类型,或者结构体类型是无法进行排序的,那么有没有一种函数是可以实现对任意类型元素的排序呢?答案是有的,就是qsort函数

在这里,我们发现,qsort函数有四个函数参数,这些函数参数都是什么意思呢?大家看以下讲解就明白了:

而对于用来比较两个元素大小的函数指针,又有以下定义:


这里我们知道,对于两个元素e1,e2,假如e1>e2,就返回一个大于0的数,如果e1<e2,就返回一个小于0的数,如果e1 == e2 ,就返回0

qsort是一个库函数,它的使用需要包含头文件#include
下面我们通过qsort函数再来排序上面的数:
1.qsort排序整形

#include#include//compar的两个参数都为const void*,返回类型为intint compar(const void* e1, const void* e2){return *(int*)e1 - *(int*)e2;//这里,假如e1>e2即(e1-e2)>0, 返回>0的数,如果 e1<e2即(e1-e2)<0 ,返回<0的数,如果e1 == e2即(e1-e2 == 0) ,返回0//这里我们直接返回(e1-e2)的值就可以了//由于e1,e2是void*类型,解引用后才能访问用来比较的两个元素,而我们知道void*类型的数不能直接解引用,所以要进行强制类型转换//强制类型转换成int*后,再解引用,就能进行运算了}int main(){int arr[] = { 1,8,1,6,8,6,4,8,9,8,2,5,7,3 };int sz = sizeof(arr) / sizeof(arr[0]);qsort(arr, sz, sizeof(arr[0]), compar);//arr表示首元素地址sz是要排序的元素个数 sizeof(arr[0])是要一个排序的元素大小//compar是函数名,函数名表示函数地址,即用来比较两个元素大小的函数地址return 0;}

这里我们发现,qsort实现了升序排序,如果要实现降序排序,只需要把e1与e2的位置调换一下就可以了。

2.qsort排序字符
与上面排序整形一个道理,例:

#include#include#includeint compar(const void* e1, const void* e2){return *(char*)e1 - *(char*)e2;}int main(){char arr[] = "jsdoqwidj";int len = strlen(arr);qsort(arr, len, sizeof(arr[0]), compar);return 0;}


3.qsort排序结构体
原理本质上都是一样的,例:

#include#include#include//qsort排序结构体以及用冒泡思想实现qsort的功能struct stu{char name[10];int age;};//结构体成员排序int cmp_struct_name(const void* e1, const void* e2){//strcmp比较字符串大小的库函数,后面会讲到//强制类型转换,再指向结构体成员namereturn strcmp(((struct stu*)e1)->name, ((struct stu*)e2)->name);}//结构体成员排序int cmp_struct_age(const void* e1, const void* e2){//强制类型转换,再指向结构体成员agereturn ((struct stu*)e1)->age - ((struct stu*)e2)->age;}int main(){struct stu s[] = { {"zhangsan",20},{"lisi",50},{"wangwu",8} };int sz = sizeof(s) / sizeof(s[0]);//qsort函数实现结构体排序//按姓名排序qsort(s, sz, sizeof(s[0]), cmp_struct_name);//按年龄排序qsort(s, sz, sizeof(s[0]), cmp_struct_age);return 0;}

在这里我们要知道strcmp函数:

strcmp也是一个库函数,它的使用要包含头文件#include,返回类型为int,两个参数为const char*

对于两个字符串如果str1<str2即(str1-str2)<0,返回一个str2,即(str1-str2)>0,返回>0的数。刚好与qsort的返回值相同
strcmp是专门用来比较字符串大小的库函数,在以上例子中就用到了此函数。

qsort函数模拟(冒泡思想)

清楚qsort函数的使用后,我们用我们学过的冒泡思想来模拟实现qsort的功能

#include#include#include//qsort排序结构体以及用冒泡思想实现qsort的功能struct stu{char name[10];int age;};//结构体成员排序(姓名)int cmp_struct_name(const void* e1, const void* e2){//strcmp比较字符串大小//强制类型转换,再指向结构体成员return strcmp(((struct stu*)e1)->name, ((struct stu*)e2)->name);}//结构体成员排序(年龄)int cmp_struct_age(const void* e1, const void* e2){//强制类型转换,再指向结构体成员return ((struct stu*)e1)->age - ((struct stu*)e2)->age;}//交换void Swap(char* buf1, char* buf2,int width){//如何实现对任意类型的元素实现交换,答案很简单//交换每一个字节从而实现任意类型数据的交换//就比如int类型,占用四个字节,把每一个字节都进行交换,就实现了整个元素的交换,而width其实是元素的大小,也就是该元素占几个字节,即交换的次数//举个例子,假如传过来的是int类型的4和5的地址,但是这里使用char*来接收的,也就是说只能接收4和5的第一个字节的地址,解引用只能访问到4和5的第一个字节的内容// 即进行交换的话也只能交换4和5的第一个字节的内容,但是4和5占用4个字节,不过有了width后,然后进行解引用,交换width次,就可以实现对4和5的四个字节的每一个字节所对应的内容进行交换,从而实现4和5的整体交换//换成short类型等也是一样int i = 0;for (i = 0; i < width; i++){char tmp = *buf1;*buf1 = *buf2;*buf2 = tmp;buf1++;buf2++;}}//冒泡排序思想实现qsort功能void bubble_sort(void* base, int sz, int width, int (*cmp)(const void*, const void*)){//趟数int t = 0;for (t = 0; t < sz - 1; t++){//排序元素对数int j = 0;for (j = 0; j < sz - 1 - t; j++){//上面还是保持冒泡排序的思想,只不过下面进行了改变// //(char*)base+j*width:找到比较的元素,char*每次跳过一个字节,而j*width,就保证了不管是什么类型的元素,都能j+1后都能访问到后面想要访问的元素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(){struct stu s[] = { {"zhangsan",20},{"lisi",50},{"wangwu",8} };int sz = sizeof(s) / sizeof(s[0]);//冒泡思想实现qsort函数功能,从而实现排序结构体类型//这里我们的冒泡排序模拟实现qsort,保证与qsort的参数一致//首元素地址、待排序元素个数、元素大小、用来比较元素的函数地址(按姓名排序)bubble_sort(s, sz, sizeof(s[0]), cmp_struct_name);//首元素地址、待排序元素个数、元素大小、用来比较元素的函数地址(按年龄排序)bubble_sort(s, sz, sizeof(s[0]), cmp_struct_age);return 0;}



在这里,最难的地方主要在于如何实现对任意类型的元素进行两两排序。这里再来画图解释一下为何要这么书写
(char*)base + j * width, (char*)base + (j + 1) * width

最后,指针是C语言的精髓所在,望诸君不负努力,加油!
end