目录
🍃前言
🍃1. 字符指针
🍃1.1 讲解以及注意事项
🍃1.2 例题
🍃2. 指针数组
🍃2.1 前知温习
🍃2.2 例题
🍃2.3 它和二维数组有什么区别
🍃3. 数组指针
🍃3.1 数组指针的定义
🍃3.2 &数组名VS数组名
🍃总结
🍃3.3 指针数组的指针
🍃3.4 数组指针的使用
🍃3.5 我们回顾并看看下面的代码
🍃总结
🍃4. 数组参数、指针参数
🍃4.0 数组传参降维问题
🍃例子
🍃为什么要降维?
🍃为什么数组和指针访问互通?
🍃4.1 一维数组传参
🍃4.2 二维数组传参
🍃4.3 一级指针传参
🍃4.4 二级指针传参
🍃试一试
🍃5. 函数指针
🍃5.1 函数地址
🍃5.2 函数本质是什么
🍃5.3 函数地址的保存
🍃5.4 区别返回指针的函数和函数指针
🍃5.5 趣例分析
🍃5.5.1 第一例
🍃 5.5.2 第二例
🍃5.5.3 如何简化代码2
🍃6. 函数指针的使用
🍃6.1 发现问题
🍃6.2 使用函数指针优化
🍃7. 函数指针的数组
🍃7.1 概念
🍃 7.2 函数指针数组的用途:转移表
🍃7.2.1 例子(还是计算器)
🍃8. 函数指针数组的指针
🍃9. 回调函数
🍃9.1 概念讲解
🍃9.2 演示qsort函数的使用
🍃9.2.1 函数说明
🍃9.2.2 排序一下数组
🍃9.2.3 排序一下结构体数组
🍃9.3 模拟实现qsort(采用冒泡的方式)
🍃9.3.1 先看看冒泡排序
🍃9.3.2 分析回调函数的构成
🍃9.3.3 如何改动
🔖敬请期待更好的作品吧~
🍃前言
学完了C指针初阶的内容,接下来就是进阶内容了,难度系数飙升有木有,,但是指针不学进阶真的不行,除非只是想基本掌握C语言或应付考试,不然要深入学习C语言就必须要把指针学好学深学透彻。
本文就来分享一波个人C指针进阶内容的学习见解和心得,由于水平有限,难免会有纰漏,读者各取所需即可。
给你点赞,加油加油!
🍃1. 字符指针
🍃1.1 讲解以及注意事项
在指针的类型中我们知道有一种指针类型为字符指针 char* ;
一般使用:
int main()
{
char ch = 'w';
char *pc = &ch;
*pc = 'w';
return 0;
}
还有一种使用方式如下:
int main()
{
const char* pstr = "hello world.";//这里是把一个字符串放到pstr指针变量里了吗?
printf("%s\n", pstr);
return 0;
}
代码 const char* pstr = "hello world.";
特别容易让人以为是把字符串 "hello world." 放到字符指针 pstr 里了,但是本质是把字符串的首字符的地址放到了pstr中。
注意到const没?因为后面的是字符串常量,是不能且不应该被修改的,当char* pstr = "hello world.";时,指针pstr存放的是字符'h'的地址,权限就被放大了——通过指针可以间接修改字符串常量的值!
比如:
int main()
{
char* pstr = "hello world.";
*pstr = 'w';
printf("%s\n", pstr);
return 0;
}
这是不希望发生的,所以运行后程序崩溃了
我们调试看看,发现有异常:
权限冲突了,因为字符常量不应该被修改,由于指针的权限大,使用指针理论上可以间接修改,但是不被允许,要是修改了就会出问题,所以要这样使用字符指针的话最好在指针前面加个const限定一下权限,保护字符串常量。
🍃1.2 例题
#include <stdio.h>
int main()
{
char str1[] = "hello world.";
char str2[] = "hello world.";
const char *str3 = "hello world.";
const char *str4 = "hello world.";
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;
}
这里str3和str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域(只读区域),当几个指针指向同一个字符串的时候,他们实际会指向同一块内存。
但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4相同。
🍃2. 指针数组
🍃2.1 前知温习
在C指针初阶有讲过指针数组,现在再来看看巩固一下。
🍃2.2 例题
int main()
{
int arr1[] = {1, 2, 3, 4, 5};
int arr2[] = {2, 3, 4, 5, 6};
int arr3[] = {3, 4, 5, 6, 7};
int* parr[3] = {arr1, arr2, arr3};
int i = 0;
int j = 0;
for (i = 0; i < 3; i++)
{
for (j = 0; j < 5; j++)
{
printf("%d ", parr[i][j]);
}
printf("\n");
}
return 0;
}
🍃2.3 它和二维数组有什么区别
二维数组是连续存放的一整块内存空间,而指针数组则不一定是存放在一起的,完全有可能是分布在不同内存位置的几个数组通过指针联系在一起的。
🍃3. 数组指针
🍃3.1 数组指针的定义
数组指针是指针?还是数组?
答案是:指针。
我们已经熟悉:
整形指针: int * pint; 能够指向整形数据的指针。
浮点型指针: float * pf; 能够指向浮点型数据的指针。
那数组指针应该是:能够指向数组的指针。
下面代码哪个是数组指针?
int *p1[10];
int (*p2)[10];
//p1, p2分别是什么?
解释:
int (*p)[10];
p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫数组指针。
这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。
🍃3.2 &数组名VS数组名
对于下面的数组:
int arr[10];
arr 和 &arr 分别是啥?
我们知道arr是数组名,数组名表示数组首元素的地址。
那&arr数组名到底是啥?
我们看一段代码:
#include <stdio.h>
int main()
{
int arr[10] = {0};
printf("%p\n", arr);
printf("%p\n", &arr);
return 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;
}
根据上面的代码我们发现,其实&arr和arr,虽然值是一样的,但是意义应该不一样的。
实际上: &arr 表示的是数组的地址,而不是数组首元素的地址。
本例中 &arr 的类型是: int(*)[10] ,是一种数组指针类型。
数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是40
🍃总结
数组名通常表示的都是数组首元素地址。
但是有两个例外:
1.sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。
int main()
{
int arr[10] = {0};
int sz = sizeof(arr);
printf("%zd\n", sz);
return 0;
}
2.&数组名,这里的数组名表示的依旧是整个数组,所以取出的是整个数组的地址,从值上看可能觉得没什么区别,但是指针类型大不相同,(假设是int arr[10])arr是int*类型,而&arr是int(*)[10]类型。
🍃3.3 指针数组的指针
int main()
{
char* arr[5] = {0};
//要用怎样的指针接收该数组的地址呢?
return 0;
}
首先得是个指针,(*p),其次得对上类型,要指向的对象是一个数组,而且是一个有五个元素的字符指针数组,那就是char* (*p)[5]。
要注意不是char(*p)[5],char(*p)[5]是字符数组指针,指向一个含五个元素的字符数组。
所以说指针数组的指针应该是这样的:类型* (*指针名)[x] ,x表示一个正整数。
🍃3.4 数组指针的使用
那数组指针是怎么使用的呢?
既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址。
#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int (*p)[] = &arr;
return 0;
}
括号[]里要标明元素个数,不同个数其实类型不同,比如int(*p)[5]就是指向含五个元素的一维数组的指针,而int(*p)[10]则是指向含十个元素的一维数组的指针,不标明的话默认是0。
那好,我们改一改,再看看怎么样:
#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int (*p)[10] = &arr;
return 0;
}
p是指向数组的,*p相当于整个数组,也就相当于数组名,而数组名又是数组首元素的地址,所以*p相当于arr,那就有*(*p + i)等价于arr[i]。
把数组arr的地址赋值给数组指针变量p ,但是我们一般很少这样写代码,十分不建议这样用,真要这样还不如直接就 int*p = arr;。
数组指针其实不是用在一维数组上的,而是二维以及更高维数组上。
🍃3.5 我们回顾并看看下面的代码
int arr[5];
int *parr1[10];
int (*parr2)[10];
int (*parr3[10])[5];
int arr[5]; //整型数组
int *parr1[10]; //整型指针数组
int (*parr2)[10]; //整型数组指针
int (*parr3[10])[5]; //整型数组指针的指针数组
🍃总结
判断是数组还是指针,看看变量名跟谁近,同时注意[]优先级比*高,如果是(*p),就是指针,而如果是p[],就是数组,一旦判断完本质是指针还是数组后,剩下的组合在一起就是对应的指针指向的或数组存放的元素的类型。
🍃4. 数组参数、指针参数
在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?
🍃4.0 数组传参降维问题
所有的数组,传参时都要降维成指针,降维成指向其内部元素类型的指针。
🍃例子
在main函数中数组大小为20字节,但传入ShowArray函数后大小变为4字节(32位平台)。
🍃为什么要降维?
如果不降维,传参时就要发生数组拷贝,使得函数调用效率降低。
在C中,任何函数调用,只要有形参实例化,必定形成临时拷贝。
C是面向过程语言,函数是核心概念,定义或调用函数就很重要,而调用函数就涉及到传参,传参就有可能传数组,而直接传数组会发生数组拷贝,为了提高效率就进行了降维,降维成指针。
🍃为什么数组和指针访问互通?
在这里解答一下《指针初阶》博文中的问题。
假设数组只能用[ ]访问,指针只能用*解引用访问:
#define N 10
void InitArr_1(int *arr, int n)//所以传的是指针
{
for (int i = 0; i < n; i++)
{
*(arr + i) = i; //在这里只能以指针的形式访问
}
}
int main()
{
int arr[N];
InitArr(arr, N);//数组名在这里相当于数组首元素地址
for (int i = 0; i < N; i++)
{
printf("%d\n", arr[i]); //以数组的形式访问
}
return 0;
}
上面代码其实没有什么问题,不过,不知道大家发现没有,如果没有将指针和数组元素访问打通,那么在C中(面向过程)中如果有大量的函数调用且有大量数组传参,会要求程序员进行各种访问习惯的变化,也就是数组定义处用数组访问法,调用的函数内部用指针访问法,多麻烦呀。只要是要求人做的,那么就会提高代码出错的概率和调试的难度。
所以干脆C将指针和数组的访问方式打通,让程序员在函数内,也好像使用数组那样进行元素访问,本质上降低了编程难度。
而且传参传数组名,形参写成数组形式,调用的函数中用数组访问法,一目了然,能让人很清楚是在使用数组,不然还要和指针区分一下到底用的是哪个。
🍃4.1 一维数组传参
🍃4.2 二维数组传参
🍃4.3 一级指针传参
思考:
当一个函数的形参为一级指针的时候,函数能接收什么参数?
🍃4.4 二级指针传参
思考:
当函数形参为二级指针的时候,可以接收什么参数?
🍃试一试
void test(char **p)
{}
int main()
{
char c = 'b';
char*pc = &c;
char**ppc = &pc;
char* arr[10];
test(&pc);//Ok?
test(ppc);//Ok?
test(arr);//Ok?
return 0;
}
答案可以由上面的图得知,主要看类型对不对的上。
🍃5. 函数指针
类比一下数组指针,可以知道函数指针就是指向函数的指针。
🍃5.1 函数地址
以下两种形式都可以拿到函数的地址。
#include <stdio.h>
void test()
{
printf("hehe\n");
}
int main()
{
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}
输出的是两个地址,这两个地址是 test 函数的地址。
🍃5.2 函数本质是什么
一个函数就是一组存储在连续内存块中的指令集合,用来执行一个子任务(也就是函数具有特定功能)。
我们写的程序源代码经过编译器处理最终得到机器代码,而函数相关指令存放在code代码区,机器语言中的函数调用基本上就是一条跳转指令,跳到函数的入口点(函数的第一条指令)。
实际上,函数只要在程序中写出来了在真正执行程序前就有地址了,不论有无被调用。
🍃5.3 函数地址的保存
那我们的函数的地址要想保存起来,怎么保存?
int (*pf)(int, int) = &Add;
int ret = (*pf)(2, 3);
//*其实可以不写,如:pf(2, 3),实际上有没有没关系的,只是有的话看起来更清晰易懂。
在获得函数地址时,&加不加都可以,只是加上更好理解。
在通过指针调用函数时,,*加不加都可以,只是加上*更好理解,如果要加必须带上括号。
🍃5.4 区别返回指针的函数和函数指针
void test()
{
printf("hehe\n");
}
//下面pfun1和pfun2哪个有能力存放test函数的地址?
void (*pfun1)();
void *pfun2();
上面的是对应类型的指针,下面的是一个返回值类型为void*的函数。
()优先级比*高,若要表示函数指针则要注意让指针名和*靠得更近也就是(*pf)加上括号,如果没加括号就是函数,还是返回指针的函数。
🍃5.5 趣例分析
🍃5.5.1 第一例
//代码1
(*(void (*)())0)();
想想看,这是个啥?
分析:
以上代码实质上是一次函数调用,调用的是0作为地址处的函数。
1.把0(int类型)强制类型转换成了void(*)()的函数指针类型,并且值为0,也就是地址为0
2.再调用0地址处的这这个函数。
🍃 5.5.2 第二例
//代码2
void (*signal(int , void(*)(int)))(int);
想想看,这是个啥?
分析:
声明的signal函数的第一个参数的类型是int,第二个参数的类型是void(*)(int)函数指针,signal函数的返回类型也是一个函数指针,类型void(*)(int)。
🍃5.5.3 如何简化代码2
重命名类型名就清晰多了:
typedef void(*pf_t)(int); //把void(*)(int)类型重命名为pf_t
这样的话原来的代码可以写成:
pf_t signal(int, pf_t);
是不是一下子就看出来是什么了?
🍃6. 函数指针的使用
🍃6.1 发现问题
写一个简单的计算器小程序:
#include <stdio.h>
void menu()
{
printf( "*************************\n" );
printf( " 1:add 2:sub \n" );
printf( " 3:mul 4:div \n" );
printf( "*************************\n" );
printf( "请选择:" );
}
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
{
menu();
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");
breark;
default:
printf( "选择错误\n" );
break;
}
} while (input);
return 0;
}
发现什么不太好的地方没有?
如图:
🍃6.2 使用函数指针优化
对于这个冗余问题,可能有人就会说,简单嘛,我把这个模块封装成函数再调用不就省事了吗?
但是这四个模块是有差异的呀,里面调用的计算函数不同啊,比如调用的是add函数,那这个封装的函数不就只能执行加法运算了嘛。
那可不可以把函数指针作为函数参数呢,在主函数中分了不同的情况,每种情况下传特定的函数的指针行不行呢?比如要执行加法运算了,就把add函数地址传入,再通过指针调用函数完成运算,即使要执行别的运算,也只是传入的函数地址不同,其他的代码完全是相同的。
//把运算统一起来
void calc(int(*pf)(int, int))
{
int x = 0;
int y = 0;
int ret = 0;
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
ret = pf(x, y);
printf("%d\n", ret);
}
//主函数发生变动
int main()
{
int x, y;
int input = 1;
int ret = 0;
do
{
menu();
scanf( "%d", &input);
switch (input)
{
case 1:
calc(add);
break;
case 2:
calc(sub);
break;
case 3:
calc(mul);
break;
case 4:
calc(div);
break;
case 0:
printf("退出程序\n");
breark;
default:
printf( "选择错误\n" );
break;
}
} while (input);
return 0;
}
在主函数main中就能控制调用函数calc内的其他的被调用函数对象是什么(add/sub/mul/div),而且是根据主函数内不同情况要求下的响应。
说实话,要是不用函数指针的话能写出这样的代码吗?
🍃7. 函数指针的数组
🍃7.1 概念
函数指针也是指针,而把函数指针放在数组中,就是函数指针数组。
看看整型指针数组:
int *arr[10];//数组的每个元素是int*
那函数指针数组长啥样呢?
函数指针:
int(*p)(int, int)
对应的数组:int(*parr[4])(int, int)
parr先和 [] 结合,说明 parr是数组,数组的内容是什么呢?
是 int (*)(int, int) 类型的函数指针。
🍃 7.2 函数指针数组的用途:转移表
🍃7.2.1 例子(还是计算器)
前面函数指针例子中用calc函数和函数指针传参的方法将四个运算函数统一了起来,而本例中不创建新的函数,而是使用函数指针数组和分支语句来对应不同情况下调用不同的函数进行运算,通过函数指针数组的值解引用来调用函数。
这时候如果想增加计算器小程序的运算功能,比如增加&运算,^运算,|运算等等,只需要创建一个运算函数然后把地址放到函数指针数组里,改一下分支语句判断条件即可。
#include <stdio.h>
void menu()
{
printf( "*************************\n" );
printf( " 1:add 2:sub \n" );
printf( " 3:mul 4:div \n" );
printf( "*************************\n" );
printf( "请选择:" );
}
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)
{
menu();
scanf( "%d", &input);
//边界判断输入是否合法
if ((input <= 4 && input >= 1))
{
printf( "输入操作数:" );
scanf( "%d %d", &x, &y);
ret = (*p[input])(x, y);//解引用数组元素调用函数
printf( "ret = %d\n", ret);
}
else if(input == 0)
{
printf("退出计算器\n");
}
else
printf( "输入有误\n" );
}
return 0;
}
为什么要把函数指针数组叫做转移表?通过函数指针调用函数实际上是跳转到对应函数,程序执行位置从一个地方转移到了另一个地方,也就是每个函数指针提供一次跳转机会,把这些指针放在一块的数组就是转移表。
🍃8. 函数指针数组的指针
说实话,理论上可以无限“套娃”下去,指针放数组里,指针又可以指向数组......师傅别念了😭
我们把上个例子的函数指针数组拿过来看看它的指针该怎么写:
int(*p[5])(int x, int y) = { 0, add, sub, mul, div };
int (*(*pfarr)[5])(int, int) = &pfarr;
很多代码看起来花里胡哨,其实只要抓住本质特征就不容易混淆:
(*parr)[ ]是数组指针,*parr[ ]是指针数组。
其他的一堆东西都是附加的,如果本质是数组指针,那么其他的东西就是数组的元素类型;如果本质是指针数组,那么其他的东西就是指针的类型。
🍃9. 回调函数
🍃9.1 概念讲解
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
解说一波:
光看定义可能会不知所云,还记得上面讲函数指针时写的计算器小程序吗?我们觉得原代码冗余,于是用函数指针传参来实现将运算功能统一起来放在calc函数里,我们是在main函数里实现的,调用calc函数,假设程序需要进行加法运算,只需要把add函数地址传给calc函数,然后在calc函数内通过所给的函数指针间接地调用add函数完成加法,若要进行其他运算也类同,只需要传入对应的函数地址即可。
并且传入的地址是从主函数传进去的,是根据主函数中满足某种条件来确定传入哪一种函数的地址的,但不是主函数直接调用add等函数,而是主函数调用calc函数,再经由calc函数来调用的。
void calc(int(*pf)(int, int))//从主函数中根据情况传入不同函数的地址
{
int x = 0;
int y = 0;
int ret = 0;
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
ret = pf(x, y);
printf("%d\n", ret);
}
说了那么一大堆,我们还是来看看实例吧。
🍃9.2 演示qsort函数的使用
🍃9.2.1 函数说明
所在头文件:<stdlib.h>
函数原型:
void qsort( void *base, size_t num, size_t width, int (__cdecl *compare )(const void *elem1, const void *elem2 ) );
我们这里简化一下:
void qsort(
void *base, //你要排序的数据的起始位置
size_t num, //待排序的数据元素的个数
size_t width, //待排序的数据元素的大小(单位:字节)
int(*cmp )(const void *e1, const void *e2 )//一个比较函数的函数指针,e1和e2接收的是要比较的两个元素的地址
);
最后一个参数用来干什么的呢?实际上qsort函数可以排序任意类型的数据,这也归功于这个比较函数的实现逻辑。有些数据类型不能直接用关系运算符>或<来比较,需要在比较函数里面做出特定的比较,然后再用函数指针来调用。
这样的话,要是想排序数组的话,比较函数传入的就是数组元素,然后实现对应的比较逻辑,再通过函数指针调用比较函数;而要是想排序结构体成员,比较函数传入的就是结构体成员,然后实现对应的比较逻辑,再通过函数指针调用比较函数,诸如此类。
void*是没有具体类型的指针,可以用来接收任意类型的地址,但是不能解引用,也不能进行指针运算,要使用根据需要强制类型转换一下。
下表是比较函数返回值对应的逻辑,据此可以return (*( int *)e1 - *(int *) e2);,只要解引用e1比解引用e2大就返回正数,相等返回0,小于返回负数。
🍃9.2.2 排序一下数组
#include <stdio.h>
#include<stdlib.h>
//要使用qosrt函数得先实现一个比较函数
int cmp_int(const void * e1, const void * e2)
{
//注意强制类型转换
return (*( int *)e1 - *(int *) e2);
}
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), cmp_int);
for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
{
printf( "%d ", arr[i]);
}
printf("\n");
return 0;
}
其实qsort()默认排升序,要排降序的话把指针解引用相减对象交换一下。
return (*( int *)e2 - *(int *) e1);
🍃9.2.3 排序一下结构体数组
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
struct Stu
{
char name[20];
int age;
}
int cmp_stu_by_name(const void* e1, const void* e2)
{
//比较字符串长度要用strcmp(),正好其返回值也满足要求
return strcmp(((struct Stu*)e1) -> name, ((struct Stu*)e2) ->name);
}
void test1()
{
//根据结构的字符数组长度来排序
struct Stu s[] = {{"zhangsan", 15}, {"lisi", 25}, {"wangwu", 30}};
int sz = sizeof(s) / sizeof(s[0]);
qsort(s, sz, sizeof(s[0], cmp_stu_by_name));
}
int cmp_stu_by_age(const void* e1, const void* e2)
{
//直接相减即可
return ((struct Stu*)e1) -> age - ((struct Stu*)e2) ->age;
}
void test2()
{
//根据结构的age变量大小来排序
struct Stu s[] = {{"zhangsan", 15}, {"lisi", 25}, {"wangwu", 30}};
int sz = sizeof(s) / sizeof(s[0]);
qsort(s, sz, sizeof(s[0], cmp_stu_by_age));
}
int main()
{
test1();
test2();
return 0;
}
🍃9.3 模拟实现qsort(采用冒泡的方式)
🍃9.3.1 先看看冒泡排序
void BubbleSort(int *arr, int sz)
{
int i = 0;
int j = 0;
for (i = 0; i < sz - 1; i++)
{
for (j = 0; j < sz - i - 1; j++)
{
if (arr[j] > arr[j + 1])
{
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = arr[j];
}
}
}
}
这是对应的升序排列的代码,要降序的话修改一下if()的判断逻辑即可:arr[j] < arr[j + 1]。
感觉好像基本实现了功能,但是还有点"蠢",何出此言?要是数组原来就已经是排好序的还需要再继续执行遍历吗?这段代码运行起来无法分辨数组是否已经排好序,不管三七二十一直接遍历,虽然结果也不会有问题,但是明显可以修改。
定义一个flag变量来确定数组状态(是否排好序),初始时默认已排序flag=1,一旦发现数组不是完全排好序的话就把flag置零,表示未排好序。每轮结束(内层循环结束)后判断一下flag,若为1说明已排好序,直接跳出循环,不然继续排序。
代码如下:
void BubbleSort(int *arr, int sz)
{
int i = 0;
int j = 0;
for (i = 0; i < sz - 1; i++)
{
int flag = 1;
for (j = 0; j < sz - i - 1; j++)
{
if (arr[j] > arr[j + 1])
{
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = arr[j];
flag = 0;
}
}
if(flag)
break;
}
}
然而这段排序代码只能排序整型数组,因为函数的参数给的就是整型指针。
🍃9.3.2 分析回调函数的构成
参数列表
void qsort(
void *base, //你要排序的数据的起始位置
size_t num, //待排序的数据元素的个数
size_t width, //待排序的数据元素的大小(单位:字节)
int(*cmp )(const void *e1, const void *e2 )//一个比较函数的函数指针,e1和e2接收的是要比较的两个元素的地址
);
进一步说明:
void*base,为什么是void*类型?因为要使用qsort()排序的数据类型不确定,这里设计成一种泛型类型,使用时强制类型转换即可。
size_t num和size_t width (size_t是sizeof()特定的无符号整数类型)可以给出元素数量和单个元素内存大小,这样就很容易用指针偏移找个各个元素,方便排序。
int(*cmp )(const void *e1, const void *e2 )用函数指针指向一个比较函数,用来确定升序或降序逻辑。
🍃9.3.3 如何改动
一个方面是把参数列表改成qsort的,然后就是看看在哪插入比较函数的调用了。
原代码中有比较的地方在哪?
就是if(arr[j] > arr[j + 1])处,改成
if(cmp((char*)base + j*width, (char*)base + (j + 1)*width))
再有就是需要一个交换函数,因为待交换的数据类型不确定,所以不止要传递两元素的地址,还要传递元素大小,再一个字节一个字节交换。
代码如下:
int cmp_int(const void * e1, const void * e2)
{
//注意强制类型转换
return (*( int *)e1 - *(int *) e2);
}
void swap(char* buf1, char* buf2, width)
{
int i = 0;
for(i = 0; i < width; i++)
{
char tmp = *buf1;
*buf1 = * buf2;
*buf2 = *buf1;
buf1++;
buf2++;
}
}
void BubbleSort(void *base, int size, int width, int(*cmp )(const void *e1, const void *e2 )
{
int i = 0;
int j = 0;
for (i = 0; i < sz - 1; i++)
{
int flag = 1;
for (j = 0; j < sz - i - 1; j++)
{
if (cmp((char*)base + j*width, (char*)base + (j + 1)*width))
{
swap((char*)base + j*width, (char*)base + (j + 1)*width, width
);
flag = 0;
}
}
if(flag)
break;
}
}
int main()
{
int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
int i = 0;
BubbleSort(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), cmp_int);
for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
{
printf( "%d ", arr[i]);
}
printf("\n");
return 0;
}
这一种版本适合任何类型数据排序,原来的冒泡排序只能排整型。
但库函数里实现qsort的内部逻辑框架用的不是冒泡而是快速排序。