关于C语言的知识放在专栏:C
小菜坤日常上传gitee代码:https://gitee.com/qi-dunyan
❤❤❤
个人简介:双一流非科班的一名小白,期待与各位大佬一起努力!
本文承接了上文:初阶指针——全面了解指针,在此基础上对指针进行更深一步的剖析,相信我,也请相信你自己!
前言
我们都知道,指针是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;
}
具体如下:
QQ录屏20221023011939
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的功能文章来源:https://www.toymoban.com/news/detail-834894.html
#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文章来源地址https://www.toymoban.com/news/detail-834894.html
到了这里,关于【C指针进阶】(C精髓)——对指针的更进一步深入剖析(图文近2w详解)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!