【C语言】指针进阶

本文详细探讨了C语言中的字符指针、字符指针数组、整型指针数组以及数组指针的概念和使用,包括指针与数组的关系,函数指针的运用,以及qsort库函数和冒泡排序算法的实现。同时,文章通过示例代码解释了sizeof和strlen在不同情况下的行为差异,并提供了笔试题的分析。
摘要由CSDN通过智能技术生成

在这里插入图片描述

欢迎来到Cefler的博客😁
🕌博客主页:那个传说中的man的主页
🏠个人专栏:题目解析
🌎推荐文章:题目大解析(更新ing)

在这里插入图片描述


👉🏻字符指针

字符指针:即创建一个char指针类型的变量,来存放字符
指向字符的指针

在这里插入图片描述
为什么上图中会发出这样的错误呢?
实际上,这里p指向的是字符串首元素的地址,即a的地址
那么为什么对其不能进行修改呢?因为a是一个常量,而常量是不能被更改的。
在这里插入图片描述

但是如果将字符串存进数组当中,解引用得到的字符就可以进行修改,因为此时的字符是变量。

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

在这里插入图片描述
str1和str2其实就是分别创建了两个数组,它们的地址是不相同的,无论它们的内部存放的是什么数据。
str3和str4则是将指针指向了常量"hello boy"中首元素h的地址,而h的地址唯一,所以str3和str4的地址相同。

👉🏻字符指针数组

字符指针数组:即创建一个char指针类型的数组存放字符

在这里插入图片描述
在字符指针中存放是怎样的呢?
arr[0],arr[1],arr[2]其实分别指向了字符串首元素的地址,即’y’、‘a’、'h’的地址。
在这里插入图片描述

所以在打印的时候,若要打印整个字符串,只要找到首元素的地址,就可以顺藤摸瓜打印剩下的字符。

整型指针数组

int main()
{
	int arr1[] = { 1,2,3,4,5 };
	int arr2[] = {4,2,3,7,8 };
	int arr3[] = { 5,4,3,1,9 };
	int* arr[] = { arr1,arr2,arr3 };
	//arr存放的是整型数组(地址)
	int i, j;
	for (i = 0; i < 3; i++)
	{
		for (j = 0; j < 5; j++)
		{
			printf("%d ", *(arr[i]+j));

		}
		printf("\n");
	}

在这里插入图片描述
而*(arr[i]+j)也等价于arr[i][j]

👉🏻数组指针

数组指针:指针指向数组的地址
与指针数组不同的是
指针数组的主要对象是数组,数组中存放指针
而数组指针的主要对象是指针,指针指向数组的地址

int arr[10];
int* p[10]=&arr;//&arr取出的是数组的地址

如上代码int* p[10]=&arr是数组指针吗?答案为,不是
实际上这里应该叫int* p[10]为整型指针数组,也就是说,p[10]是一个存放整型指针的数组罢了。
因为这里p与[]结合了,所以p就不是指针,而是一个数组了
但是我们要的是一个指针,来指向数组的地址,那么应该如何纠正呢?

int arr[10];
int (*p)[10]=&arr;

我们这里对p单独用括号括起来,将其与*绑定在一起,这个时候,p被明确指明了就是一个指针,所以此时p指向的就是数组的地址,后面的[10]和前面的int现在代表的含义分别是所指向的数组的元素数量和类型罢了

所以数组指针,我们只需要对变量单独括号起将其与*绑定,将其声明为指针这个身份。
剩下的类型和[]其实就是所指向的数组的元素类型和元素数量

👉🏻&数组名和数组名

我们在数组章节中知道
&数组名取的是数组的地址,数组名是数组首元素的地址
在这里插入图片描述
但为什么上图中二者打印的地址结果相同?
实际上,&arr在输出出来的地址是数组首元素的地址,但它的本质是是数组的地址,只是表达形式和输出arr的地址一样罢了

在这里插入图片描述
我们可以看到,如果分别对&arr和arr地址+1.
前者跨越的地址长度是40字节,而后者为4字节,因为前者跨越的字节长度决定于数组地址的长度。
而其实在上文中我们了解到了数组指针。
&arr其实就是一个数组指针int (*) [10] ,指向类型为int,元素为10个的数组
数组的地址+1,跳过整个数组大小。

👉🏻数组指针的运用

在这里插入图片描述
如图上我们创建了一个函数来打印二维数组
在这里,我们对函数print传入了arr,在这里,arr是什么呢?
arr代表的是二维数组第一个元素的地址,而二维数组的第一个元素是第一行的一维数组。
所以这里arr的地址其实就是一个数组地址.

void print(int arr[3][5], int row, int col)

那么此时我们再来分析函数形参中的参数里,能不能做些什么修改呢?
既然我们传的是一个数组地址,那么我们是不是就可以用一个数组指针类型的变量来接收我们的数组地址。
所以我们可以改成这样👇🏻

void print2(int (*arr)[5], int row, int col)
{
	int i = 0, j = 0;
	for (i = 0; i < row; i++)
	{
		for (j = 0; j < col; j++)
		{
			printf("%d ", (*arr + i)[j]);
			}
		printf("\n");
	}
}

打印效果👇🏻
在这里插入图片描述
这里打印的时候,对数组指针arr解引用得到数组地址,所以我们对arr进行加+1可以得到第二行一维数组的地址。
而在这里 (*arr + i)[j]就相当于arr[j],(arr + i)作为数组地址,代表着整个数组。
那么既然(arr + i)[j]和arr[j]表达的意思一样
我们平常在打印
(arr+i)时,是逐渐打印数组的各个元素,是否可以用
((*arr + i)+j)逐渐打印数组的各个元素呢?
在这里插入图片描述
如图看,显然是可以的。
我们其实由此就可以作个简单的小结论

对于一个数组指针进行解引用后,得到的是数组地址,本质是一个地址。
它其实可以等价于普通数组arr[]左边的arr,作用上无论是解引用还是下标引用实现的效果和arr都一样

数组指针判断

int arr[5];//整型数组
int *parr1[10];//整型指针数组
int (*parr2)[10];//指向整型数组的指针
int (*parr3[10])[5];//这个代码是什么意思呢?

首先,我们可以看到parr3右邻[],这个就说明了parr3是一个数组,而后再看parr3左邻*,说明parr3是一个指针数组(存放指针的数组)。
所以总体意思就是,prr3是一个数组,数组中存放着指针,而数组中的指针指向的是一个类型为Int,元素数量为5的数组。
因为prr3的数组元素数量为10,即有10个指针,所以这里指向了10个类型为Int,元素数量为5的数组。

👉🏻数组参数、指针参数

一维数组传参

一、

int main()
{
   int arr[10] = {0};
   test(arr);
}

在创建一个test函数时,能创建哪些参数形式来接受arr呢?
可以有

void test(int arr[])
{}
void test(int arr[10])
{}
void test(int *arr)
{}

二、

int main()
{
   int *arr[10] = {0};
   test(arr);
}

此时我们又可以创建什么参数形式来接受呢?我们首先得明确,此时arr为一个指针数组,arr取的地址是首元素的地址,而这里的元素为指针,注意,为指针,即arr取的是指针的地址,一个指针的地址,我们若要将其接受存放,则要么需要一个指针数组再来存放,要么就是个二级指针来存放,所以有👇🏻

void test2(int *arr[20])
{}
void test2(int **arr)
{}

二维数组传参

int main()
{
 int arr[3][5] = {0};
 test(arr);
}

如上一个二维数组,我们可以创建一个怎样的参数形式来接受呢?
首先我们得先明白arr此时取的是什么地址。
在学过二维数组的知识后,我们易知,arr取的是整型数组地址
那么数组地址能被怎样存放呢?
若不是指针类型的,我们一般有常规传参形式(按照数组原本的模样进行传参)

void test(int arr[3][5])
{}
void test(int arr[][5])//记得列不能省略
{}

而如果要以指针的形式传参,我们则需要一个指向数组的指针来存放数组地址,即有
👇🏻

void test(int (*arr)[5])
{}

一级指针传参

int main()
{
 int arr[10] = {1,2,3,4,5,6,7,8,9};
 int *p = arr;
 int sz = sizeof(arr)/sizeof(arr[0]);
 //一级指针p,传给函数
 print(p, sz);
 return 0;
}

这是传的是一个一级指针类型的参数,我们只要正常用一个一级指针来接收就行了

void print(int *p, int sz)

这个时候我们思考一个问题,当形参部分是一个一级指针的时候,它能接收什么形式的实参呢?

  • 一级指针
  • 数组名(数组首元素地址)
  • &变量名,取该变量的地址

二级指针传参

当一个函数的形参为二级指针的时候,它可以接收怎样的实参形式?

void test(char **p)
  • 二级指针
  • 取一级指针的地址
  • 指针数组的数组名
int main()
{
 char c = 'b';
 char*pc = &c;
 char**ppc = &pc;
 char* arr[10];
 test(&pc);//取一级指针的地址
 test(ppc);//二级指针
 test(arr);//指针数组的数组名
 return 0;
}

👉🏻函数指针

我们都知道:

  • 整型指针:指向整型的指针
  • 字符指针:指向字符的指针
  • 数组指针:指向数组的指针
    所以,我们可以推理出函数指针其实就是
  • 函数指针:指向函数的指针
    我们看一段代码
#include <stdio.h>
void fun()
{

}
int main()
{
	printf("%p\n", fun);
	printf("%p\n", &fun);

	return 0;
}

![在这里插入图片描述](https://img-blog.csdnimg.cn/093ad1aed74148be860bb15d88805ec5.png
我们可以看到函数名和取地址函数打印的结果相同。
我们就可以得到一个结论:函数名和取地址函数代表都是函数的地址,没有区别
所以这个时候我们就又会引出一个问题:函数地址该怎么被存放?🤔
如下👇🏻👇🏻👇🏻

int fun(int x,int y)
{

}
int main()
{
	int (*p)(int,int)=fun;

	return 0;
}

int (*p)(int,int)作为一个函数指针变量接收函数fun的地址。这个函数指针变量我们拆解成三部分来分析下

  • (*p):强调了p是一个指针
  • int:这是说明了所指向的函数的返回类型
  • (int,int)说明了所指向的函数的参数类型

由此我们得出函数指针变量是由这三部分的内容组成,只有这三部分都齐全后,我们才可以称p为一个函数指针

函数指针调用

在这里插入图片描述
如上图,我们通过p找到函数地址,然后*解引用调用函数,然后对函数传入实参(2,4),最后就成功调用了函数的功能了。
但我们其实知道,p和函数名add其实是一样的,都是地址。
还记得我们之前调用函数的时候都是这样的👇🏻

int ret=add(2,4);

既然地址()这种形式就能调用函数,那么与函数名等价的p,我们也就可以直接以p(2,4)的形式直接打印
在这里插入图片描述
所以我们就会用,那为什么还要有(p)这样解引用再使用的写法呢?
其实这样的写法主要是为了说明p是一个指针,我们如果要加 ,必须要用p后要用括号括起来,如果不用()括起来,那么p左邻
号,右邻括号,p会被当做为函数名处理,而就不是指针了。

《有趣的》代码分析😏

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

代码1分析:
我们将代码1用空格分隔出它的组成部分

(*( void (*)() )  0)();

void (*)()其实就是个函数指针类型,它放在()中的作用就是将0从整型强制类型转换为函数指针类型。
然后现在就变为了
(*函数指针类型)(),这个是不是就很眼熟了?不就类似于我们上面的(*p)(2,4)吗
所以这串代码的本质就是一个调用函数的代码
代码2分析:
我们其实可以从里面提取出👇🏻

signal(int , void(*)(int))

这个是什么呢,我们定睛一看,原来声明的是一个函数,里面的两个参数类型分别为整型和函数指针。
一个函数,有了函数名和参数之后,它还差什么呢?我们就会想到,还差了返回类型
所以当我们把signal(int , void(*)(int))从代码2中去除掉后剩下的代码为👇🏻

void (*)(int);

剩下的就是一个函数指针,这就说明了该函数的返回类型就是个函数指针
至于为什么函数要放在(*)中让人这么难看懂,我只能说,语法规定,理解就好🤧
里面的类型很复杂,能不能将代码简化点呢?

代码2 typedef重新定义函数指针名

typedef void(*)(int)  fun1;

哎,我们这里将void()(int) 定义为了一个名称为fun1,以后我们书写代码时,fun1就可以代表void()(int)类型,这样是不是就大大增大了代码的可读性呢?
但是这里请注意!这样的写法是错误的!
正确写法应为👇🏻

typedef void(*fun1)(int)  ;

语法规定:对含有*的类型进行重定义名时,重命名需靠 *进行命名才行。
因此,经过重命名后,代码2可以简化为👇🏻

fun1  (*signal)(int , fun1);

小练习✍🏻

声明一个指向含有10个元素的数组的指针,其中每个元素是一个函数指针,该函数的返回值是int,参数是int*,正确的是( )
A.(int p[10])(int)
B.int [10]*p(int )
C.int (
(*p)[10])(int *)
D.int ((int *)[10])*p

看到这种题不用慌,我们逐步来分析下。
我们阅读题目,我们明确了题目要求的第一个点,就是要一个指针
那好,我们先声明好一个指针(*p)
这时再看题目要求,要求这个指针指向的是一个含有10个元素的数组
于是有(p)[10]
其实写到这,我们不用再看题目,都能知道题目接下来要我们求什么,我们这里既然已经指向了一个数组,数组的元素数量既然已经出来了,现在还差的就是数组元素类型了
所以这时我们再看题目,果然,告诉我们元素类型是一个函数指针
而这个函数指针是什么样的呢?返回值是int,参数是int
题目也都有说明
这个时候我们就可以写出这个函数指针为:int ( *)(int *)
所以我们就可以写成

int ( *)(int *)*p)[10];

如果这时以为就大功告成的话,那就大错特错了。
这种写法是错误的!
还记得上文里代码2里的写法吗,也是出现了类似的情况。
这种错误的写法从语法理解上很好理解。
但如果正确的写法,应该将(*p)[10]放进( * )中,即👇🏻

int (**p)[10])(int *)  ;

这个时候我们一看选项,只有C符合,故答案为C选项。

👉🏻编写一个计算器小程序📱

版本一(常规)

void menu()
{
	printf("**********************************\n");
	printf("*********1.加法  2.减法***********\n");
	printf("*********3.乘法  4.除法***********\n");
	printf("*********   0.退出程序 ***********\n");

}
void add()
{
	int x, y;
	printf("请输入你要计算的数字:");
	scanf("%d %d", &x, &y);
	printf("%d\n", x + y);
}
void sub()
{
	int x, y;
	printf("请输入你要计算的数字:");
	scanf("%d %d", &x, &y);
	printf("%d\n", x - y);
}void mul()
{
	int x, y;
	printf("请输入你要计算的数字:");
	scanf("%d %d", &x, &y);
	printf("%d\n", x * y);
}void div()
{
	int x, y;
	printf("请输入你要计算的数字:");
	scanf("%d %d", &x, &y);
	printf("%d\n", x / y);
}
int main()
{
	int input = 0;
	do
	{
		menu();
		printf("请输入:");
		scanf("%d", &input);
		switch (input)
		{
		case 1: add();
			break;
		case 2:sub();
			break;
		case 3:mul();
			break;
		case 4:div();
			break;
		default:printf("退出程序\n");
			break;

		}

	} while (input);
	return 0;
}

在这里插入图片描述

版本二——函数指针数组

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 (*p[4])(int, int) = { add,sub,mul,div };
	int i = 0;
	int x, y;
	printf("请输入你要进行计算的数字:");
	scanf("%d %d", &x, &y);
	for (i = 0; i < 4; i++)
	{
		printf("%d\n", p[i](x, y));
	}
	return 0;
}

我们这里创建了一个函数指针数组,用来存放函数,p作为函数指针变量名,我们在上述已经说过,它的本质是一个指针(地址)
我们在要调用数组中的函数时,如果加*号,则需要用()括起来,像(*p[i]),要么不加,则就是p[i],直接变量名就行。
在这里插入图片描述
*多少其实不重要,主要能够强调p是个指针就行。

版本一以函数指针数组方式优化

void menu()
{
	printf("**********************************\n");
	printf("*********1.加法  2.减法***********\n");
	printf("*********3.乘法  4.除法***********\n");
	printf("*********   0.退出程序 ***********\n");

}
void add()
{
	int x, y;
	printf("请输入你要计算的数字:");
	scanf("%d %d", &x, &y);
	printf("%d\n", x + y);
}
void sub()
{
	int x, y;
	printf("请输入你要计算的数字:");
	scanf("%d %d", &x, &y);
	printf("%d\n", x - y);
}void mul()
{
	int x, y;
	printf("请输入你要计算的数字:");
	scanf("%d %d", &x, &y);
	printf("%d\n", x * y);
}void div()
{
	int x, y;
	printf("请输入你要计算的数字:");
	scanf("%d %d", &x, &y);
	printf("%d\n", x / y);
}
int main()
{
	int input = 0;
	do
	{
		menu();
		printf("请输入:");
		scanf("%d", &input);
		void (*p[5])() = { NULL,add,sub,mul,div };
		p[input]();

	} while (input);
	return 0;
}

在这里我们抛去了switch 语句
直接创建一个函数指针数组来存放各个功能的函数
void (*p[5])():
*p[5]一个存放指针的数组,数组中的指针指向着类型为无,返回类型为void的函数。
我们在调用函数的时候,要么:

  • 函数指针变量名[input](),因为是无类型,所以括号内为空
  • (*函数指针变量名[input])()

二者任选其一都可以调用到函数。效果如下👇🏻👇🏻👇🏻
在这里插入图片描述

👉🏻指向函数指针数组的指针

#include <stdio.h>
int (*p[])(int);//指向一个参数为int,返回类型为int的函数

首先这里给出了一个函数指针数组,这个我们已经学习过了。
但是我们现在需要一个指向函数指针数组的指针。
我们该如何创建这个指针呢?
我们要像逐层剖析这个语句的每一部分
1.指针,既然它是指针,所以我们先声明一个指针👇🏻

(*p)

2.指向函数指针数组,我们得关注重点,这是一个数组,所以我们知道这个指针是指向一个数组的,所以有

(*p)[]

3.在我们的逐层剖析下,都已经到数组了,此时还缺什么?不用题目说我们都知道是数组元素类型,再看下题目要求的类型,噢,原来是元素是函数指针类型(我们这里就浅设置这个函数指针所指向的函数参数为int,返回类型为int)

int (*)(int);

这下好了,往事俱备,开始组合

int (*)(int)  (*p)[]; 

如果这样就完了,那么恭喜,书写语法错误
我们上文中已经出现两次这种书写错误的案例,正确的书写应为👇🏻

int (*(*p)[])(int);    

我们可以用错误写法帮我们理解,但是在书写时,必须按规定的格式来写才可以。

👉🏻回调函数

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

🪐 qsort库函数

冒泡排序想来大家都不陌生,这是一个很经典的排序算法。
但每次如果我们想给一段数字排序,都要写一段这样的冒泡排序是不是很麻烦,而且如果我们要比较的是字符呢?
所以,这个时候我们有了一个万能的库函数——qsort,它的强大有

  • 直接进行排序
  • 可以对其它类型进行排序
    吹了这么久,那就先让我们看看qsort函数如何使用吧
    在这里插入图片描述
    里面的参数是什么意思呢?👇🏻👇🏻👇🏻
    1.void* base:所要排序的数组的首位元素的地址(指针)
    2.size_t_num:所要排序的元素个数
    3.size_t_size:所排序的元素大小,单位为字节
    4.int ( * compar)(const void *,const void *):
    这个就要着重分析下了,我们可以知道,这是一个函数指针,指向的函数的参数类型为const void *,返回类型为int,也就是说,我们这里需要放一个函数地址而这个函数的作用就是比较两个元素的大小.
    int的返回有3种:>0;=0;<0;
    好了,让我们从实战中认识一下

qsort实战分析

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

我们这里要分析一段代码

return *(int*)p1- *(int*)p2;//返回>0、=0、<0

就为什么我们这里要将p1和p2强制类型转换呢?
其实问这个问题之前,我们还有一个问题:就是为什么qsort里面的比较函数的参数类型一定要是void呢?
其实很简单,我们既然想让qsort实现对多类型的变量的比较,所以,在我们想要进行排序的时候,我们得让qsort知道我们要比较的类型是什么,所以我们设置void
是为了能接收任何类型的指针。但问题在于这样我们就无法对void指针进行解引用(void无具体类型,解引用不知道访问多大空间)。
所以我们需要强制类型转换,比如我们知道我们传的指针类型是整型,我们就将它强制类型转换为整型,就可以进行比较了。
效果如下👇🏻
在这里插入图片描述
那么问题来了,如果我们想要对其降序排序呢,如下👇🏻

return *(int*)p2- *(int*)p1;//将元素位置互换即可

在这里插入图片描述

qsort——结构体排序

比较年龄

struct S
{
	char name[20];
	int age;
};

int compare_age(const void* p1, const void* p2)
{
	return ((struct S*)p1)->age - ((struct S*)p2)->age;
}

int main()
{
	struct S s[] = { {"B",15},{"A",13},{"C",20} };//这里创建一个结构体数组
	int sz = sizeof(s) / sizeof(s[0]);
	//比较年龄
	qsort(s, sz, sizeof(s[0]), compare_age);
	return 0;
}

效果如下👇🏻
在这里插入图片描述

注意,这里结构体声明一定要放在函数上方(血泪)

比较名字(运用到strcmp)

#include <string.h>
int compare_name(const void* p1, const void* p2)
{
	return strcmp(((struct S*)p1)->name, ((struct S*)p2)->name);
	
}
int main()
{
	struct S s[] = { {"B",15},{"A",13},{"C",20} };//这里创建一个结构体数组
	int sz = sizeof(s) / sizeof(s[0]);
	//比较年龄
	//qsort(s, sz, sizeof(s[0]), compare_age);
	//比较名字
	qsort(s, sz, sizeof(s[0]), compare_name);
	return 0;
}

在这里插入图片描述
注意:我们在比较强制类型转换完(struct S*)p1之后,记得要将其()起来

🪐冒泡排序算法实现qsort功能

int cmp_int(const void* p1, void* p2)
{
	return *(int*)p1 - *(int*)p2;
}
void Swap(char* buf1, char* buf2, int width)
{
	int i = 0;
	for (i = 0; i < width; i++)
	{
		int tmp = *(buf1 + i);
		*(buf1 + i) = *(buf2 + i);
		*(buf2 + i) = tmp;
	}
}
void bubble_sort(void* base, size_t sz, size_t width, int(*p)(const void* p1, const void* p2))
{
	size_t i = 0,j=0;//size_t为无符号整数,为了严谨性(因为替换元素数和字节长度不可能为负数)
	//接下来执行冒泡排序的逻辑
	for (i = 0; i < sz - 1; i++)
	{
		for (j = 0; j < sz - 1 - i; j++)
		{
			if (p((char*)base + j * width,(char*)base + (j + 1) * width) > 0)
			{
				//如果满足条件,此时开始交换
				Swap((char*)base + j * width,(char*)base + (j + 1) * width,width);
			}
		}
	}
}
int main()
{
	int arr[10] = { 9,7,8,5,6,4,3,1,2,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

难点分析:
1.

if (p((char*)base + j * width,(char*)base + (j + 1) * width) > 0)

为何这里我们要将base专门强制类型为char*,然后jwidth?
首先,我们不知道一开始到底传输的是什么类型的指针,所以我们强制类型转换这个步骤肯定要执行.
专门设置为char
类型,即此时指针所指向的数组的元素所占空间为1字节,它搭配上jwidth有什么好处呢,举个例子
如果我们对一个整型数组进行排序,那么width的长度就为4字节,(char
)base + j * width这个指针每一次访问所跨越的长度就是为4个字节,这个长度刚刚好可以对应到整型数组中的每一个元素。
我们就是通过这样的设置,可以让我们对不管是什么类型的数组元素,对其的元素都能访问的到。
2.交换

void Swap(char* buf1, char* buf2, int width)
{
	int i = 0;
	for (i = 0; i < width; i++)
	{
		int tmp = *(buf1 + i);
		*(buf1 + i) = *(buf2 + i);
		*(buf2 + i) = tmp;
	}
}

我们上面设置为char*,即此时指针所指向的数组的元素所占空间为1字节,也就是说,如果我们要进行交换,也只能进行1字节大小的交换。
但如果我们要交换的类型为整型(4字节),只交换一次肯定不够,要交换4次
所以我们要交换width次,所以进行了width次循环交换。
此时如果我们想要变为降序👇🏻

if (p((char*)base + j * width,(char*)base + (j + 1) * width) <0)//>0改为<0

💥bubble_sort运算优化

在冒泡排序的算法下,我们对元素进行了分别比较,但是,有一种可能,就是当我们排到了n次序后,已经是有序了,但此时循环还未结束,就增大了多余的计算量。
所以我们这里设置一个flag,如果已经有序,则退出循环

void bubble_sort(void* base, size_t sz, size_t width, int(*p)(const void* p1, const void* p2))
{
	size_t i = 0,j=0;//size_t为无符号整数,为了严谨性(因为替换元素数和字节长度不可能为负数)
	//接下来执行冒泡排序的逻辑
	for (i = 0; i < sz - 1; i++)
	{
		int flag = 1;//我们假设已经有序
		for (j = 0; j < sz - 1 - i; j++)
		{
			if (p((char*)base + j * width,(char*)base + (j + 1) * width) >0)//如果满足条件,此时开始交换
			{
				flag = 0;//如果进行交换了,说明无序
				Swap((char*)base + j * width,(char*)base + (j + 1) * width,width);
			}
		}
		if (flag == 1)
		{
			break;
		}
	}

整体效果👇🏻👇🏻👇🏻
在这里插入图片描述

👉🏻sizeof和strlen与指针和数组的代码分析

一维整型数组(sizeof)

int a[] = {1,2,3,4};
printf("%d\n",sizeof(a));//16
//sizeof(a)为整个数组的总大小
printf("%d\n",sizeof(a+0));//4/8
//a+0这个动作使得a不再是单独存放在sizeof中,它的意义变为,数组首元素地址+0
//即还是首元素的地址,内存大小为4/8(32位地址大小为4,64位地址为8)
printf("%d\n",sizeof(*a));//4
//*对a的首元素进行解引用,即最后为sizeof(1),1为整型,故内存大小为4
printf("%d\n",sizeof(a+1));//4/8,第二元素的地址
printf("%d\n",sizeof(a[1]));//4,sizeof(1)
printf("%d\n",sizeof(&a));//4/8
//取的是数组的地址,但是还是地址,一视同仁,4/8
printf("%d\n",sizeof(*&a));//16
//&a(int(*)[4])得到整个数组的大小,此时对它解引用得到整个数组的大小
printf("%d\n",sizeof(&a+1));//4/8
//&a+1还是地址,一视同仁,4/8
printf("%d\n",sizeof(&a[0]));//4/8
printf("%d\n",sizeof(&a[0]+1));//4/8

实际打印👇🏻👇🏻👇🏻
在这里插入图片描述

一维字符数组(sizeof,无‘\0’)

char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", sizeof(arr));//6
//sizeof(arr)计算的是整个数组的大小
printf("%d\n", sizeof(arr+0));//4/8
//arr+0为数组首元素地址加0,即还是首元素的地址,一视同仁,4/8
printf("%d\n", sizeof(*arr));//1
//等同于sizeof('a'),大小为1字节
printf("%d\n", sizeof(arr[1]));//1
//等同于sizeof('a'),大小为1字节
printf("%d\n", sizeof(&arr));//4/8
//为地址,一视同仁,4/8
printf("%d\n", sizeof(&arr+1));//4/8
printf("%d\n", sizeof(&arr[0]+1));//4/8

实际打印👇🏻👇🏻👇🏻
在这里插入图片描述

一维字符数组(strlen,无‘\0’)

char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", strlen(arr));//随机值
//因为没有\0,所以strlen根本不确定会读到哪里
printf("%d\n", strlen(arr+0));//随机值
//arr+0为数首个元素的地址,strlen从首元素的地址开始读
//但因为没有\0,所以strlen根本不确定会读到哪里
printf("%d\n", strlen(*arr));//出现报错(err),非法访问
//*arr为字符'a',因为strlen只读取地址,'a'的ASCII码值其实就是97
//strlen读取一个为97的地址,会造成非法访问
printf("%d\n", strlen(arr[1]));//err
printf("%d\n", strlen(&arr));//随机值
printf("%d\n", strlen(&arr+1));//随机值-6
printf("%d\n", strlen(&arr[0]+1));//随机值-1

实际打印👇🏻👇🏻👇🏻
在这里插入图片描述

一维字符数组(sizeof,有‘\0’)

char arr[] = "abcdef";
printf("%d\n", sizeof(arr));//7
//sizeof(arr)求的是整个数组的大小,这其中包含了\0,所以总大小为7
printf("%d\n", sizeof(arr+0));//4/8
//arr+0为首元素的地址,既然为地址,一视同仁,4/8
printf("%d\n", sizeof(*arr));//1
//等价于sizeof('a'),大小为1字节
printf("%d\n", sizeof(arr[1]));//1
printf("%d\n", sizeof(&arr));//4/8
printf("%d\n", sizeof(&arr+1));//4/8
printf("%d\n", sizeof(&arr[0]+1));//4/8

实际打印👇🏻👇🏻👇🏻
在这里插入图片描述

一维字符数组(strlen,有‘\0’)

char arr[] = "abcdef";
printf("%d\n", strlen(arr));//6
//strlen从数组首元素地址开始读取
printf("%d\n", strlen(arr+0));//6
printf("%d\n", strlen(*arr));//err,非法访问
//strlen(‘a’)==strlen(97),非法访问地址
printf("%d\n", strlen(arr[1]));//err,非法访问
printf("%d\n", strlen(&arr));//6
//取的是整个数组的地址,但是还是从数组的首元素地址开始访问,所以长度还是6
printf("%d\n", strlen(&arr+1));//随机值
//&arr+1此时指向的地址后不确定\0在哪,所以长度随机
printf("%d\n", strlen(&arr[0]+1));//5
//取数组第二个元素的地址开始访问到\0,长度为5

实际打印👇🏻👇🏻👇🏻
在这里插入图片描述

一维字符数组(sizeof,有‘\0’,指针类型为char *)

char *p = "abcdef";
printf("%d\n", sizeof(p));//4/8
//p为字符串"abcdef"首个字符的地址,但也是地址
//一视同仁,4/8
printf("%d\n", sizeof(p+1));//4/8
printf("%d\n", sizeof(*p));//1
//相当于sizeof('a')
printf("%d\n", sizeof(p[0]));//1
//p[0]相当于*(p+0),也就是sizeof('a')
printf("%d\n", sizeof(&p));//4/8
//&p取的是指针p的地址,一视同仁,4/8
printf("%d\n", sizeof(&p+1));//4/8
printf("%d\n", sizeof(&p[0]+1));//4/8

实际打印👇🏻👇🏻👇🏻
在这里插入图片描述

一维字符数组(strlen,有‘\0’,指针类型为char *)

char *p = "abcdef";
printf("%d\n", strlen(p));//6
//strlen从字符串首元素'a'地址开始读取
printf("%d\n", strlen(p+1));//5
strlen从字符串第二个元素'b'地址开始读取
printf("%d\n", strlen(*p));//err,非法访问
//strlen('a')==strlen(97)
printf("%d\n", strlen(p[0]));//err,非法访问
//*(p+0)为'a'
printf("%d\n", strlen(&p));//随机值
//&p取了指针p的地址,但是不知道\0到底在哪,所以长度随机
printf("%d\n", strlen(&p+1));//随机值
printf("%d\n", strlen(&p[0]+1));//随机值

实际打印👇🏻👇🏻👇🏻
在这里插入图片描述

总结:
1.sizeof只关心其所占内存空间大小,内部一般有

  • 数组名:求整个数组的大小
  • 数组中的某元素
  • 纯整型或纯字符
  • 指针(地址):这个一视同仁为4/8

2.strlen只是决定读取某个地址,从而往后到\0处,为字符长度,内部一般有

  • 数组名:从数组首元素地址开始读
  • 纯元素(整型或字符),会自动都转化为10进制地址:一般都是非法访问
  • 指针(地址),从当初地址往后直到读到\0

🔍二维整型数组(sizeof)

int a[3][4] = {0};
printf("%d\n",sizeof(a));//48
//a取的是整个二维数组的地址,而二维数组的首个元素是个一维数组
//即取的是一个数组的地址,相当于int(*)[4],数组大小为16
//而一共三个这样的元素,所以共为48
printf("%d\n",sizeof(a[0][0]));//4
//a[0][0]相当于*(a+0)[0],*(a+0)对一维数组的地址进行解引用得到整个数组
//*(a+0)为二维数组首元素(数组名),但意义不是首元素地址,只是为arr[],[]旁边的arr
printf("%d\n",sizeof(a[0]));//16
//a[0]为二维数组首元素,即数组名,sizeof(数组名)大小为16
printf("%d\n",sizeof(a[0]+1));//4/8
//a[0]+1这个动作使得a[0]数组名不再单独在sizeof内部,a[0]+1变为一个地址,所以为4/8
printf("%d\n",sizeof(*(a[0]+1)));//4
//*(a[0]+1))相当于a[0][1],a[0]是首个元素(数组名)
//数组名+1就是在该数组内地址加+1,再解引用得到其中的某个元素,大小为4
printf("%d\n",sizeof(a+1));//4/8
//a+1使得a数组名不再单独在sizeof内部,a+1是跨越首元素地址大小,但还是地址
//一视同仁,4/8
printf("%d\n",sizeof(*(a+1)));//16
//*(a+1)拿到二维数组第二个元素(数组名),大小为16
printf("%d\n",sizeof(&a[0]+1));//4/8
//&a[0]+1,即&数组名+1,等价于a+1,第二个元素地址
//一视同仁,4/8
printf("%d\n",sizeof(*(&a[0]+1)));//16
printf("%d\n",sizeof(*a));//16
printf("%d\n",sizeof(a[3]));//16
//这里不存在越界,因为sizeof内部不会进行真正的计算,不会进行真正的访问
//sizeof只是根据二维数组的这个类型取推断得到的结果

实际打印👇🏻👇🏻👇🏻
在这里插入图片描述

1.sizeof(a+0),a+0是一个地址,所以大小为4/8
2.sizeof(*(a+0)),sizeof(a[0])本质是数组名,在sizeof内部求的是数组的大小

👉🏻笔试题分析

一、

int main()
{
    int a[5] = { 1, 2, 3, 4, 5 };
    int *ptr = (int *)(&a + 1);
    printf( "%d,%d", *(a + 1), *(ptr - 1));
    return 0;
}

*(a + 1)数组第二个元素,为2。
(&a + 1)取数组地址+1跨越整个数组长度
(int *)(&a + 1)强制类型转换为int *.
(ptr - 1),因为类型被强制换为int *,所能访问的字节长为1,此时-1就是减去1字节长度的地址,此时指向的是数组第5个元素,即5的地址。
故打印结果分别为2,5.
在这里插入图片描述
二、

//由于还没学习结构体,这里告知结构体的大小是20个字节
struct Test
{
 int Num;
 char *pcName;
 short sDate;
 char cha[2];
 short sBa[4];
}*p;
//假设p 的值为0x100000。 如下表表达式的值分别为多少?
//已知,结构体Test类型的变量大小是20个字节
int main()
{
p=(struct Test*)0x100000;
 printf("%p\n", p + 0x1);
 printf("%p\n", (unsigned long)p + 0x1);
 printf("%p\n", (unsigned int*)p + 0x1);
 return 0;
}

0x1为16进制的1,其实也就是1.
那么0x100000+0x1,因为结构体的大小为20字节,此时+1就是跳过20字节的长度,即为0x100014(1x16+4x16^0=20)
(unsigned long)p强制类型转换为整型,此时+1即纯粹整型之间的运算加+1,所以为0x100001.
(unsigned int*)p强制类型转换,此时所能访问的字节长度为4,+1即加4个字节,所以为0x100004
在这里插入图片描述
在这里插入图片描述
三、

int main()
{
    int a[4] = { 1, 2, 3, 4 };
    int *ptr1 = (int *)(&a + 1);
    int *ptr2 = (int *)((int)a + 1);
    printf( "%x,%x", ptr1[-1], *ptr2);
    return 0;
}

ptr1[-1]就是*(ptr1-1),此时指向数组第四个元素4.
(int)a强制类型转化为整型,假设a地址此时指向的是0x100000,+1后就是0x100001。
此时又(int *)强制类型转换为整型地址
在这里插入图片描述
四、

int main()
{
    int a[3][2] = { (0, 1), (2, 3), (4, 5) };
    int *p;
    p = a[0];
    printf( "%d", p[0]);
 return 0;
}

p = a[0]其实就是一个数组指针int(*)[2]
在这里插入图片描述
五、

int main()
{
    int a[5][5];
    int(*p)[4];
    p = a;
    printf( "%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
    return 0;
}

p[4][2]相当于(*(p+4)+2),但是因为这里指针p指向的是一个类型为整型大小为4的数组,所以它的步幅长度为4 * 4=16字节,而a的步幅为20字节,所以二者地址相差4字节长度。
在这里插入图片描述

六、

int main()
{
    int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    int *ptr1 = (int *)(&aa + 1);
    int *ptr2 = (int *)(*(aa + 1));
    printf( "%d,%d", *(ptr1 - 1), *(ptr2 - 1));
    return 0;
}

(&aa + 1)此时指向元素10后的地址
(aa + 1)此时指向二维数组第二个元素的地址,*(aa + 1)对其解引用得到数组名(数组首元素地址)
在这里插入图片描述

七、

int main()
{
 char *a[] = {"work","at","alibaba"};
 char**pa = a;
 pa++;
 printf("%s\n", *pa);
 return 0;
}

在这里插入图片描述

八、

int main()
{
 char *c[] = {"ENTER","NEW","POINT","FIRST"};
 char**cp[] = {c+3,c+2,c+1,c};
 char***cpp = cp;
 printf("%s\n", **++cpp);
 printf("%s\n", *--*++cpp+3);
 printf("%s\n", *cpp[-2]+3);
 printf("%s\n", cpp[-1][-1]+1);
 return 0;
}

在这里插入图片描述
1.**++cpp:
先对cpp++,此时cpp指向存放c+2地址处的地址,解引用第一次得到c+2的地址,再次解引用,对此时c+2指向的地址(POINT的地址)进行解引用,得到POINT
2.++cpp+3:注意此时二级指针cpp指向的仍是c+2处的地址
在这里插入图片描述
对cpp++,此时指向了存放c+1地址处的地址
在这里插入图片描述

解引用得到c+1指针此时所指向的地址(NEW)
再对该地址- -,此时指向了存放ENTER地址的地址。
然后再对其解引用得到ENTER的地址,也就是指向ENTER首字符E的地址,然后再+3,此时就是指向ENTER中E的地址,此时打印字符串从E开始打印至\0,为ER.
3.*cpp[-2]+3:
此时cpp指向了c+1处,*cpp[-2]+3为 * (cpp-2)+3
(cpp-2)此时指向了c+3处(存放c+3地址处的地址)
2 * cpp-2)得到FIRST的首字符的地址,+3指向了S
4.cpp[-1][-1]+1:
等价于 * ( * (cpp-1)-1)+1,cpp-1此时指向了c+2处,
(cpp-1)得到c+2指针, * (cpp-1)-1指向了NEW处的地址, * ( * (cpp-1)-1)得到NEW首元素N的地址,此时+1指向E的地址。

在这里插入图片描述


如上便是本期的所有内容了,如果喜欢并觉得有帮助的话,希望可以博个点赞+收藏+关注🌹🌹🌹❤️ 🧡 💛,学海无涯苦作舟,愿与君一起共勉成长
在这里插入图片描述

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值