【C语言】C语言数组和指针


前言

接下来的讲解部分是指针的进阶,包含多种指针以及对应的数组,这部分章节对我们来说很重要,也是c语言中的重点模块儿,重要性不言而喻
我们直接进入正题,开始我们今天重要的学习旅程吧😎😎😎

一、指针部分

1.字符指针:

在字符指针使用时,我们通常有两种使用的方式,前者是一般使用方式,后者是我们今天重点所讲部分

int main()
{
    char ch = 'w';
    char *pc = &ch;
    *pc = 'w';
    return 0;
}

定义一个字符变量存放字符,然后在取出字符的地址,存到字符指针当中去,我们后续可以通过解引用操作符对字符进行操作

int main()
{
    const char* pstr = "hello bit.";
    printf("%s\n", pstr);
    return 0;
}

下面这样的定义类型,其实只是将常量字符串的首字符地址放到了字符指针pstr中,而我们打印字符串使用%s时,其实也只需要将首字符地址传给printf函数,它会自动打印字符串直到\0结束

1.1相关的练习题

#include <stdio.h>
int main()
{
    char str1[] = "hello bit.";
    char str2[] = "hello bit.";
    const char *str3 = "hello bit.";
    const char *str4 = "hello bit.";
    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;
}

在这里插入图片描述
问题详解:
1.由答案我们可以看出str1和str2是不同的,这是为什么呢,其实答案很好理解的,因为我们创建了两个不同的字符数组(他们连名字都不相同,那这两个数组怎么可能相同啊???),那么他们在内存中的空间位置肯定是不同的,而数组名代表首元素地址,两个不同的数组的首元素地址肯定也是不相同啊,那么自然str1肯定和str2是不相同的啦
2.首先常量字符串要在内存中开辟空间存储它本身,那么我们有必要在内存中储存两个一模一样的东西吗?(c/c++会把常量字符串储存到单独的一个内存区域中) 当这两个指针指向同一个常量字符串时,实际上就是指向同一块儿地址**(指针就是地址,地址就是指针)**

2.数组指针

2.1数组指针的定义

1.数组指针嘛,那其实非常简单,就是指向数组的指针,数组有哪些组成部分呢?(有数组名,元素个数,元素类型),那我们写一个指针,让他指向数组就可以了

例如:
int arr[10]={0};
int (*ps)[10]=&arr;
//注意ps和[]的结合优先级较高,如果没有括号ps会先和[]结合,那样就不是指针了,变成数组了

2.2&数组名和数组名的对比

我们下面看一段代码,比较这两种操作的不同

#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+1的地址相比于&arr是要大40个字节,而arr+1的地址是要比arr的地址大4个字节。
那么由此便可以说明问题,&arr实际上取出的是整个数组的地址

2.3数组指针的使用

我们可以通过打印数组内容的方式,来练习使用一下数组指针

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int(*pa)[10] = &arr;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", (*pa)[i]);
	}
	for (i = 0; i < 10; i++)
	{
		printf("%d ",*(*pa+i));//*pa==arr,pa放的是&arr,解引用操作后相当于把&符号抵消了
	}
	return 0;
}

代码解释:
首先我们将一维数组arr的地址放到一个数组指针*pa里面。
使用方法1:我们之前就知道,如果我们想要访问一个数组的元素内容,我们可以通过下标访问的形式。那这里第一个for循环就可以很好解释,(*pa)==arr,(*pa)[i]==arr[i]
使用方法2:*pa得到首元素地址,对首元素地址进行解引用操作,即可拿到首元素,那么我们每次解引用时让首元素地址向后挪动整数i,即可跳过元素的单位字节大小的地址。这里补充一个小知识点,指针的类型可以决定,指针±整数一次性跳过多少个字节 ,之后再进行解引用操作,就可以拿到相应的元素了
不足之处: 这里有很多人,感觉这样访问数组元素的方法比较智障🤣🤣🤣,我也感觉很智障,哈哈哈😆,但指针使用数组指针的场景其实不是这样的,这里只不过想用代码的样子给大家呈现一下,这个数组指针的使用方法大概是什么样子的

下面的代码再较高级呈现一下,数组指针的使用形式(坚持读下去,相信你自己😋)

void print1(int arr[3][5], int x, int y)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < x; i++)
	{
		for (j = 0; j < y; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
}

void print2(int(*p)[5], int x, int y)
{
	int i = 0;
	for (i = 0; i < x; i++)
	{
		int j = 0;
		for (j = 0; j < y; j++)
		{
			printf("%d ", *(*(p + i) + j));
			//*(p+i)其实得到的是这一行的首元素的地址
			//+i表示的是跳过整整一行的元素
			printf("%d ", (*(p + i))[j]);
			//*(p+i)拿到这一行的数组名,p放的就是&arr,*p则为arr
			printf("%d ", p[i][j]);
//二维数组比较特殊*(p+i)其实相当于p[i],*(p[i]+j)相当于p[i][j]
   	}
		printf("\n");
	}
}
int main()
{
	int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
	print1(arr, 3, 5);
	print2(arr, 3, 5);
	return 0;
}

代码解析:
1.知识点 我们知道数组名代表首元素地址,当二维数组的数组名被当作参数传递时,我们可以把二维数组想象成多个一维数组的集合,则每一个一维数组相当于这个二维数组的每一个元素,这样来看二维数组名其实就是第一行一维数组的地址,所以我们就可以用数组指针来接受这个参数
2.知识点 * (*(p+i)+j)这个代码有很多人是比较难理解的,我们重点来讲解一下这个代码,由知识点1可以知道p代表第一行数组的地址,那么如果我们对这个地址进行±整数的话,那它是不是就跳过一行数组的地址了,(因为指针的类型决定指针±整数后,跳过字节的个数嘛) ,所以我们可以这样的先对这个p进行解引用这样就拿到了第一行的数组名了,我们再对这个数组名解引用就拿到二维数组的第一行数组的第一个元素了,之后我们再让这个数组名±整数,那他就可以在第一行数组里面跳来跳去了,我们再次对他解引用,就可以完全拿到第一行数组的每一个元素了。
当我们拿到第一行所有的元素之后,想要拿第二行的数组的元素个数时,我们只要让这个接收二维数组的数组名的指针,也就是p+1,不就好了么。所以我们再第一次解引用的括号中让i从0慢慢变大,这样就能保证每一行的数组的数组名都可以拿得到,最后每一行的数组名再加减整数,再解引用,完全就可以访问到二维数组的所有元素内容了
3.(补充内容)来看几个代码,加深对指针的理解:

int arr[5];
int* parr1[10];
int(*parr2)[10];
int(*parr3[10])[5];

解释1: arr是数组名,这个数组类型就是去掉数组名剩下的部分,比如这个数组的大小是5个int,数组元素类型是int
解释2: parr1是数组名,去掉数组名,剩下的就是数组的类型,比如这个数组的大小是10个int*,数组元素类型是整形指针
解释3: parr2是一个指针,指针所指向的是一个数组,这个数组大小是10个int,每个数组元素类型是int
解释4: parr3是一个数组名,这个数组的大小是10,数组的每个元素类型是一个指针数组,每个指针数组可以存放5个int型的指针
在这里插入图片描述

最后总结:

*(p+i)==p[i];//切记,我们这里拿到的是每一行的数组名
*(*(p+i)+j)==p[i][j];
*(p[i]+j)==p[i][j]

其实由上面的代码,我们也可以感觉到,指针和数组似乎冥冥之中有一种联系,他们似乎总是可以相互表示,代码真是神奇哈🧐

3.函数指针

3.1概念的引入

#include <stdio.h>
void test()
{
 printf("hehe\n");
}
int main()
{
 printf("%p\n", test);
 printf("%p\n", &test);
 return 0;
}

在这里插入图片描述
其实从这里,我们可以看出一些端倪,就是如果当我们想要传一个函数的地址,调用这个函数时,我们可以不用去传&函数名,直接传函数名就OK了

3.2函数指针的写法和有关代码阅读

我们接下来,先写一个函数指针,存放一下函数的地址,之后再阅读一个较难的代码

void put()
{
	printf("hello world!");
}
int main()
{
	put();
	void(*ps)() = put;
	return 0;
}

我们阅读这段代码的方法其实可以类比,指针部分第二个数组指针中的方法。
解释: *ps是一个指针首先,这个指针指向的对象是一个函数。这个函数的返回类型是空,参数类型是无
下面来看两端代码,提高我们的解读能力

代码1
(*(void (*)())0)();
代码2
void (*signal(int , void(*)(int)))(int);

代码1: 我们知道指针去掉名字,剩下的就是指针类型,道理相同,void(*)()其实就是一个函数指针的类型,具体指向的函数是返回类型是空,无参数类型,那么在0之前的括号里面放一个函数指针的类型,那其实就是强制类型转换,将0这个整型转换为函数指针类型,我们是知道指针类型加上一个名字之后,那么这个名字其实就能代表这个函数的地址。这其实与我们的代码不谋而合,0现在其实就是函数的地址,也是这个函数的函数名。或许这里很多人会感觉很不舒服,0怎么就是函数的地址了呢?其实你可以这么想,当0的类型是函数指针类型时,我们可以想一下,什么东西可以拥有类型呢?(例如:指针去掉指针名剩下的就是指针类型,数组去掉数组名剩下的就是数组类型,由此我们可以看出去掉什么东西,剩下的就是类型呢?答案显而易见,也就是去掉名字)所以名字才有类型,0都有函数指针类型了,那么0其实就是函数名 然后,对函数名进行解引用操作,拿到这个函数,对这个函数进行调用,又因为这个函数是无参数的,所以调用这个函数时,是不对这个函数进行传参的,那么最右边的括号里面是什么东西都没有的
代码2: 我们知道一个函数共有3个组成部分,分别是,函数名,函数参数,函数返回类型,当我们看到signal后面有个括号时,我们其实就可以猜到,这里其实就是调用了一个名叫signal的函数,这个函数的参数是int和函数指针类型,而且这个函数的返回类型也是一个函数指针类型

3.3最后的一小部分的补充练习

int (*p)(int a, int b);  
int* p(int a, int b);    

代码1: p是一个指针变量这个指针所指向的是一个函数类型,这个函数的返回类型是int参数分别也是两个int
代码2: p这里是一个函数名,这个函数的返回类型是int型的指针,函数的参数是两个int

4.指向函数指针数组的指针

4.1概念解释:

这里的这个指针其实也就是个地址而已** 永远记住指针就是地址,地址就是指针** ,那这个地址究竟是什么呢?它其实就是个数组的地址,这个数组里面的每个元素都是函数指针,概念就是这么简单,我们只要将他层层剥离即可完美得到概念
//下面我们来写一段相应的代码,以便加深我们对于概念的理解和掌握

4.2上代码

void test(const char* str)
{
 printf("%s\n", str);
}
int main()
{
 1.void (*pfun)(const char*) = test;
 2.void (*pfunArr[5])(const char* str);
 3.pfunArr[0] = test;
 4.void (*(*ppfunArr)[5])(const char*) = &pfunArr;
 return 0;
}

代码1 首先*pfun是一个指针,这个指针的类型是一个函数指针,指向的函数是一个返回类型为void,参数类型为const char 的一个函数,这个指针中存放了test函数的地址
代码2 首先pfunArr是一个数组,这个数组的大小是5,数组的每个元素是函数指针,每个指针所指向的函数类型为返回类型是void,参数类型是const char

代码3 将test函数的地址放到函数指针数组的第一个元素里面,使这第一个元素指向的函数是test函数
代码4 将函数指针数组的地址放到指向函数指针数组的指针当中,**这里的指针是比较难写的,如果直接写不好写的话,我们可以像下面这样,将函数指针数组的数组名替换为(指针)即可 ,这样来写,就不怕我们把这个指针给写错了

void(*pfunArr[5])(const char* str);
void (*(*ppfunArr)[5])(const char*) = &pfunArr

二、传数组和指针时,函数的参数设计

1.牢记以下重要的东西

很重要的知识要记住:我们要牢记,当传数组或指针到函数里面时,实际上传过去的是地址!地址!地址!一定要记住了😡😡😡
也是很重要的知识:我们再设计函数参数来接收地址时,有两个选择,你觉得哪个方便就用哪个,一个选择是用指针接收地址,另一个选择是,就用它本身去接收他自己(这个非常重要,因为我怕你在平常阅读代码的时候,由于这个知识点的缺失,从而看不懂一些代码)

2.一维数组传参

void test(int arr[])正确,用它本身去接收,大小可以不用写
{}
void test(int arr[10]) 完全正确,用它本身去接收
{}
void test(int*arr) 正确,传过来int数据的地址,我用一级指针接收
{}
void test2(int*arr[20]) 正确,完全用它本身去接收
{}
void test2(int**arr) 正确,用二级指针接收传过来的一级指针
{}
int main()
{
	int arr[10] = { 0 };
	int* arr2[20] = { 0 };
	test(arr);
	test2(arr2);
	return 0;
}

3.二维数组传参

void test(int arr[3][5]) 完全正确,用它自己接收自己
{}
void test(int arr[][])不正确,行可以省略,但列是不可以省略的
{}
void test(int arr[][5])正确,没有省略列,用它自己接收自己
{}
void test(int *arr)二维数组数组名,是首行数组的地址,应该数组指针来接收而不是整形指针
{}
void test(int*arr[5])这是拿了个整型指针数组接收地址来了,地址必须用指针接收,完全错误
{}
void test(int(*arr)[5]) 这个完全正确,用指针数组来接收二维数组的首行数组的地址
{}
void test(int**arr) 二级指针接收是没必要,这里又不是传一级指针过来,人家就传个地址而已
{}
int main()
{
	int arr[3][5] = { 0 };
	test(arr);
	return 0;
}

4.一级指针传参

void print(int* p, int sz)//接收地址,用指针来接收
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d\n", *(p + i));
	}
}
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;
	int sz = sizeof(arr) / sizeof(arr[0]);
	print(p, sz);
	//一级指针p(里边放着首元素地址),传给函数print
	return 0;
}

这里面就用到了我们的“牢记一下重要东西的内容了”,我们用它本身去接收他自己
所以当函数参数是一级指针时,它能接收什么东西呢???😢
1.可以接受它本身,(也就是一个存放某个元素地址的一级指针)
2.可以接收某个变量的地址

5.二级指针传参

void test(int** ptr)
{
	printf("num=%d\n", **ptr);
}
int main()
{
	int n = 10;
	int* p = &n;
	int** pp = &p;
	test(pp); 
	test(&p); 
	return 0;
}

这里面还是用到,我们所牢记的内容了
所以当函数参数是二级指针时,它能接收什么东西呢???😢
1.可以接收他本身,(也就是一个存放一级指针地址的二级指针)
2.可以接收某个一级指针变量的地址
3.也可以接收指针数组的数组名

三、数组部分

1.指针数组

这个是真没什么可说的了,简直太简单了,我们随便起个数组名加上个数组大小,再加个数组元素类型就好了嘛,这么简单,就不说了

 int* arr1[10]; 这个是整形指针数组
 char* arr2[4]; 这个是字符指针数组
 char** arr3[5]; 这个是二级字符指针数组

2.函数指针数组

2.1概念的引入

在我们熟练掌握数组的三元素组成后,如果我们想要写一个函数指针数组的话,其实非常简单,我们只需要将数组元素类型写成函数指针类型就可以

例如:int(*parr[5])(char*)

像上面的数组的元素类型就是,int( * )( char * )那这其实就是一个函数指针类型,指向的函数的返回类型是int,参数是char*

2.2函数指针数组的具体使用场景

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(*parr[4])(int, int) = { Add,Sub,Mul,Div};
	int i = 0;
	for (i = 0; i < 4; i++)
	{
		printf("%d\n", parr[i](2, 3));
	}
	
	return 0;
}

功能所在:上面代码其实可以初步向我们展示函数指针数组的功能,他其实就是方便在当我们调用多个函数时,省去我们重复写调用函数的代码,如果有函数指针数组的话,我们直接访问函数指针数组的元素内容就可以了,然后向我们得到的函数名传相应的参数就可以了
下面给大家做一个功能较完整的函数指针数组的使用吧,上面的代码只能对这个数组的使用简单介绍一下,下面我们来完整的实现一下这个数组吧!!!

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()
{
	menu();
	int input = 0;
	int x = 0;
	int y = 0;
	int (*(parr)[4])(int, int) = { Add,Sub,Mul,Div };
	do
	{
		printf("请输入您要选择的菜单的序号:>");
		scanf("%d", &input);
		if (input == 0)
		{
			printf("退出程序");
		}
		else if (input <= 1 && input <= 4)
		{
			 printf("请输入两个操作数:>");
			 scanf("%d %d", &x, &y);
			 printf("%d\n", parr[input - 1](x, y));
			 printf("请继续选择输入您要的序号:>");
		}
		else
		{
			printf("输入错误,请重新输入");
		}
	} while (input);
	
	return 0;
}

四、回调函数

1.回调函数的概念(用函数指针调用的函数)

概念:回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

我们直接大白话给他翻译成通俗易懂的语言。怎么样就是回调函数呢?就比如你现在有一个可以实现两数之和功能的函数Add,你明明可以在main函数里面直接调用这个函数,给他传上两个整数的参数,让他返回和的值。但是,什么叫回调函数呢?其实就是你稍微拐了个弯儿,你把这个函数作为参数传递给一个Calc函数,然后Clac函数的参数被设计成为一个指向Add函数的函数指针,然后我们在Calc函数中,用接收Add函数的函数指针p(假设指针的名字是p)重新调用Add函数,这时Add函数就被称为回调函数

2.回调函数的使用场景

2.1使用场景一:

我们先用上面那个代码,来应用一下回调函数的使用
如果我们想要实现加减乘除这些函数功能的实现,除了上方写一个函数指针数组来实现,也还可以用switch的语句来实现,例如下面的代码:

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;
	do
	{
		menu();
		printf("请选择:>\n");
		scanf("%d", &input);
		 
		switch (input)
		{
		case 1:
			printf("请输入两个操作数:>\n");
			scanf("%d %d", &x, &y); 
			printf("%d\n", add(x, y));	 
			break;
		case 2:
			printf("请输入两个操作数:>\n");
			scanf("%d %d", &x, &y);
			printf("%d\n", sub(x, y));
			break;
		case 3:
			printf("请输入两个操作数:>\n");
			scanf("%d %d", &x, &y);
			printf("%d\n", mul(x, y));
			break;
		case 4:
			printf("请输入两个操作数:>\n");
			scanf("%d %d", &x, &y);
			printf("%d\n", div(x, y));
			break;
		case 0:	
			printf("退出程序\n");
			break;
		default:
			printf("选择错误,重新选择\n");
			break;
		}
	} while (input);
	return 0;
}

通过观察,我们其实可以看到发生了代码冗余的问题,因为在每一条case语句中,我们都出现了printf(“请输入两个操作数:>\n”);scanf(“%d %d”, &x, &y);printf(“%d\n”, div(x, y));这三串代码,那么其实我们可以封装一个函数减少多余代码冗余的问题,那么我们就可以像下面这样来实现

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 Calc(int(*ps)(int , int ))
{
	int x = 0;
	int y = 0;
	printf("请输入两个操作数:>\n");
	scanf("%d %d", &x, &y);
	printf("%d\n", ps(x, y));
}
int main()
{
	int input = 0;
	int x = 0;
	int y = 0;
	do
	{
		menu();
		printf("请选择:>\n");
		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");
			break;
		default:
			printf("选择错误,重新选择\n");
			break;
		}
	} while (input);
	return 0;
}

如果我们需要一个函数Calc实现4种函数功能的话,那这个函数参数就必须是函数指针,我们在Calc函数里实现了四种运算法则的函数,这四种函数就是回调函数

2.2使用场景二:

void print( const char* str)
{
	printf("%s\n", str);
}
void test(void(*p)( const char*))
{
	p("I LOVE YOU");//将字符串首字符地址传过去了
}
int main()
{
	test(print);
	return 0;
}

这里我们没有直接调用函数print,而是通过test函数的参数(也就是函数指针)来接收print函数的地址,在test函数里面将print函数实现真正的调用,那么这种机制就被称为函数回调的机制,print函数就是回调函数

2.3使用场景三qsort函数:

我们先介绍一下,qsort函数如何使用吧😁
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

parameters是参数的意思,第一张图片向我们介绍了,qsort函数的返回类型和参数类型,第二张图片向我们介绍了各种参数所代表的意思
base是你要排序的数组的起始地址,num是数组所有元素的大小(而不是所有元素的字节的大小),width是数组每个元素的字节大小,最后一个参数是一个函数指针,用来接收你传过去的函数名(也就是函数地址)第三张图片是我们所设计的函数的功能,他只要负责返回大于0或小于0或等于0的数字就完全OK了
下面通过代码,来演示一下qsort函数的使用

int cmp(const void* p1, const void* p2)
{
	return (*(int*)p1 - *(int*)p2);
}
int main()
{
	int arr[10] = { 1,3,5,7,9,2,4,6,8,0 };
	int i = 0;
	qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(arr[0]), cmp);
	for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		printf("%d ", arr[i]);
	}
	
	return 0;
}

在这里插入图片描述
我们完美的实现了,qsort函数的使用**(如果你阅读到了这里,小编真心佩服你,你一定能拿到理想的offer的,我们一起加油,耶耶耶🤩🤩🤩)**

2.4自己实现一下qsort函数

struct stu
{
	char name[20];
	int age;
	char sex[5];
	int tele[12];

};
int cmp_by_int(void* buf1, void* buf2)
{
	return (*(int*)buf1 - *(int*)buf2);//需要把无类型指针转换为int*的指针,因为我们需要返回一个整数(>0,<0,=0这三种)
}
int cmp_by_float(void* buf1, void* buf2)
{
	return (*(int*)buf1 - *(int*)buf2);
}
int cmp_by_struct_by_age(void* buf1, void* buf2)
{
	return ((struct stu*)buf1)->age - ((struct stu*)buf2)->age;
}
void swap(char* e1, char* e2,int width)
{
	int i = 0;
	for (i = 0; i < width; i++)
	{
		char tmp = '1';
		tmp = *e1;
		*e1 = *e2;
		*e2 = tmp;
		e1++;
		e2++;
	}

}
void my_qsort(void* base, int num, int width, int(*ps)(void* elem1, void* elem2))
{
	//确定冒泡排序的趟数
	int i = 0;
	
	for (i = 0; i < num; i++)
	{
		int j = 0;
		//确定每一趟需要排序的元素对的对数
		for (j = 0; j < num - 1 - i; j++)
		{
			//确定是否要进行交换元素,也就是是否要排序
			if (ps((char*)base + j*width, (char*)base + (j+1)*width) > 0)
			//不同元素类型的字节宽度是不一样的,所以传j*width,不同元素传过去的地址大小是不同的
			{
				//进行交换元素
				swap((char*)base + j*width, (char*)base + (j+1)*width, width);
				//我们在进行交换元素时,其实道理和上面的if判断条件是相同的,我们传过去的地址大小也是无法确定的
				//所以要传他字节宽度的整数倍
			}
			else
			{
				;
			}
		}
	}
}

void test3()
{
	struct stu s1[] = { {"zhangsan",20,"man",15598303778},
		{ "wangwu",30,"women",13232746588 },
		{ "lisi", 40, "man", 13231244563 } , };
	my_qsort(s1, sizeof(s1) / sizeof(s1[0]),sizeof(s1[0]), cmp_by_struct_by_age);
}
void test2()
{
	float arr2[10] = { 1.0,3.0,5.0,7.0,9.0,2.0,4.0,6.0,8.0,10.0 };
	my_qsort(arr2, sizeof(arr2) / sizeof(arr2[0]), sizeof(arr2[0]), cmp_by_float);
	int i = 0;
	for (i = 0; i < sizeof(arr2) / sizeof(arr2[0]); i++)
	{
		printf("%f ", arr2[i]);
	}
}
void test1()
{
	int arr1[10] = { 1,3,5,7,9,2,4,6,8,0 };
	my_qsort(arr1, sizeof(arr1) / sizeof(arr1[0]), sizeof(arr1[0]), cmp_by_int);
	int i = 0;
	for (i = 0; i < sizeof(arr1) / sizeof(arr1[0]); i++)
	{
		printf("%d ", arr1[i]);
	}
	printf("\n");
}
int main()
{
	test1();
	test2();
	test3();
	return 0;
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
欧克,从上面的四张图片,我们就可以看出,my_qsort函数完美的成功实现,小编在自己实现这个函数的过程中,遇到了4个bug,每个都能让我生不如死😣😣😣
下面给大家,说说我遇到的bug吧,这样你们在自己编写代码时,就可以不用犯小编遇到的错误了,更加节省你们的时间,少走些弯路,嘻嘻🤭🤭🤭
1.结构体的声明放在使用结构体函数的下面,一定要把类型声明放在cmp_by_struct_age函数声明的上面
2.my_qsort函数内部的for循环结构的判断条件要设置好
3.my_qsort函数内部,我们再回调函数时,要注意传过去的地址,因为不同元素单个个体的地址大小是不同的,所以我们要用下面这样的传地址方式

swap((char*)base + j*width, (char*)base + (j+1)*width, width);
if (ps((char*)base + j*width, (char*)base + (j+1)*width) > 0)

4.my_qsort函数的第二和第三个参数分别是,数组的元素个数(记住是元素个数,比如一个结构成员,一个浮点数,一个整型,都是一个元素)和单个元素的字节大小(记住是字节大小,也就是1,2,3,4这些大小,是整数)

  • 19
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 22
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

rygttm

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

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

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

打赏作者

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

抵扣说明:

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

余额充值