指针的进阶

   

目录

1.指针数组(int* p[]={数组名1,数组名2,数组名3...)

2.数组指针(int (*p)[]={&(数组名1),&(数组名1+1),&(数组名1+2)...})

2.1 arr和&arr的区别:

2.2数组指针的用途

2.3常见的数组与指针结合的类型

 3.函数指针

3.1函数指针数组 

3.2回调函数

4.金句省身


     

      写在前面:本文重点是讲解指针相关的结构化形式和传参用法以及相关考察题的讲解,让你一篇文章弄懂指针的相关操作用法,从此不再惧怕指针,那让我们开始吧......

前言:首先,我们知道指针是用来存放变量地址的变量类型,而地址是唯一标识的一块空间,指针的字节大小是固定的(32位平台上是4个字节,64位平台上是8个字节),而且指针是有类型的,指针的类型决定了指针加减时的步长,还有指针解引用操作时候的权限,比如int *p;char *q=(char*)&p;就是将解引用操作类型设为了char*,也就是只读取了p的最低位的字节赋给了q,我们还应该了解了指针的相关运算等知识。

1.指针数组(int* p[]={数组名1,数组名2,数组名3...)

指针数组的每个元素存放的都是一个指针,如int* arr[],再比如char* arr[]={"abc","def","ghi"},其实这也是一个指针数组,可以变相的看做二维字符数组,只不过是一维数组里每个数组元素存放的是一个字符串的首元素的地址,也就是说,找到了这个一维的指针,也就找到了这个字符串,其实也可以写成char* arr[]={&'a',&'d',&'g'},数组名代表数组首元素的地址,注意常量字符串带结尾的'\0'哦,不要忘了,它和字符数组还是有区别的哦。

int main()
{
	char arr1[] = { 'a','b','c' };
	char arr2[] = { 'd','e','f' };
	char arr3[] = { 'g','h','i' };

	//指针数组
	char* arr[] = { arr1,arr2,arr3 };//数组名等于数组首元素地址
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 3; j++)
		{
			//printf("%c ", arr[i][j]);
			printf("%c ", *(arr[i] + j));//加j表示的是在每一个元素对应的数组的第j+1个元素
			//两种方式均可,    arr[i]=arr+i
		}
		printf("\n");
	}
	return 0;
}

当然了,这里的指针数组不一定只能存放一级指针

char arr[]

char* arr[]//存一级指针

char** arr[]//存放二级指针

这几种放到后面会再讲的,不要着急,其实我也着急,毕竟都想看难的,但是凡事要循序渐进,哈哈

2.数组指针(int (*p)[]={&(数组名1),&(数组名1+1),&(数组名1+2)...}

       假如我给你一个数组,让你给我数组的地址你会怎么表示,我们都知道,数组名绝大部分情况下是首元素地址,但是这里有两个例外第一个就是在sizeof(数组名)的时候求出的就是数组的总的字节数,还要注意数组在传参进入功能函数时要将其大小一并求出作为参数传入,因为数组在传参时一般都是将数组首元素即数组名传入功能函数,而函数会拷贝一份形参来代替传入的参数进行操作,所以一切操作在两者无联系的情况下(所谓无联系,比如就是没有引用等操作)不会相互影响,这个拷贝的形参也就只复制了数组的首元素地址,与原数组并不一致,所以在main函数外部求数组长度的时候要看好,最好是在main函数内部就求出传入功能函数,或者将数组设置为全局函数都可以),至于这第二个就是&(数组名),表示的是整个数组,该操作取出的就是数组的地址了,要记住。

我们整形指针指向的是整形,存放的是整形的地址,那么逻辑上的数组指针就应该是指向的是数组,存放的就是数组的地址,也就是我们前面说的&(数组名),其类型就是每个数组的类型,而存储的元素却是指针,形式上就是 int (*p)[]={&arr1,&(arr+1),&(arr+2)...};,数组指针本质上是数组,但是其存放的却是每个数组元素的地址,

对于这个地方我还是想再啰嗦一下,数组指针一般是用来处理二维数组问题比较方便,int(*p)[4]相当于数组中存放的是一个指向由四个元素构成的数组的数组首元素,这里的p代表指向一维数组的指针,相当于&arr,*p虽然结果上也是如此,但是*p实际上相当于arr[0]

问:int(*p)[4]中p+i和(*p)+i 的区别:

对于p+i,我们知道此时的p=&arr,相当于p指向的是一个一维数组,那么此时p的操作步长就会变成所指向的一维数组的总的字节数,对于一维数组来说,这项操作就属于非法访问了,对于二维数组来说,p+i=&arr[1],也就是相当于p指向了下一行;

对于(*p)+i,p=&arr,对p解引用,*p=arr,则相当于在一维数组上来进行操作,arr+i就相当于arr[0][i]。

2.1 arr和&arr的区别:

前面说的是arr表示的是数组首元素的地址,&arr表示的是数组的地址,那两者到底又什么区别呢,我们来探讨一下:

上面我们讲过大多数情况下arr=&arr[0],但是有两个特殊情况,就是sizeof(arr)和&arr的时候,我们来编译一下看看结果:

 我们发现,虽然arr和&arr在地址上相同,但是他们在意义上是不同的,arr+1表示的就是正常的数组首元素加1,也就是到了arr[1]的位置上,arr的类型和&arr的类型都是一样的,他们都是指针(int*),但是对于&arr,它代表的是整个数组,而且&arr+1之后,在地址上增加了sizeof(arr)个字节,也就是数组的10个元素,40个字节,而他的类型是数组指针,也就是(int(*)[10]),对于类型的判断,我们通常将变量名去掉即可判断,它们两者的类型不一样,所以所代表的意义不一样。

2.2数组指针的用途

        数组指针可以指向一整个数组,int  (*p)[10]=&arr,就相当于*p=arr,就可以将*p当做arr来使用,arr[i]=(*p)[i]

二维数组传参问题:

void print(int(*p)[4], int r, int c) //二维数组的传参的时候行可以不写,但是列必须要写
{
	for (int i = 0; i < r;i++)
	{
		for(int j = 0; j < c; j++)
		{
			printf("%d ", *(*(p + i) + j));//p表示二维数组的首元素的地址,也就是第一行的地址,则p+i表示的就是当前的二维数组中正在遍历的那一行的一维数组的数组名
			//printf("%d ", *(p[i] + j));
		}
		printf("\n");
	}
}
int main()
{
	int arr[3][4] = { {1,2,3,4},{5,6,7,8},{9,10,11,12} };
	int col = sizeof(arr[0]) / sizeof(arr[0][0]);  //列
	int row = (sizeof(arr) / sizeof(arr[0][0])) / col;   //行
	
	print(arr, row, col);
	
	return 0;
}

我们都熟悉并且了解了一维数组的传参问题,一般都是传入数组名,也就是首元素的地址,但是对于二位数组,我们知道它的每一行就是一个一维数组,也就是一维数组的数组,符合数组指针的定义,同样的,二维数组的数组名也是首元素的地址,只不过二维数组的首元素是第一行的一维数组整体,所以设置参数的时候要设置成数组指针的形式,

这里可以和一维数组的数组指针进行比较着理解,

2.3常见的数组与指针结合的类型

int main()
{
	int arr[10];  //普通整形数组
	int* arr[10];  //指针数组
	int(*arr)[10];  //数组指针
	int(*arr[10])[10];
	/*
	对于这最后一个,我们说[]的结合性是高于解引用符号*的,所以在判断类型时,我们可以将变量所代表的符号先拿掉,剩下的就是它的类型,这个例子中变量符号其实是arr[10]这个数组,所以拿出来之后,还剩下,
	 int (*)[10],这明显是个数组指针的标志,只是现在存放数组指针的又是一个数组罢了,相当于它的每个元素都是一个数组的地址,具体可以看下面的图
	*/
	return 0;
}

 拓展:关于二维数组传参的几种方法

 在上面的几个方案中,第一,二,四种没什么说的,我们在上面就说了,那对于第三种,明显该变量类型就是指针,这是一个指针数组,但是我们传过去的参数是一个数组指针(二维数组的数组名等于第一行元素的地址也就是第一行的数组对应的地址,数组的地址需要用数组指针来接收,所以第三个不行,对于第五个,二级指针用来接收一级指针的地址,但是我现在传过去的是一个一维数组的地址,是一行的地址,并不是一级指针的地址,所以不可行。

 3.函数指针

我们平时经常写函数,那函数有没有地址呢?是的,函数也有地址,可以用&(函数名)或者直接是函数名也可以表示该函数的地址,二者并无差别,不像数组一样。

那么如何声明函数指针呢?我们有

                函数返回类型   (*p)(函数参数类型) =(&) 函数名;//其中&有无均可

函数指针的应用场景往往比较复杂,不会例子里的找麻烦式应用一样,这里不作展开,感兴趣的可以去在专门查一下。

3.1函数指针数组 

顾名思义,函数指针数组和指针数组一样,只不过每个数组元素是一个指针,还是函数指针,存放函数指针的数组就是函数指针数组。

本质上就是将原来的函数指针的单个变量变为数组进行封装,类型不变,但是须符合能成为数组的特性,比如函数返回值类型,参数列表需相同。

 

 这也是函数指针的一个用途,可以用来封装功能函数,以便于后续的维护和操作。

3.2回调函数

回调函数:

函数指针的调用,即是一个通过函数指针调用的函数;如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,就说这是回调函数。

下面是一个函数指针数组的简单应用(简单两位正整数的四则运算计算器的封装实现)

#define _CRT_SECURE_NO_WARNINGS
#include <bits/stdc++.h>
#include <windows.h>
#include<time.h>
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 Cul(int(*pf)(int, int)) {  //pf是函数指针
	int x, y;
	printf("请输入两个操作数->\n");
	scanf("%d%d", &x, &y);
	printf("%d\n" ,pf(x, y));
}
int main()
{
	
	int input;
	do {
		printf("---------------------------------\n");
		printf("1.两个数相加   2.两个数相减\n");
		printf("3.两个数相乘   4.两个数相除\n");
		printf("0.退出计算器               \n");
		printf("---------------------------------\n");
		printf("请选择->\n");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			Cul(Add);
			Sleep(1000);
			break;
		case 2:
			Cul(Sub);
			Sleep(1000);
			break;
		case 3:
			Cul(Mul);
			Sleep(1000);
			break;
		case 4:
			Cul(Div);
			Sleep(1000);
			break;
		case 0:
			printf("退出计算器\n");
			Sleep(1000);
			break;
		default:
			printf("输入有误,请重新输入\n");
			Sleep(1000);
			break;
		}
		system("cls");
	} while (input);

	return 0;
}

拓展:qsort回调函数的简介

qsort函数是一个C库函数,其底层是基于快速排序原理实现的排序函数,

其用法如下:

 可见,qsort排序可以排序任意类型的数据,它的最后一个参数传入的是函数指针,前三个参数基本不用变,变的是最后一个函数指针所指向的排序方法,

我们现在重点来看这个比较函数指针

int (__cdecl *compare )(const void *elem1, const void *elem2 );

其中compare是一个函数指针,在实际情况下,这类函数往往需要我们自己写,我们通过返回值大小来“教计算机”判断大小,规则如下:

这里比如我们排序的是整形数组,则需要比较两个整形的大小,

int cmp(const void* a, const void* b)//在C中我们认为任何类型的指针都可以显式转换为void类型,且不会丢失数据,但是不能进行解引用操作,所以我们需要将其进行强制类型转换才能使用,并且不会影响其原来的值
{
	return *(int*)a - *(int*)b;//强制类型转换
    //当然如果想实现降序排序的话,减法反过来就好了
}
void print(int arr[], int sz)
{
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}
int main()
{
	int arr[10] = { 0,2,5,7,9,3,6,1,8,4 };
	int len = sizeof(arr) / sizeof(arr[0]);

	qsort(arr, len, sizeof(arr[0]), cmp);//将函数的地址传给qsort,qsort中的函数指针接收这个cmp函数的地址
	//注意包含头文件stdlib.h
	print(arr, len);
	return 0;
}

 

重点来了,我们的目的,是利用学过的函数指针的知识来实现一个类似于qsort的排序算法,当然,重点就落在了cmp函数指针的写法上,我们慢慢来思考:

首先,我们写这个函数的目的,是为了实现任意数据类型的排序,这里我们以最简单的冒泡排序为基本原理来实现该功能。

第二,比较函数中的类型要怎么设置,现在的情形是我们不知道用这个函数的人要比较什么类型的数据,所以我们不能将两个待比较的数据简单的设置为整形或者其他类型,这里有一个办法,就是将使用者传入的数据进行逐字节比较,因为数据的最小单位是一个字节(char),所以我们可以将传入的参数的地址强制转换为(char*),然后我们可以根据一个数据类型所占的字节数(width参数由使用者传入,目的是让我们直到一个数据占多少个字节,方便后续的比较),那接下来就会产生一个问题,如何交换两个数据呢,前面我们将数据进行逐字节比较,当然也可以进行逐字节交换,但是要注意一个数据的字节数为width,一个有效数据的部分是从传入的字节开始到底width-1个字节。

int cmp(const void* a, const void* b)//在C中我们认为任何类型的指针都可以显式转换为void类型,且不会丢失数据,但是不能进行解引用操作,所以我们需要将其进行强制类型转换才能使用,并且不会影响其原来的值
{
	return *(int*)a - *(int*)b;//强制类型转换(注意,此处的强制类型本应由使用者传入(这里以(int*)代替,其类型由使用者需要比较的类型决定,因此我们不必担心返回值会因为数据类型不同而出错
}
void Swap(char* a, char* b, size_t width)
{
	size_t i = 0;
	while (i< width)
	{
		char temp = *(a+i);
		*(a+i) = *(b+i);
		*(b+i) = temp;
		i++;
	}
}
void print(int arr[], int sz)
{
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}
void my_qsort(void* base, size_t num, size_t width, int (*cmp)(const void* , const void* ))
{
	for (size_t i = 0; i < num; i++)
	{
		for (size_t j = 0; j < num - i - 1; j++)
		{
			if (cmp((char*)base + j*width, (char*)base + (j + 1) * width)>0)//返回结果大于0就交换
			{
				Swap((char*)base + j * width, (char*)base + (j + 1) * width,width);
            }
		}
	}
}
int main()
{
	int arr[10] = { 0,2,5,7,9,3,6,1,8,4 };
	int len = sizeof(arr) / sizeof(arr[0]);

	my_qsort(arr, len, sizeof(arr[0]), cmp);
	print(arr, len);
	return 0;
}

4.金句省身

       你的层次,决定了你圈子的高度,你是什么样的人,就会进什么样的圈子,你强大的时候,你处的圈子也强大,你弱小的时候,你处的圈子也弱小,这个时候就算结识了很多厉害的人,也只只能算仅仅认识而已,对弱小的你来说是无用的,成年人的结识,都和利益相关,如果双方没有利用的价值,就会一拍而散。

别说什么成年人的社会很残忍,这就是现实,大部分的人都愿意锦上添花,不愿意雪中送炭,所以,不要把时间都浪费在无效的社交上,多去提升自己吧......

 

        文章的篇幅可能有点长,创作不易,只是想把学到的东西用我们最能接受的方式分享给大家,不求大家的支持,但是还是希望读者朋友能够提出些宝贵的意见和建议,我也会逐步提升内容的质量的,希望我们共同进步。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值