本文承接了上文:初阶指针——全面了解指针,在此基础上对指针进行更深一步的剖析,相信我,也请相信你自己!
指针全解
前言
1、初阶指针大致回顾
2、 字符指针
3、指针数组
4、 数组指针
5、数组传参和指针传参
6、函数指针
7、 函数指针数组
8、指向函数指针数组的指针
9、回调函数
qsort 函数实现
qsort函数模拟(冒泡思想)
前言
我们都知道,指针是C语言中必不可少的一部分,是C语言的精髓所在,一个学习C语言的人如果不对指针有着深刻的理解,那还不算得上是真正入门,本篇文章整理了对于指针更深一步的了解所学到的知识,以及一些个人的见解,期待能帮到您对此有更深刻的认识。话不多说,直接开搞!
1、初阶指针大致回顾
在之前的指针初阶文章里,主要讲了以下内容:
指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
不管是什么类型的指针,它的大小是固定的4/8个字节(32位平台/64位平台)。
指针是有类型,指针的类型决定了指针的±整数的步长,指针解引用操作的时候的权限。
指针的运算——两个指针相减后的结果的绝对值,就是两个指针之间的元素个数。
避免野指针的出现,写出好的代码
二级指针是存放一级指针地址的指针变量
大概就是这些,接下来,正式进入指针进阶。
2、 字符指针
字符指针是用来存放字符的一种指针类型,我们平常都是这么使用:
#include<stdio.h> int main() { char ch = 's'; char* p = &ch; //这里的p就是指针变量,类型为char*,存放字符's'的地址 return 0; }
或者这样使用:
#include<stdio.h> int main() { const char* p = "abcdef"; //这里要注意一点,就是这里的p存放的实际上是字符串首字符'a'的地址,并不是整个字符串的地址 return 0; }
2.1 字符数组与字符指针
大家这里还要注意一点,就是字符数组与字符指针之间的区别,它们两个看上去很像,但是所表达的意思却完全不同,如下代码:
#include<stdio.h> int main() { char str[] = "abcdef"; char* str1 = "abcdef"; return 0; }
在这里,str表示的是一个数组,既然是数组,那么这里面的元素是可以进行修改的,就比如把a改成z,只需要把z赋值给数组首元素就可以了,但是,下面的str1所指向的是一个字符串常量,而字符串常量是不可以进行修改的。如下:
#include<stdio.h> int main() { char str[] = "abcdef"; char* str1 = "abcdef"; str[0] = 'z'; *str1 = 'z'; return 0; }
所以我们一般在用字符指针指向字符串常量时,前面会加一个const,从而增加代码的健壮性,避免我们后面万一修改字符串常量的操作,因为修改的话,编译器是不会报错的,只有通过调试才能发现错误,加上const后,如果进行修改的话,编译器会报错提醒。
另外,我们还可以通过字符串首字符的地址找到整个字符串:
#include<stdio.h> int main() { const char* str1 = "abcdef"; printf("%s\n", str1);//abcdef printf("%c\n", *str1);//a return 0; }
这里注意一点,str1存放的是首字符a的地址,所以再对其解引用的时候,打印出来的其实是首字符a。
2.3 练习题
#include <stdio.h> 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];//二级字符指针的数组
举个例子:
我们可以通过指针数组来实现用一维数组模拟二维数组:
#include<stdio.h> int 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、&数组名,数组名前面加上&,表示取出的是整个数组的地址。
#include<stdio.h> int 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<stdio.h> //用数组指针来接收整个数组的地址,这里注意[]要与数组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<stdio.h> //由于传过来的是二维数组里的第一行的地址,第一行有四个元素,所以可以把它看作传来的是一个有四个元素的一维数组的地址 //所以用数组指针来接收,指针指向数组的四个元素 //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 <stdio.h> void test(int arr[3])//第一种,我们用数组传参,那我们可以用数组的形式来接收参数,写成arr[]的形式,方括号里的数组可以省略,但必须有方括号 {} void test(int* arr)//第二种,数组名表示首元素地址,我们可以用指针来接收地址 {} void test2(int* arr[10])//指针数组传参,指针数组接收,没毛病,方括号的10可省略 {} void test2(int** arr)//二级指针来接收一级指针的地址,没毛病 {} int main() { //arr是整形数组,具有10个元素,每个元素类型为int int arr[10] = { 0 }; //arr2为指针数组,具有10个元素,每个元素类型为int* int* arr2[10] = { 0 }; test(arr);//数组名表示首元素地址,这里传送的是arr[0](int类型)的地址 test2(arr2);//数组名表示首元素地址,这里传的是arr2[0](int*类型)的地址 }
5.2二维数组传参
对于二维数组传参,我们通常用以下几种方式来接收
#include<stdio.h> void 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一级指针传参
对于一级指针的传参与接收,有以下几种:
#include<stdio.h> void 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二级指针传参
对于二级指针的传参与接收,有以下几种:
#include<stdio.h> void 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函数指针定义
在上面的内容中,我们讲到了&数组名表示取出整个数组的地址,数组名表示数组首元素的地址,那么,我们不妨大胆猜测,&函数名,以及函数名是不是表示函数的地址呢?带着这个疑问,我们来看以下代码:
#include<stdio.h> int add(int x,int y) { return 0; } int main() { printf("add = %p\n", add); printf("&add = %p\n", &add); return 0; }
在这里,我们可以发现,原来&函数名以及函数名所表示出来的地址是相同的,这里由于函数并不像数组那样,具有多个元素,每个元素都有各自所对应的地址,所以,&函数名以及函数名都表示函数的地址,两者之间没有任何区别。
因此,我们可以这样来定义函数指针:
#include<stdio.h> int 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函数指针使用
#include<stdio.h> int 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,返回类型为int sum1 = (*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); //5 printf("%d\n", sum1);//5 printf("%d\n", sum2);//5 return 0; }
6.3小练习
看如下代码
//代码1 (*(void (*)())0)();/ //代码2 void (*signal(int , void(*)(int)))(int);
解释如下:
#include<stdio.h> int main() { //代码1 (*(void (*)())0)(); //这里可以先把(void(*)())先给去掉,此时就剩下(*0)();这表明调用0所指向的函数,但是0只是一个整形,怎么能调用函数呢,只有函数指针才可以调用函数 //所以(void(*)()),最外面的()是强制类型转换,void(*)()是一个函数指针的格式 //所以这个代码的意思就是把0,强制类型转换为一个函数指针类型,此时的0不再是一个整形,而是一个函数指针,然后调用0所指向的函数 //代码2 void (*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定义
函数指针数组,顾名思义就是一个数组,用来存放函数指针的数组,即该数组可以存放多个参数相同、返回类型相同的函数地址
如下:
#include<stdio.h> int 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,参数都为int int //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函数指针数组的使用
有了函数指针数组,我们就可以通过数组的元素,来访问所对应的函数指针即函数地址了。举个例子:
#include<stdio.h> int 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,即3 sub1 = p[1](1, 2);//p[1]即sub,本质即sub=sub(1,2),返回1-2,即-1 printf("%d\n", sum1);//3 printf("%d\n", sub1);//-1 return 0; }
了解这个后,我们可以通过函数指针数组实现一个具有加减乘除的简易计算器,如下:
#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; //函数指针数组,下标为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; }
8、指向函数指针数组的指针
8.1定义
上面我们讲完了函数指针数组,而我们一开始所了解的整型数组,字符数组等,它们都可以将自己的地址存放到指针变量中,然后通过这个指针就可以访问它们,而作为函数指针数组,它也有自己的地址,我们也可以通过一个指针变量来存放它的地址,然后再通过指针来灵活使用它们。
那么它的形式是怎样的呢?且看下面:
#include<stdio.h> int 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指向函数指针数组的指针的使用
了解它的含义后,那么该如何使用呢?举个例子:
#include<stdio.h> int 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);//3 printf("%d\n", sub1);//-1 return 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回调函数的使用*
这里我们再举之前计算器的例子,对那段代码进行修改,如下:
#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; } 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 函数实现
到目前为止,我们学过了一种排序方法,即冒泡排序,如下:
#include<stdio.h> void 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 1 return 0; }
但是,冒泡排序有一个缺陷,就是冒泡排序只能排序整形,对于char类型,或者结构体类型是无法进行排序的,那么有没有一种函数是可以实现对任意类型元素的排序呢?答案是有的,就是qsort函数
在这里,我们发现,qsort函数有四个函数参数,这些函数参数都是什么意思呢?大家看以下讲解就明白了:
而对于用来比较两个元素大小的函数指针,又有以下定义:
这里我们知道,对于两个元素e1,e2,假如e1>e2,就返回一个大于0的数,如果e1<e2,就返回一个小于0的数,如果e1 == e2 ,就返回0
qsort是一个库函数,它的使用需要包含头文件#include<stdlib.h>
下面我们通过qsort函数再来排序上面的数:
1.qsort排序整形
#include<stdlib.h> #include<stdio.h> //compar的两个参数都为const void*,返回类型为int int 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<stdio.h> #include<stdlib.h> #include<string.h> int 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<stdio.h> #include<stdlib.h> #include<string.h> //qsort排序结构体以及用冒泡思想实现qsort的功能 struct stu { char name[10]; int age; }; //结构体成员排序 int cmp_struct_name(const void* e1, const void* e2) { //strcmp比较字符串大小的库函数,后面会讲到 //强制类型转换,再指向结构体成员name return strcmp(((struct stu*)e1)->name, ((struct stu*)e2)->name); } //结构体成员排序 int cmp_struct_age(const void* e1, const void* e2) { //强制类型转换,再指向结构体成员age return ((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<string.h>,返回类型为int,两个参数为const char*
对于两个字符串如果str1<str2即(str1-str2)<0,返回一个<0的数,如果(str1==str2),返回0,如果str1>str2,即(str1-str2)>0,返回>0的数。刚好与qsort的返回值相同
strcmp是专门用来比较字符串大小的库函数,在以上例子中就用到了此函数。
qsort函数模拟(冒泡思想)
清楚qsort函数的使用后,我们用我们学过的冒泡思想来模拟实现qsort的功能
#include<stdio.h> #include<stdlib.h> #include<string.h> //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