【C指针进阶】(C精髓)——对指针的更进一步深入剖析(图文近2w详解)

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

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

前言

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

1、初阶指针大致回顾

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

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

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

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的功能

#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

  • 122
    点赞
  • 194
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 93
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 93
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

诺诺的包包

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

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

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

打赏作者

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

抵扣说明:

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

余额充值