关于指针的一些总结

Posted 正义的伙伴啊

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于指针的一些总结相关的知识,希望对你有一定的参考价值。

指针

  • 指针是什么
  • 指针和指针类型
  • 野指针
  • 指针运算
  • 数组指针
  • 二级指针
  • 字符指针
  • 指针数组
  • 指针传参
  • 函数指针

指针是什么?

在计算机科学中,指针(Pointer)是编程语言中的一个对象,利用地址,它的值直接指向(points to)存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。因此,将地址形象化的称为“指针”。意思是通过它能找到以它为地址的内存单元。

在这里插入图片描述

内存和地址的关系

内存中最小的一个单元为1个字节,内存中每一个内存都对应这一块独一无二的地址,而地址其实是由32跟地址线(32位操作系统)通过通电、断电分别表示0和1代表所形成的32个二进制位,这样我们就能计算32位操作系统的内存了:
232 byte = 4194304 kb =4096 mb = 4gb 所以32位操作系统有4gb内存同理64位操作系统
如图一个int类型占4个字节 ,所以占了四个格子,每个格子都有一个地址,那么这个int类型就是有4个地址了吗?其实并不是,我们一般取第一个字节的地址作为整个int类型的地址。我们地址在内存中是以32位二进制码存储的,但是我们在调试时打开内存监视时,会发现其实显示出来的时8位16进制。这里开辟了五个空间
这里提供了五个地址和其所对应的值,如果你记住了地址你就能访问地址里面所储存的值。然而要记住这些地址显然太过笨拙,于是用字母命名地址

在这里插入图片描述
当然这些名字就是我们所熟悉的变量 。但是有一点很重要,名字与内存之间的关联是由编译器为我们所提供的,硬件仍然通过地址访问内存位置。

指针的大小

  • 在32位平台上,地址时32个0和1的排列组合的二进制位,也就是4个字节,所以一个指针变量的大小就是4个字节
  • 在64位平台上,地址时64个0和1的排列组合的二进制位,也就是8个字节,所以一个指针变量的大小就是8个字节

指针和指针类型

变量有整型,浮点型之分,那么指针也有这样的分类

int *p=NULL;
char *p=NULL;
float *p=NULL;
short *p=NULL;
double *P=NULL:
void *p=NULL;

变量的类型决定变量在内存中的大小,但是指针的大小在32位操作平台上是一个32位的二进制码定值,那么指针的类型有什么意义?

指针±整数

在这里插入图片描述
指针的类型决定指针向前或向后走一步有多大。

指针的解引用

#include<stdio.h>
int main()
{
	int a = 1000000;             // 1
	char* p =(char *) &a;
	int *pc =&a;
	*p = 0;                       //2
	*pc = 0;                      //3
}

打开调试,在内存中输入&a监视内存
当1执行完时,a的内存:在这里插入图片描述
当2执行完时,内存变为:

当3执行完时,内存变为:在这里插入图片描述
总结:指针的类型还决定了指针解引用时的权限(能访问几个字节)如:int * 能访问4个字节,而char *只能访问一个字节。

指针运算

  • 指针减指针
  • 指针的关系运算

指针减指针

#include<stdio.h>
int main()
{
	int arr[3] = { 1,2,3 };
	printf("%x", &arr[2] - &arr[0]);
	return 0;
}

指针实际上存放的是地址的值,而我们又知道指针的类型决定了指针加减整数移动的的“步伐”,所以指针减去指针自然就代表了两个地址之间元素的差值。

指针的关系运算

这里我要注意指针的解引用操作符在一些表达式的含义:

  1. *p++:这里因为++(后置)的优先级和解引用 操作符优先级相同,顾按照结合律从右向左,所以应该是 *(p++),又因为后置++所以地址先解引用后地址++,所以顺序为 *p -> p++
  2. *++p:这里同样++的优先级与解引用操作符优先级相同,按照结合律, *(++p)顺序为:++p -> *p
  3. ++*p:按照上面的规律:顺序为:*p -> (*p)+1 最后 A=(*p)+1
  4. (*p)++:同理顺序为: *p -> (*p)+1 最后A= *p

指针类型

数组指针

#include<stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	printf("%p\\n", arr);     1
	printf("%p\\n", &arr[0]); 2 
	printf("%p\\n\\n", &arr);  3


	printf("%p\\n", arr+1);    4
	printf("%p\\n", &arr[0]+1);5
	printf("%p\\n", &arr+1);   6
	return 0;
}

在这里插入图片描述
arr为数组名代表的是首元素的地址,&arr为数组的地址,两者都是指针,但是指针类型不同
这里我们就要介绍一下数组指针,数组指针是一种指向数组的指针!

#include<stdio.h>
int main()
{
	int arr[10] = { 0 };
	int(*p)[10] = &arr;     //定义方式如下
	//[]的优先级大于*的优先级,p先和*结合说明p是一个指针,指针指向的是int [10]

}

[]的优先级大于*的优先级,p先和 * 结合说明p是一个指针,指针指向(指针的指向实际上就是去掉指针剩下的就是指针指向的类型)的是int [10]

这里我们从前三个可以知道1,2,3所表示的地址的值是相同的,都是第一个元素的地址,上面已经说过了数组名和数组首地址名的指针类型不同,顾后三组4,5,6地址的值就不同了!实际上arr是一个int 类型的指针,而&arr是一个int ( * )[10]类型的指针(数组指针),指向的元素是一个int [10]类型的数组。arr加一跳过四个字节,&arr+1跳过整个数组,也就是410=40个字节。
在这里插入图片描述
我们首先要搞清楚一维数组在内存中的排列方式是,随着下标的增大,地址也在增大

  • 不同点:

arr --------指针类型:int * ------------------指针的步伐是一个int的大小
&arr ------指针类型:int ( *)[10] ----------指针的步伐是一个int [] (一个数组)的大小

  • 相同点:

arr和&arr的值相同都是数组首元素的地址。

数组名与指针的关系

上面说到了数组名是一个指针,大小是首元素的地址,所以我们可以使用指针来访问一个数组!

#include<stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;
	for (int i = 0; i < 10; i++)
	{
		printf("%p ----------- %p\\n", &arr[i], p + i);
	}
	return 0;
}

在这里插入图片描述
所以p+i实际上就是arr[i]元素的地址。也就是 p+i <== > &a[i]
同理 *(p+i) < == > a[i] < == > * (arr+i)

实际上我们就知道了 例如一个指向数组首元素的地址的指针p,p[i]等价于 * (p+i)

数组指针的使用

数组指针最长使用的还是二维数组,首先我们要了解二维数组的构造:
通常我们会把二维数组元素想象成元素形式排列,但实际上:

#include<stdio.h>
int main()
{
	int arr[2][3] = { 0 };
	for (int i = 0; i <2; i++)
	{
		for (int j = 0; j < 3; j++)
		{
			printf("%p\\n", &arr[i][j]);
		}
	}
	return 0;
}

在这里插入图片描述
其实二维数组相邻元素地址也是相邻排列的。
在这里插入图片描述
实际上二维数组可以看成一个指针数组,例如int a[2][3]可以看成一个含有两个元素的指针数组,该数组的元素为指向含有三个元素的数组的指针,定义如下:

int* p[2] = { arr[0],arr[1] };

实际上这里指针p和数组名arr作用是一样的,一下代码来验证一下:

#include<stdio.h>
int main()
{
	int arr[2][3] = { 1,2,3,4,5,6 };
	for (int i = 0; i <2; i++)
	{
		for (int j = 0; j < 3; j++)
		{
			printf("%p-----------%d\\n", &arr[i][j],arr[i][j]); //打印用arr表示的数组元素地址和数组的值
		}
	}
	printf("-----------------------\\n");
	int* p[2] = { arr[0],arr[1] };
	for (int i = 0; i <2; i++)
	{
		for (int j = 0; j < 3; j++)
		{
			printf("%p-----------%d\\n", &p[i][j],p[i][j]);//打印用p表示的数组元素地址和数组的值
		}
	}
	return 0;
}

在这里插入图片描述
结果我们发现 p和arr是完全等价的,同时也说明二维数组实际上是一个存放一维数组指针的指针数组!
从中我们可以知道二维数组名的含义:

  1. arr表示第一行数组的地址 ————arr+1 第二行数组的地址
  2. arr[ i ]表示第i行数组的首元素 ————arr[i]+1 第i行第二个元素的地址
  3. &arr表示二维数组的地址————&arr+1跳过整个二维数组

二级指针

指针变量归咎到底还是一个变量,而变量就会有地址,所以指针变量的地址存放在哪里?这就是二级指针!
在这里插入图片描述
**

**

字符指针

**
定义一个字符指针:

#include<stdio.h>
int main()
{
	char ch = 'w';
	char* pc = &ch;
}

这里将z字符‘w’的地址存入了字符指针中。

#include<stdio.h>
int main()
{
	const char* p = "abcdef";
	printf("%s\\n", p);
	return 0;
}

这里看似将一个字符串存入了一个字符指针中,但实际上是将“abcdef”的首字符的地址存入了p中。
在这里插入图片描述
实际上"abcdef"被存入了内存中的常量区,所以’abcdef’也被称为常量字符串,常量区的变量是不可以被修改的,高级一点的编译器在定义指向常量字符串的指针时必须前面加上const来防止字符串被修改!

#include<stdio.h>
int main()
{
	char str1[] = "abcdef";
	char str2[] = "abcdef";
	const char* str3 = "abcdef";
	const char* str4 = "abcdef";
	if (str1 == str2)
	{
		printf("str1 str2地址相同\\n");
	}
	else
	{
		printf("str1 str2地址不相同\\n");
	}
	if (str3 == str4)
	{
		printf("str3 str4地址相同\\n");
	}
	else
	{
		printf("str3 str4地址不相同\\n");
	}
}

在这里插入图片描述
这里其实有说明了另外一个问题,如果存入常量区的字符相等,那么分别指向他们的指针也就同样相等。文字常量区相同字符串共用内存,可以有效防止重复内容占用内存。但是数组类型的字符串实际上是开辟了一块新的空间,而且字符串的内容可以随意被修改,所以指针的地址自然不同。

指针数组

**
指针数组顾名思义是一个数组,然而这个数组是一个存放指针的数组,所以叫指针数组。

int *arr1[10];  
int** arr2[10];
char* arr3[10];

由于[]的优先级高于*,所以arr先于[]结合,所以是一个数组,我们要想知道一个数组存放的元素类型只要将数组整个去掉剩下的就是元素类型,所以上面的元素类型分别为 int * ,int **,char *。

定义数组指针

int *p[3]={"hello","goodbye","thank"};

注意数组指针的数组名实际上是一个二级指针!

int **pc = p;

原因是:数组名是第一个元素的地址,而第一个元素是一个指针,也就是指针的地址,也就是二级指针。

指针传参

写代码的时候要把数组或者指针作为参数传给函数,那么函数的参数一个如何书写?
原则:传过去的地址一定要与接收的类型匹配。

一维数组传参

void test(int arr[])           //1
void test(int arr[10])         //2
void test(int* arr)             //3
int arr[10] = {};
test(arr);


void test2(int* arr[20])        //4
void test2(int** arr)           //5
int* arr[20] = {};
test2(arr2);

以上引用均为正确的;

二维数组传参


void test1(int* arr)
{

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

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

}
void test4(int** arr)
{

}

int main()
{
	int arr[3][5] = { 0 };
	test1(arr[1]+1);
	test2(arr);         //错误
	test3(arr);
	test4(arr);        //错误
}

函数指针

c语言中还有函数指针:存放函数地址的指针。

函数指针的性质:
&函数名=函数名=函数的地址。 由此我们也可以推出 函数名=*函数名

定义方式:
返回值类型 (*函数名) (函数参数类型)

解读两端函数指针定义:

(*(void (*)())0)();
第一步:
*p=(void (*)())0;   //p是函数指针
第二部:
(*p)()

这里我们从0入手,0先被强制类型转换成void (*)()类型也就是函数指针类型,然后被解函数指针解引用调用。

void (*signal(int ,void(8)(int)))(int)
第一步:
*p=signal(int ,void(*)(int))
第二部:
void(*P)(int)

signal函数调用一个int 一个函数指针,返回一个函数指针(p),函数指针解引用调用一个函数参数为int的函数。
但实际上这样写是不规范的,因为p是一个函数指针,在定义的时候并没有说明变量类型。

typedef void(*P)(int);
p signal(int ,p);

函数指针数组

顾名思义是存放函数指针的数组
定义:

int (*p[10])()

函数指针的用途:
实现计算器:



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 menu()
{
	printf("**************************\\n");
	printf("**** 1. add    2. sub ****\\n");
	printf("**** 3. mul    4. div ****\\n");
	printf("****     0. exit      ****\\n");
	printf("**************************\\n");
}

int main()
{
	int input = 0;
	//计算器-计算整型变量的加、减、乘、除
	//a&b a^b a|b a>>b a<<b a>b

	do {
		menu();
		
		int x = 0;
		int y = 0;
		int ret = 0;
		printf("请选择:>");
		scanf("%d", &input);

		switch (input)
		{
		case 1:
			printf("请输入2个操作数>:");
			scanf("%d %d", &x, &y);
			ret = Add(x, y);
			printf("ret = %d\\n", ret);
			break;
		case 2:
			printf("请输入2个操作数>:");
			scanf("%d %d", &x, &y);
			ret = Sub(x, y);
			printf("ret = %d\\n", ret);
			break;
		case 3:
			printf("请输入2个操作数>:");
			scanf("%d %d", &x, &y);
			ret = Mul(x, y);
			printf("ret = %d\\n", ret);
			break;
		case 4:
			printf("请输入2个操作数>:");
			scanf("%d %d", &x, &y);
			ret = Div以上是关于关于指针的一些总结的主要内容,如果未能解决你的问题,请参考以下文章

关于typedef的用法总结

几个关于js数组方法reduce的经典片段

关于数组指针传参设计的总结

ref:关于JAVA中一些安全漏洞示例说明及如何规避方法代码示例总结分享

关于指针和链表中的一些问题

关于局部指针的一点总结