C语言———指针进阶3

目录

6、函数指针数组:

7、指向函数指针数组的指针:

8.、回调函数 :


6、函数指针数组:

函数指针数组是存放函数指针的数组,数组中每个元素的类型都是函数指针类型。

因为,Add是加法函数,,Sub是减法函数,,两者的参数的个数及类型以及函数返回类型是一样的,,所以对于两个函数指针变量的类型,即函数指针类型就是一样

的,,那么既然类型一样,,能不能把它们存放在同一个数组里面呢?

 pf是共同的,,不要动,,此时,因为[ ]的优先级高于*,,所以,pfArr先与[2]组成一个数组,,数组里面有2个元素,因为这是一个数组,把数组名和[2]拿下来,即,

我们把 pfArr[2] 拿下来,剩余的就是 int(*)(int,int) ,,这就是我们数组每个元素的类型,对于,数组来说,如果 int arr [10];这是一个整型数组,,如果把 数组名

arr和[ 10] 去掉得到:int,这就是我们数组arr每个元素的类型,,这样我们就得到了所谓的,函数指针数组,,是数组,我们就可以对他初始化,得:

 函数指针数组里面就可以存放多个,同类型的函数指针;

我们这里可以初始化为Add,,Sub,,这两个都是函数名, 而函数名就代表函数的地址,,对于Add来说,,,就是把Add函数的地址放在了函数指针变量pf1中,,对

于Sub来说,,,就是把Sub函数的地址放在了函数指针变量pf2中,,这里的pf1===Add,,Sub===pf2,,所以初始化的时候,{ }里面也可以放 pf1,,pf2; 这就相

当于是一个 一维数组;

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

空指针是不可以使用的,即使你初始化为空指针NULL,也不能去使用该空指针。

虽然这个代码已经实现了我们的要求,,但是,过程太啰嗦,,功能不全,,如果我还想在此基础上再实现按位与,按位或,按位异或,左移,右移等等功能的话依然

涉及到两个整数进行运算,,所以就要写出更多的,这种调用函数,,同时下面的switch里面也要对应的增加不少东西,,代码就会越来越长,所以说,这个代码不好,

要进行改正,这时就可以使用函数指针数组进行优化:现在,,我们知道,,如果调用一个函数的话,,不一定必须使用函数名进行调用,也可以使用指针调用一个函

数,比如:

前两种方法都是通过指针来调用函数的,只不过在函数指针类型中,*号不起实际作用,所以,第二种方法就是第一种方法的改善,而第三种方法就是通过函数名来调用

函数;

所以,,在该模拟实现计算器的功能的时候,,我可以不通过每一个都使用函数名来调用函数,,我们前面写的程序,都是直接调用对应功能的函数名来实现功能

的,,所以,如果要用指针,该怎么做呢?

如果想要调用Add函数,那么我就把Add的地址取出来,即&Add或者 Add,,因为只是一个函数的地址,,所以要放在一个函数指针变量中,然后对该函数指针变量解

引用就可以得到对应的函数,然后进行传参,就可以直接使用该函数了,,我们又发现,加减乘除对应的函数,参数部分的类型,个数和该函数的返回类型都是一样

才可以放在同一个函数指针数组里面,所以,我们可以把这四个函数指针放在一个函数指针数组里面去,以Add为例:

int (*pf)(int,int)=Add;

我们有四个函数,,所以可以得到:

int (* pfArr[4])(int,int)={Add,Sub,Mul,Div};

这就是一个函数指针数组的书写形式, 因为[ ]的优先级高于*,,所以,pfArr会先和[4]组成一个数组,我们知道,如果是:

int (*pf)(int,int)=Add;他就是一个函数指针,,本质上是一个指针,,(*pf)说明pf是一个指针变量,,然后往后走,发现(),代表是一个函数指针变量,函

数的参数类型都是int整型,,函数的返回类型是int整型,对于一个指针变量来说的话,,去掉指针变量的名称,剩下的就是指针变量的类型;比如,int* pa=&a; 此处的

pa就是一个整形指针变量,,去掉pa剩下的 int*,就是我这个整形指针变量的类型;

而对于我们的:int (* pfArr[4])(int,int)={Add,Sub,Mul,Div};来说,,pfArr会先和[4]组成一个数组,说明是一个数组,,数组元素就是4个,,这就是一个

数指针数组,,本质上是一个数组,,对于数组来说的话,,去掉数组名pfArr和方块[4],剩下的int(*)(int,int)就是我这个数组里面4个元素,每个元素的类型,,

发现每个元素的类型都是函数指针类型,就是函数指针类型,,所以,这就是一个函数指针数组

因为在过程中,输入1就代表调用函数Add,,,输入4就代表调用函数Div,,但是如果函数指针数组写成:int (* pfArr[4])(int,int)={Add,Sub,Mul,Div};那

么,,下标为0就会调用函数Add,,为了使得下标能够与我们输入的值对应起来,即,输入1,调用Add函数,,但是Add函数的下标为0,,如果想让两者对应起来的

话,,我们可以在Add函数前面再放一个元素,Add函数的下标就会自动往后推一个,,从而使得下标与输入的值对应起来,所以,元素个数就应该是5个,int (*

pfArr[5] )(int,int)={0,Add,Sub,Mul,Div};,初始化按照自己总结的应该是补一个0上去即可,但是,我们发现元素的类型是一个函数指针类型,按理说,上面

的函数指针数组应该写成:int (* pfArr[5] )(int,int)={0,pf1,pf2,pf3,pf4};这样,拿掉pfArr[5]

剩下的就是每个元素的类型,,所以,pf1-4,他们的类型都是int(*)(int,int),,这样才可以讲得通,,所以这里的0本质上应该和pf1是一样的类型,,而pf他们都是函数指针变量,所以,0也应该是一个指针变量,,0对应的指针就是空指针NULL,,所以应该写成:

int (* pfArr[5] )(int,int)={NULL,pf1,pf2,pf3,pf4};

如果对下标进行访问,比如pfArr[1],他就应该得到的是pf1,,而pf1里面存放的是Add函数的地址,然后对pf1解引用就可以得到函数Add,然后传参,调用Add函数,即

(*pf1)(3,5),,优先级的原因,所以加上括号,,这样就可以得到Add函数了,,但是我们知道,在函数指针变量中,*不起到实际作用,,所以,(*pf1)(3,5)

===(pf1)(3,5)===pf1(3,5),,而Add===pf1,所以pf1(3,5)===Add(3,5);所以,按理说,int (* pfArr[5] )(int,int)={NULL,pf1,pf2,pf3,pf4};中

{ }里面存放的应该是函数的地址,但是,由于从函数的地址到函数本身,不需要进行解引用,就可以直接把{ }里的内容写成函数,,所以再进行下标访问的时候,出来

的就是函数本身;但是由于,pf1===Add,等等,,所以改写成了:int (* pfArr[5] )(int,int)={NULL,Add,Sub,Mul,Div};,,所以这里的空指针的下标就是

0.

在这里,通过下标访问的时候,本质上应该是访问到pf,即访问到的是函数的地址,,但是从函数的地址到函数本身,不需要进行解引用,所以,就可以理解为,下标

访问到的就是某个函数本身,,所以,pfArr[input] 这个整体就是数组访问下标,得到的就是函数的本身,,然后后面的()就是函数的参数,前面的括号可以不写,

即:pfArr[input]() ,不写的话,程序一样会正常运行,加上括号只是看起来更加明确;

 7、指向函数指针数组的指针:

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

函数指针数组,本质上是一个数组,,是数组就可以取出数组的地址。

如果想得到一个指向函数指针数组的指针,,即数组指针,就要把数组的地址拿出来,就是&数组名,只有数组名代表数组首元素的地址,&数组名,这里的的数组名是

二个特例中的其一,,所以,这里的数组名表示整个数组,&数组名,就拿到了整个数组的地址,然后放在一个数组指针变量中,类型仿照之前的写即可;

例如:

int (* p2 [4])(int ,int); 这是一个函数指针数组,现在把这个数组的地址取出来,就是&p2,因为p2是数组名,,数组名代表数组首元素的地址,但是&数组名,这里的

数组名就是代表整个数组,所以,&p2就拿到了这个函数指针数组的地址,然后放到一个指针变量p3里面,因为p3是一个指针,所以*p3代表这就是一个指针,但是[ ]的

优先级高于*,,所以要写成(*p3),然后往后走找到[ ],说明这是一个数组指针,即指向数组的指针,他指向的数组即函数指针数组,元素个数是4个,所以写成[4],

然后每个元素的类型就是:int(*)(int,int)因为,对于一个数组来说,,去掉数组名和[ ],剩下的就是该数组里面每个元素的类型,所以应该是:int(*)(int,int) (*p3) [

4]=&p2;但是不允许这样写,要把 (*p3) [4]放在int(*)(int,int)里面,得到:int(* (*p3) [4])(int,int)=&p2;看到(*p3),先解析()里面的内容,,*告诉我p3是一个指针,

往右看有[4],说明该指针指向的那个数组有4个元素,如果要看数组里面每个元素的类型,,就去掉(*p3) [4]看到每个元素的类型就是int(*)(int,int),所以这就是一个数

组指针;应该是这样进行考虑的,,不要考虑成下面这种情况,如果看成下面这种情况,应该就是一个 函数指针数组了, 而我们想得到的是一个指针

现在(*p3)前面是*,后面是[4],由于[ ]的优先级高于*,,所以,(*p3)先与[4]结合,代表一个数组,这个数组里面有4个元素,去掉(*p3) [4] ,剩下的就是数组里面

每个元素的类型,即函数指针类型,, 这样分析是错误的,不可以把(*p3)看做一个数组名。

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

对于数组来说:

1.去掉数组名和[ ],剩下的部分就是数组里面每个元素的类型,即数组元素的类型;

2.只去掉数组名,剩下的部分就是该数组的类型,,即数组类型;

例如: int arr[10];

去掉数组名arr和[10],剩下的就是int,,这就是数组里面每个元素的类型,即数组元素类型,如果只去掉数组名arr,即剩下的是 int [10],这就是该数组的类型,即数组

类型;

8.、回调函数 :

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个 函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调
函数。回调函数不是由该函数 的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进 行响应。
就是指,有一个A函数,但是不直接调用,把A函数的地址传给一个B函数,,那么B函数的形参部分就会接收传过来的A函数的地址,又因为地址是放在一个指针中的,
所以B函数的形参部分里面会有A函数的指针,在B函数里面,通过A函数的指针来调用A函数,这就称为回调函数机制,即通过函数的指针再调用函数;函数递归是自己
调用自己,,这里和函数递归不一样

我们知道,红框里面的都是重复的,比较冗余,,那我想,能不能把把蓝色框这个整体实现成一个函数呢,,每次使用就去调这个函数不就行了吗,只需要把冗余的部

分在函数里面实现一次,然后我们每次使用,就可以直接调取这个函数不就可以了吗,但是,我发现:

Add和Sub这里是不一样的,而且还不能调换Add和Sub在自己蓝框里面的相对位置,,只能这个顺序,,如果我把第一个蓝框看成函数A,,那么第二个蓝框不可以看成

函数A,因为,里面不一样,,只能写成B函数,同样,下面的乘除只能设为C和D函数,,这样的话,,不也一样是冗余吗,只不过把冗余的部分转移到了ABCD函数里

面,,,我们只写一份代码,,设为 Calc()函数,让这个函数可以实现加减乘除,我们现在可以把蓝框里面不同的地方通过函数的参数传递过去,就可以解决这个问

题了,,因为不一样的是函数,即Add,,Sub,,Mul,,Div,,我可以把这四个不一样的东西以函数参数的形式传过去,把四个函数名当做参数传过去就行,把函数

名Add,Sub,Mul,Div,函数名当做参数传过去,,因为,函数名===&函数名,,代表的都是该函数的地址,,如果把地址传过去,参数部分就要用函数指针来接

收,即形参部分使用: 

因为,x和y之前都是在main函数里面创建的,,现在我把关于x和y的计算都放在了Cala函数里面,,不在main函数里面了,,所以,就可以把x和y的定义放在Cala函数

里面定义即可,我们形参部分中的pf放的就是我们函数的地址,按理说,如果解引用就可以找到这个函数了,,然后再传参,,即*pf(x,y),但是优先级的问题,,

我们必须先让pf解引用,,即(*pf)(x,y),,这就得到了函数本身,,然后再传参即可,,但是我们知道,对于函数来说的,,*不起到实际作用,,所以可以简写

成: pf(x,y)这样就可以得到函数,并进行传参了,直接把得到的结果return返回就可以了。

 即:

现在来看就是,在main函数里面,创建了Add,Sub,Mul,Div这些函数,,但是不直接调用这些函数,而是把这些函数的地址传给Cala函数,那么Cala函数里面就要

会接收传过来的函数的地址,因为地址是在一个指针中的,,所以Cala函数形参部分就要使用函数指针来接收,,,然后在Cala函数里面,通过这些函数的指针来调用

这些函数,,这就是回调函数机制;

这就是把重复的代码写到一个函数Cala里面,每次使用就调用该函数Cala,减少冗余.

首先演示一下qsort函数的使用:

#include <stdio.h>

//qosrt函数的使用者得实现一个比较函数

int int_cmp(const void * p1, const void * p2) 
{
  return (*( int *)p1 - *(int *) p2);
}
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;
 }

用这种方法来写sizeof的大小也是可以的,但是不具有通用性,,当排序其他类型的时候就不满足了,,所以直接写成:sizeof(arr[0]),这样的话,不管你数组里面每个

元素的类型是什么,都会自动计算出该数组中一个元素所占内存空间的大小了。

冒泡排序的思想:

但是这种方法只适用于对int整型数据进行排序,对于其他类型的数据不再适用,,所以,该方法具有一定的局限性:

假设为升序排列:

让相邻的两个元素比较,从首元素开始,首元素与第二个元素比较,如果前面大后面小,就调换位置,如果前小后大或者前后相等,则不动,,即当前一个元素等于后

一个元素的时候,换不换位置都是可以的,再去比较第二个和第三个元素,直到把所有的元素都访问一遍,这就是一趟冒泡排序,一趟冒泡排序就把数组里面的一个数

字放到了他的最终位置上面,如果是升序,那么一趟冒泡排序就会把最大的数字放在了最后面,因为是升序,一趟冒泡排序后,就会把最大的那个数放在了最后的位置

上,现在再去处理除了最大的那个数字以外其他的数字,这个最大的数的位置就不会发生改变了,因为他是升序,他已经是最大的数,一定会放在最后,位置不会再发

生改变。

第二趟冒泡排序的时候,就会把除了最大元素外的其他元素的最大的数字放在最后面的位置上,一趟冒泡排序就会解决这一组数字中的一个数字,

比如: 4 1 3 2 5

第一趟冒泡排序:

1 4 3 2 5

1 3 4 2 5

1 3 2 4 5 其中4和5本来就是前小后大,不需要进行调换,所以,一趟冒泡排序就结束了,,这样就把这一组中的最大值放在了最后面,现在要进行第二趟冒泡排序,要

把刚刚的最大值以外的数字进行排序,,最大值位置确定就不需要再变位了,所以:

第二趟冒泡排序:

1 3 2 4

1 3 2 4

1 2 3 4 这样一趟冒泡排序后就会把除了5之外的数字中的最大值4放在了最后面的位置,然后多几次冒泡排序,就可以实现说有的数升序排列了;

冒泡排序一趟就会让一个数字出现在他的最终的位置上,所以说,我的数组里面有n个元素,就会进行(n - 1)趟冒泡排序,最终达到升序;比如,我的数组里面有10个

元素,要把这10个元素进行升序排列,,一趟冒泡排序就会把一个元素放在它的最终位置上,那么10个元素就需要9趟冒泡排序,从而把10个元素都放在他们的最终位

置上,从而完成升序排列,因为要进行调位,10个元素只需要调动9次就可以完成升序了,因为,我们有10个元素,当进行了9趟后,就把9个元素已经放到了该元素的

最终位置上,一共10个数字,10个位置,其中9个元素已经在自己的位置上面了,所以说,剩下的哪一个元素,不需要再进行一趟冒泡排序,他自己本身就在他的位置上

了,所以,如果数组里面有n个元素,想进行冒泡排序的话,只需要n-1趟冒泡排序就可以把每个元素放到该元素对应的位置上面了;

而对于每一趟冒泡排序内部来说,,如果10个数字,要进行9对比较然后换位,就可以把所有的数字过一遍了,我们第一趟冒泡后,就把最大值放到了最后面的位置上

面,他的位置就不需要变动了,所以,当第二趟冒泡的时候,我可以不管那个最大值,只需要对他以外的其他元素进行比较换位就可以了,这样第二趟冒泡就相当于对9

个数字,即做8对比较然后换位就可以把所有的数字过一遍了,所以,j循环的时候,让每一趟冒泡里面 j<sz-1-i ,就可以了。

所以,要进行几次冒泡排序,就和我的元素个数有关,所以传参给函数的时候,除了要把数组传过去,还要把元素个数传过去;

如果两个数相等的话,也不需要进行换位;

这就是我们自己实现的排序公式,那么函数库里是否有库函数可以帮助我们来实现快速排序呢?

所以就用到了qsort(),快速排序的函数,,要注意的是,,库函数qsort()函数,可以排序任何类型的数据,比如,整型数据,字符串数据,结构体数据等,,都可

以进行排序,不管什么类型都可以用qsort函数来进行排序;

但是我们刚刚写的代码,由于调用函数参数部分为 int arr[ ],所以,只能用来排序整型,其他类型不可以进行排序,,如果想排序其他类型,,,只能改变代码才可以;

qsort()函数的使用:

qsort函数是一个库函数,即快速排序,,

该函数有4个参数,其中,第4个参数是一个函数指针,,(*compar)代表的是,变量compar是一个指针,往后走有个(),代表的是compar是一个函数指针,,该函

数指针指向的那个函数的有两个参数,两个参数的类型都是 const void*,该函数的返回类型是int整型;

其中,4个参数中,

第一个参数,base里面放的就是待排序的数据中,首元素或者是首对象的地址,因为,实参部分的第一个参数是一个地址,即形参中base里面存放的是一个地址,传参

的时候,形参要接受这个地址,就要用指针来接收,所以, void*base,*就说明是一个指针变量,前面的void默认为无具体类型,根据要排序的数据是什么类型来写,

因为库函数不需要再对函数进行实现,所以说前面的void具体被写成什么是系统自己进行处理的;

但是,如果我们要模拟实现qsort函数的时候,就要把这个函数的形参部分写出来了,这个时候,形参是我们自己设定的,而不是系统自动改的,,因为我们可以比较各

种各样类型的值,,所以形参第一个参数我可以写成 void* 不变,,然后具体应该是什么类型直接到函数内部进行强制类型转换就可以了,就可以避免每次比较不同类型

的值都需要把形参部分改掉的缺陷了;

第二个参数,num就是指排序数据元素的个数,所以实参部分那就是排序数据元素的个数,,而size_t===unsigned int,,无符号整型,它本质上是一个整型,所以形参

用整型来接收,,又因为,数据元素的个数不可能是一个负值,,所以用size_t来用与形参里面,因为库函数不需要再对函数进行实现,所以在形参的时候,系统会写成

size_t的类型,但是我们不需要管,只需要把实参写出来就行,形参交给系统;但是我们如果自己要模拟实现qsort函数的时候,就要把这个函数的形参部分写出来了,

这个时候,形参是我们自己设定的,而不是系统自动改的,,因为num指的就是待排数据元素的个数,个数用整型来写,按理说,个数不可能为负值,所以写成size_t的

形式,但是如果是整型的话,,我们习惯于写成int形式,,所以这里,,直接写成int就可以了,,我们也不会输入一个负值去当做元素的个数;

第三个参数,size,我们知道,qsort可以排序任何类型,当把待排序数据的地址传过去的时候,系统不知道你传过来的数据是什么类型,他不知道访问几个字节才可以

找到下一个元素,所以,这个时候你要告诉它,你的一个元素占几个字节,比如是一个整型int,你就要告诉他占4个字节,本质上就是在告诉他,你的数据到底是什么类

型的,所以,实参中的size就是代表一个数据所占的空间大小,就是在告诉系统你的数据是什么类型的,,而所占空间的大小肯定是一个整数int类型,而且不可能是一

个负数,所以形参部分用size_t来接收,即无符号整型,即size就是待排序数据中,一个元素的所占空间的大小,单位是字节;因为库函数不需要再对函数进行实现,所

以在形参的时候,系统会写成size_t的类型,但是我们不需要管,只需要把实参写出来就行,形参交给系统;但是我们如果自己要模拟实现qsort函数的时候,就要把这

个函数的形参部分写出来了,这个时候,形参是我们自己设定的,而不是系统自动改的,,因为size指的就是待排数据元素的每个元素所占空间的大小,大小用整型来

写,按理说,大小不可能为负值,所以写成size_t的形式,但是如果是整型的话,,我们习惯于写成int形式,,所以这里,,直接写成int就可以了,我们知道,一个元素

所占空间的大小不可能为一个负值,,所以,传给形参的时候,一定是一个正数,直接用int整型就可以,没必须规定太严格,毕竟,我们传过去元素所占的空间大小不

可能是一个负值,因为前面写成了 void*base,,所以我不可以通过base+1来找下一个元素,,void*是无具体类型的指针,它+1,不知道会访问几个字节,void*是一种

无具体类型的指针,以void*为类型的指针变量可以存放任意类型的地址,void*类型的指针变量不可以直接进行解引用和 ++或-- 的操作,只有把他强制类型转换成某一

个具体的指针类型之后才可以再进行解引用。

第四个参数,我们写的对代码对整数数字进行排序,,其中,整型大小可以用,=号进行直接比较,,但是如果我们自己写代码相比较两个字符串的的大小时候,就不可

以再直接使用大小号进行比较了,字符串比较大小要用strcmp来比较字符串的大小,,第四个参数我们要写一个函数指针,该函数指针指向的那个函数的返回类型一定

是一个int类型,即>0,<0,=0的一个整数,该函数有两个参数,这两个参数就是放的我们要比较的两个数据的地址,由于不知道什么类型的数据,直接写成无具体类型

的指针,需要什么类型的数据在函数里面直接强制转换就可以了,所以,把参数的类型都写成无具体类型的指针,,这样就避免比较不同类型的数据要每次都改变参数

的类型的缺陷;

假设指针变量为p,,因为const在*左边,所以他固定的是*p中p所指向的那个内容不发生改变,,只进行比较,不交换,所以指向的内容不发生改变,用const修饰,交

换交给qsort系统内部去处理,,我们这里只负责比较两者的大小,知道这两个数据是不是需要进行交换,如果对于qsort库函数来说,,具体交换的过程不需要了解,只

需要知道他这里是否需要交换就可以了,,但是如果我们去模拟实现qsort函数,我们不仅要比较两个数据,如果需要交换的话,还应该写个函数,专门用于两个数值的

交换;

如果我们自己写代码对整型数据进行排列的时候,,框里面的是不变的,,因为需要几趟冒泡排序,每一趟冒泡排序需要对比几对,这都是固定的,,唯一不一样的就

是两个数据之间的比较方法不一样,,

两个元素的比较方法和交换方法可能是不一样的,要发生变化,

所以,qsort函数的第四个参数就是用来:比较待排序数据中的两个元素的一个函数,因为compar是一个函数指针变量,他里面存放的是一个函数的地址,compar指向

的那个函数就是用来比较待排序数据中两个数据大小的;等该函数比较完之后会返回一个int整型,

如果返回值>0,则第一个元素大于第二个元素 ;

如果返回值=0,则第一个元素等于第二个元素;

如果返回值<0,则第一个元素小于第二个元素;

首先(*cmp)是一个指针,往后走有(),说明是一个函数指针,该指针指向的那个函数有两个参数,参数类型都是 const void *类型,并且,该函数的返回类型是int整

型,,我们就看该函数的参数,,,我们只写出来它的类型就可以了,,不需要写上名字,,如果我们写上名字,,e1,e2,,观察知道,const void*,只是一个指针

类型,,所以,e1,e2要放要进行比较的两个元素的地址,类型根据具体情况而定,void*是一个无具体类型的指针,,因为我们要比较的两个元素,不一定是什么类

型,,所以,设计成void*,当传什么类型的元素都可以进行比较,,,对于第四个元素的形参,我们也不用考虑,因为,这是一个库函数,我们不需要管它的形参,,

我们看到形参是一个函数指针,,说明我们实参第四个参数应该是一个地址,而形参部分又是函数指针,,所以第四个参数应该是一个函数的地址,对于函数的地址,

我们可以写成&函数名,或者函数名,这两种情况,,都代表地址,然后形参中,compar是一个函数指针变量,(*compar)代表这是一个指针,往后看到(),说明是

一个函数指针,,该指针指向的那个函数里面有几个元素,每个元素的类型是什么,返回类型是什么,要根据这个函数进行观察,,

我们知道, int (*p)(int,int)是根据 int Add(int x,int y);写出来的,,我们虽然不写qsort函数的实现过程,但是我要把cmp_int的函数实现出来,,由于qsort的

形参是 int (*compar)(const void*,const void*),他又是根据函数写出来的,所以,函数的参数应该也有两个,每个类型都是 const void* ,,并且函数的返回类型

是int整型,,这样才可以与qsort函数的形参对应起来,,那么我设函数名就是 cmp_int,,,该函数的实现应该说就是:

就比如 int Add(int x, int y),,一样,这是函数的定义,要写出来x和y,即写出变量名字,,这里我可以给他们的名字写成e1,e2,就可以了;

现在就是函数功能的实现,qsort函数的第四个参数就是用来比较两个数据的大小,,但是不同类型的数据要进行比较大小则需要使用不同的比较方法和换位方法,,第

四个参数把用何种方式交给了用户,,因为用户知道,他自己要用什么方法去比较他输入进去的数据,

就比如,我们要比较的是两个整型的大小,,我们知道,在cmp_int函数里面,参数部分放的是两个整型的地址,即e1里面放的就是我们要比较的两个数字,第一个数字

的地址,即e2里面放的就是我们要比较的两个数字,第二个数字的地址,,,我们如果想得到这两个数,对e1和e2解引用不就行了?

答案是不行的,,因为根据qsort第四个参数的形参得出来我们函数的形参都是const void* 类型的,这是不可以变的,也就是说,我们把要比较的两个整型的地址分别放

在了两个 void*的指针中,,我对e1,e2解引用,,系统不知道要访问4个字节,,我们平时写的 int* pa=arr;这里pa是一个整型指针,他是int*的,,*pa就是系统知道

访问4byte,,现在void*是一个无具体类型的指针,,对e1,e2解引用,,系统不知道访问几个字节,正因为我们这里的e1,e2的指针类型是void*,不是int*,,,而我

们又知道,e1,e2里面本来就是要比较的两个整型数据的地址,,所以就强制类型转换,,正好也匹配,不会出错,,得: *(int*)e1, *(int*)e2

这就代表先对e1,e2强制类型转换成int*类型的指针,再解引用就可以访问4byte,,访问的e1,e2对应的那两个整型数据,对于强制类型转换,这里不会涉及到优先级

的问题,,可以不加括号,

这样就可以与我们前面的:

qsort库函数在排序的时候,默认的是 升序 ,即,前大于后则交换,前小于等于后,则不交换。

若第一个元素大于第二个元素,返回值>0,则需要交换;

若第一个元素等于第二个元素,返回值=0,则不需要交换;

若第一个元素小于第二个元素,返回值

对应起来了;

因为qsort函数在排序的时候,默认是升序排列, 对于升序来说的话,只有前大于后的时候才进行交换,前小于等于后的时候,不需要进行交换,再看qsort函数的第四个

参数的返回值,对于数据来说,如果前大于后,则返回值>0,,此时要进行交换,前小于等于后,则返回值0,系统会默认此时的第一个元素大于第二个元素,则进行交

换,后两者同理,所以,对于降序来说,只需要保证在使用该函数的时候,返回值>0就进行交换,其他情况不交换,又知道,当降序的时候,只有前小于后的时候,才

进行交换,要想得到在前者小于后者时能够返回>0的数,从而进行交换的话,就返回:return *(int*)e2-*(int*)e1,,即可满足要求。

 int compare(const void *a , const void *b )

{

return *(int *)a - *(int *)b; //升序排序

//return *(int *)b - *(int *)a; //降序排序

}

下面的第一个参数和第二个参数均指的是compar函数形参部分中的参数。

升序排列时,

若第一个参数指针指向的“值”大于第二个参数指针指向的“值”,则返回正,则系统就会交换两个数值的位置;

若第一个参数指针指向的“值”等于第二个参数指针指向的“值”,则返回零,系统不交换两个数值的位置;

若第一个参数指针指向的“值”小于第二个参数指针指向的“值”,则返回负,系统不交换两个数值的位置;

降序排列时,则刚好相反。

若第一个参数指针指向的“值”小于第二个参数指针指向的“值”,则返回正,则系统就会交换两个数值的位置;

若第一个参数指针指向的“值”等于第二个参数指针指向的“值”,则返回零,系统不交换两个数值的位置;

若第一个参数指针指向的“值”大于第二个参数指针指向的“值”,则返回负,系统不交换两个数值的位置;

因为我们是一组一组比较的,只看两个元素就可以了,

return *(int *)a - *(int *)b; //升序排序

假设第一个参数指针指向的元素为3,第二个参数指针指向的元素元素为4,,,3-4=-1,,小于0,所以第一个参数指针指向的元素元素的小于第二个参数指针指向的元

素元素,,则不需要换位,

假设第一个参数指针指向的元素元素为3,第二个参数指针指向的元素元素为3,,,3-3=0,,等于0,所以第一个参数指针指向的元素元素的等于第二个参数指针指向

的元素元素,,则不需要换位,

假设第一个参数指针指向的元素元素为3,第二个参数指针指向的元素元素为2,,,3-2=1,,大于0,所以第一个参数指针指向的元素元素的大于第二个参数指针指向

的元素元素,,则需要换位,先2,后3,

这就是升序排列,反过来就是降序排列;

//return *(int *)b - *(int *)a; //降序排序

假设第一个参数指针指向的元素元素为3,第二个参数指针指向的元素元素为4,,,4-3=1,,大于0,所以第一个参数指针指向的元素元素的小于第二个参数指针指向

的元素元素,,则需要换位,

假设第一个参数指针指向的元素为3,第二个参数指针指向的元素为3,,,3-3=0,,等于0,所以第一个参数指针指向的元素的等于第二个参数指针指向的元素,,则

不需要换位,

假设第一个参数指针指向的元素为3,第二个参数指针指向的元素为2,,,2-3=-1,,小于0,所以第一个参数指针指向的元素的大于第二个参数指针指向的元素,,则

不需要换位;

qsort函数的作者,不知道你要排序那些数据,所以要把这些数据传过去,就传他们的首元素的地址就可以,但是作者不知道你传过来的地址是什么类型的地址,所以他

在形参中第一个参数的类型设置为void*,无具体类型的指针,

作者也不知道,你要排序的数据有多少个元素,所以要把元素的个数传过去,告诉作者,

作者不知道你要排序的数据是什么类型的,,他不知道要访问几个字节才能找到一个元素,,所以要把一个元素所占空间的大小传过去,让作者知道一次访问多少个字

节,,即访问多少个字节可以找到一个元素;

进行排序就必然要进行两个元素的比较和换位,,但是不同类型的数据元素进行比较和换位的方法是不一样的,,这要用户自己设定才可以;

上面讲的例子是用来排序整型数据的,如何使用qsort函数来排序结构体数据呢?

结构体数组应该怎么书写呢?

每一个元素就用一个{ }括起来,,前面[ ]里面的元素个数可以不写,这就相当于是一个一维数组,系统会根据后面初始化里面数出元素的个数,此处可以省略不写元素个

数;

因为结构体里面有多种类型,比如年龄和名字,分别是int整型和字符串类型,,我们排序之前要先定义一个基准,即到底排序哪种类型。

即按照结构体的年龄排序,还是按照结构体的名字排序,两者的类型不一样,所以排序方法也不一样,要先进行定义一个基准;

注意,qsort函数的第一个参数是待排序的数据中的首元素的地址,这里是对各个结构体数据进行排序,把每一个结构体变量的内容看做一个整体,所以首元素的地址就

是{"zhangsan",30}的地址,所以第一个参数位置写数组名即可。

直接把这两个放在strcmp函数里当两个参数即可,该函数的返回值和qsort函数的第四个参数所指的函数的返回值是一样的,,

如果strcmp返回一个大于0的数,则第一个元素大于第二个元素;

如果strcmp返回一个等于0的数,则第一个元素等于第二个元素;

如果strcmp返回一个小于0的数,则第一个元素小于第二个元素;

就可以直接把strcmp函数得到的数据直接作为 return的返回值,

strcmp函数的头文件是: #include(string.h)

所谓比较字符串的大小:

1.

以字符的ASCII值确定,比较规则是,从第一个字符开始,顺次向后直到出现不同的字符或者结束为止,然后以第一个不同的字符的ASCII值确定,例如上面的”abc”

和"aabdfg",由于第一个字符相同,都是'a'所以看下一个字符,第二个字符,一个是'b',一个是‘a',由于b的ASCII值比a的ASCII值大,所以,这二个字符串的比较结果

是"abc">"aabdfg",

2.

字符比较(character comparison)是指按照字典次序对单个字符或字符串进行比较大小的操作,一般都是以ASCII码值的大小作为字符比较的标准。

3.

字符串比较的时候,字符串的大小是从最左边第一个字符开始比较,大者为大,小者为小,若相等,则继续比较后面的字符;

再如ABC与ABC123比较,比较三个字符后第一个串结束,所以就是后面一个串大,所以,长度不能直接决定大小,字符串的大小是由左边开始最前面的字符决定的。

4.

大写字母和小写字母的ASCII代码值是有区别的,所以,”yes”>”YEs”。

5.

当字符串全部用英文字母的大写(或小写)组成时,字符串的大小顺序和它们在字典中的顺序相同。

6.

由汉字组成的字符串可以参加比较。如”李红”

7.

当字符串有空格时,空格也参加比较。如" t一ABOOK" (表示空格),"A—BOOK",显示前面的大。

对于刚刚的那个题而言,要比较"zhangsan","lisi","wangwu",,我们先看首字母一不一样,发现z,l,w,都是不一样的,我们查表得到对应的ASCII码值分别为:z-

122,,l-108,,w-119,,所以,升序的话,应该是,"lisi","wangwu","zhangsan",这样就是字符串的大小,

如果是两个字符进行比较,那么比较的就是他们对应的ASCII码值即可;

使用回调函数,模拟实现 qsort (采用冒泡的方式)。
注意:这里第一次使用 void* 的指针,讲解 void* 的作用。
#include <stdio.h>
int int_cmp(const void * p1, const void * p2)
{
  return (*( int *)p1 - *(int *) p2);
}
void _swap(void *p1, void * p2, int size) 
{
    int i = 0;
    for (i = 0; i< size; i++)
   {
        char tmp = *((char *)p1 + i);
       *(( char *)p1 + i) = *((char *) p2 + i);
       *(( char *)p2 + i) = tmp;
   }
}
void bubble(void *base, int count , int size, int(*cmp )(void *, void *))
{
    int i = 0;
    int j = 0;
    for (i = 0; i< count - 1; i++)
   {
       for (j = 0; j<count-i-1; j++)
       {
            if (cmp ((char *) base + j*size , (char *)base + (j + 1)*size) > 0)
           {
               _swap(( char *)base + j*size, (char *)base + (j + 1)*size, size);
           }
       }
   }
}
int main()
{
    int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
    //char *arr[] = {"aaaa","dddd","cccc","bbbb"};
    int i = 0;
    bubble(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; 
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

脱缰的野驴、

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值