对指针的初级认识

发布于:2022-12-06 ⋅ 阅读:(371) ⋅ 点赞:(0)

目录

1. 指针是什么?

2. 指针和指针类型

2.1 指针的解引用

2.2 指针的解引用

3. 野指针

3.1 野指针成因

3.2 如何规避野指针

4. 指针运算

4.1 指针+-整数

4.2 指针-指针

4.3 指针的关系运算

5. 指针和数组

6. 二级指针

7. 指针数组



1. 指针是什么?

指针理解的2个要点:

1. 指针是内存中一个最小单元的编号,也就是地址;

2. 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量。

总结:指针就是地址,口语中说的指针通常指的是指针变量。

⑴对指针的理解:

理解指针先认识内存,内存——是一块很大的空间。

 为了对内存进行很好的管理和使用,把一个大的内存空间化为一个一个小的内存单元,而一个一个

小的内存单元就像现实生活中的一个一个的房间,这样一个一个小的内存单元的大小是一个字节。

对房间进行编号就是房间号,这个房间号就是房间的地址或编号,有了编号就能快速找到对应的房间。则内存中能快速找到要找的内存单元,就需要给每个内存单元编号,即通过编号就能快速定义到对应的内存单元,编号又被称为地址

那么一个一个的内存单元编号是怎么产生的呢?

机器一般是32位机器或者是64位机器。32位机器有32根地址线,这里的地址线是物理的电线,电线会通电,通电的时候会有高电平和低电平,高电平模拟的就是1,低电平模拟的是0,电信号转化为数字信号对应的就是1和0。32根物理线(地址线)产生的电信号转换为数字信号,32根电线上要么是1要么是0,则数字信号就有32位,产生的结果可能是:

00000000000000000000000000000000

……

11111111111111111111111111111111

这里就有2的32次方个地址。

所以就把电信号产生的数字信号的这种二进制序列当做内存单元的编号。

这些内存单元的编号是不需要存起来的,这些内存单元的地址编号是由硬件电路直接产生的。

每个地址标识一个字节,一个地址(编号)管理一个字节,2的32次方个地址即有2的32次方个字节,那我们就可以给 (2^32Byte == 2^32/1024KB ==2^32/1024/1024MB==2^32/1024/1024/1024GB == 4GB) 4G的空闲进行编址

⑵再理解指针:

指针是内存中一个最小单元的编号,也就是地址;

内存单元都有自己的编号,这个编号也被称为地址,一块空间的地址的作用是能够指向这块空间,所以一个地址编号能够找到这块空间,所以把地址也称为指针。

所以对于内存单元:

内存单元的编号 = 内存单元的地址 = 指针

这三者其实是一回事,指针就是地址

认识内存单元的布局

#include <stdio.h>
int main()
{
	int a = 10;//在内存中创建了一个整型变量
	//这个变量想要在内存中存储10,则需要开辟自己(a)的空间,分配给了a4个字节的空间
	//每一个字节都有自己的内存单元编号
	return 0;
}

a在内存中是怎么存放的呢?

为什么a在内存中放的是0a 00 00 00呢?

10是整数,10写成二进制序列是:00000000000000000000000000001010太长,内存中展示不方便,内存中存的是二进制,但是在内存窗口看的是十六进制数字,4个二进制位能转化为1个二进制位,4个0(二进制)转化为一个0(十六进制),1010是10即是a,所以:10的十六进制是:00 00 00 0a,所以是转化为十六进制之后倒着放进去的。

00000000 00000000 00000000 00001010

00 00 00 0a

 打印a的地址:

#include <stdio.h>
int main()
{
	int a = 10;
	printf("%p\n", &a);
	return 0;
}//0098F7B4
//每次开辟的空间不一样

存放a的地址:

pa = &a;

&a是地址,也是指针,把a的地址存到pa中,pa一定是个变量,因为存放的是指针,所以pa是指针变量,pa的类型是int *;这里pa是指针,其实是指针变量,即平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量。

综上:指针就是地址,口语中说的指针通常指的是指针变量。


对指针变量的理解:

int* pa = &a;

pa是指针变量,它的类型是:int *

int*可以分开理解:*表示pa是指针变量,int说明pa指向的变量a的类型是int,pa指向的是整型变量。

指针变量:
我们可以通过&(取地址操作符)取出变量的内存起始地址,把地址可以存放到一个变量中,这个
变量就是指针变量

指针变量的创建:

#include <stdio.h>
int main()
{
int a = 10;//在内存中开辟一块空间
int *p = &a;//这里我们对变量a,取出它的地址,可以使用&操作符。
//a变量占用4个字节的空间,这里是将a的4个字节的第一个字节的地址存放在p变量
中,p就是一个指针变量。
return 0;
}

注意是:当a是int时&a取的是a四个字节中的第一个字节的地址。地址在内存中是连续存放的。

取a地址时,取出的是变量所占空间起始的那一个字节的地址。

总结:
指针变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)。

32位机器上有32根地址线上的电信号产生的数字信号是由32位个0、1组成的二进制序列,就是一个地址,如果把地址存起来,则需要32个比特位的空间(32bit)即是4个字节。所以用来存放地址的指针变量也需要4个字节的空间

#include <stdio.h>
int main()
{
	int a = 10;
	int* pa = &a;
	//printf("%p\n", &a);
	printf("%d\n", sizeof(pa));//4
	return 0;
}

说明指针变量pa占了4个字节,因为地址的大小是4个字节,这个字节需要存起来则需要4个字节的空间,所以一个指针变量的大小就是4个字节,在64位平台上就需要8字节的空间。

总结:

在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以
一个指针变量的大小就应该是4个字节。
那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地
址。
指针(指针变量)是用来存放地址的,地址是唯一标示一块地址空间的。一个地址就能找到唯一的内存空间。
指针的大小在32位平台是4个字节,在64位平台是8个字节。


2. 指针和指针类型

变量有不同的类型,整形,浮点型等。准确的说,指针也有类型:
char  *pc = NULL;
int  *pi = NULL;
short *ps = NULL;
long  *pl = NULL;
float *pf = NULL;
double *pd = NULL;

由此可以得出:指针的定义方式是: type + * 。
其实:
char* 类型的指针是为了存放 char 类型变量的地址。
short* 类型的指针是为了存放 short 类型变量的地址。
int* 类型的指针是为了存放 int 类型变量的地址。

#include <stdio.h>
int main()
{
	char* pc;
	char* pa;
	char* pd;
	printf("%d ", sizeof(pc));
	printf("%d ", sizeof(pa));
	printf("%d ", sizeof(pd));
	return 0;
}//4 4 4

各种类型的指针的大小是一样的,为什么不直接用一种类型?指针类型的意义是什么?
接下来探讨:

2.1 指针的解引用

#include <stdio.h>
int main()
{
	int n = 0x11223344;//0x表示十六进制数字,
//11占一个字节,22占一个字节33占一个字节,44占一个字节,
//该数字是4个字节的存储,a变量是int就是4个字节
	//int* pi = &n;
	//*pi = 0; 
//在调试的过程中观察内存的变化:
//按&n,4个字节的内容由44 33 22 11变成00 00 00 00 ,这里改变了4个字节
	
	//这次是把n的地址取出来放到pc中,pc的类型是char*,
	char* pc = &n;
	//取地址n取出的是整型的地址,由int*非要放到char*里是可以的,
//虽然运行时会报错但从理论上讲,pc里是可以放得下n的地址的,
//因为地址的大小都是4个字节,不同指针类型的大小一样,只不过有类型的差异
	*pc = 0; 
//在调试的过程中观察内存的变化:
//按&n,4个字节的内容由44 33 22 11变成00 33 22 11 ,这里改变了第1个字节
	return 0;
}

仅仅由int*变成char*,让在访问内存数据的时候产生不一样的效果,如果是整型指针,在解引用时访问了4个字节,如果是字符指针,在内存访问的时候访问了1个字节。

这里指针类型的作用是:

指针类型决定了在解引用的时候一次能访问几个字节——其实讲的是指针的权限。

在一次解引用时:
int*指针——能访问4个字节
char*指针——能访问1个字节
……
float*——能访问4个字节
double*——能访问8个字节
即确定了指针类型的意义。

不同类型的指针的权限是不一样的。

指针类型的第一个意义:指针类型决定了从某一个内存地址开始向后访问的时候访问几个字节,权限是多大。(指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。)

2.2 指针的解引用

#include <stdio.h>
int main()
{
	int a = 10;
	int* pa = &a;
	int pc = &a;
	printf("%p\n", pa);//012FF714
	printf("%p\n", pc);//012FF714

	printf("%p\n", pa);//012FF714
	printf("%p\n", pa+1);//012FF718,跳了4个字节

	printf("%p\n", pc);//012FF714
	printf("%p\n", pc+1);//012FF715,跳了1个字节
	return 0;
}

指针类型的第二个意义,指针类型决定了指针+1或者-1的步长。如整型指针向后走一步走了4个字节,如果是字符指针,向后走一步走了1个字节。
即指针类型决定了它向前走一步或向后走一步走的步长是多少,单位是字节。

指针类型的意义非常重要——容易操作内存的数据,便于内存空间的使用。

若用指针实现:
创建一个整形数组,10个元素:
1、初始化数组的内容是1—10;
2、打印数组

#include <stdio.h>
int main()
{
	int arr[10] = { 0 };
	/*int* p = &arr;*///错误!!!
	int* p = arr;
//p存的是数组第一个元素的地址,p指针指向了0,
//p+1跳一个元素(跳过一个整形即跳过4个字节),通过指针可以改变数组内容。

	int i = 0;
//第一个元素是p+0得到的,第二个元素p+1可以得到,第三个元素p+2可以得到,……
//则i从0开始,p+i表示的就是下标为i元素的地址。*(p+i)就访问了一个元素内容。
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i + 1;
		printf("%d ", arr[i]);//1 2 3 4 5 6 7 8 9 10
	}

	printf("\n");

	//若是倒着打印:
	int* q = &arr[9];//p指向了最后一个元素,若不表示成9,则是arr[元素个数-1}——最后一个元素的内容

	for (i = 0; i < 10; i++)
	{
		//printf("%d ", *q);//10 9 8 7 6 5 4 3 2 1
		//q--;
		printf("%d ", *q - i);//10 9 8 7 6 5 4 3 2 1
	}
//整型指针--就跳回了一个整形,即跳回4个字节,指向前一个元素

	//若是一个字符一个字符的来:用char*类型指针访问
	int brr[10] = { 0 };
	char* pc = (char*)brr;
	return 0;
}

注意这里:

char* pc = (char*)brr;

这里类型有差异,进行了强制类型转化,访问的是起始元素四个字节的第一个字节
这里强制类型转化的是指针,不会溢出,只是地址类型转化了一下,值(整型指针和字符指针都是4个字节大小)并没有发生任何变化。
但如果是把大的数字放到小的空间中,则必然会发生数字的变化。
一个整型变量的地址强制类型转化成char*指针之后,它的类型发生了变化,地址没有发生任何变化,只不过强制类型转化之后是按强制类型转化之后的类型来理解的:如int*指针强制类型转化为char*指针,再对指针解引用时访问的是1个字节,+1时跳过1个字节,其他没有区别,地址是不会发生变化的。


3. 野指针

概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

3.1 野指针成因

1. 指针未初始化——会导致野指针。

#include <stdio.h>
int main()
{
	int a;//局部变量未初始化,默认为随机值
	//注:全局变量的值默认是0
	
	//这里的p指针称为野指针,这种指针实际没有明确指向的
	int* p;//p是局部变量指针未初始化,默认为随机值
	*p = 20;//p里放的是随机值,*p是通过p里存的随机值作为地址然后找了一块空间把20放进去
	printf("%d\n", a);
	return 0;
}

对于:

int* p;
*p = 20;

野指针就像野狗没有人管理,这条野狗没有主人,它没有绳子牵制是很危险的;类似,指针变量没有初始化,即这个指针没有指向任何对象。这里的20放到p里放的地址所指向的空间,但因为p里放的地址是个随机值,这个随机值找到的空间是不属于当前程序,所以这里是有问题代码,所以这里的p就是野指针。

2. 指针越界访问——会导致野指针。

#include <stdio.h>
int main()
{
	int arr[10] = { 0 };
	int* p = arr;
	int i = 0;
	for (i = 0; i <= 10; i++)
	{
//当指针指向的范围超出数组arr的范围时,p就是野指针
//数组名是首元素地址,p指向数组中10个元素里的第一个元素
		*p = i;
		p++;
	}
	return 0;
}

3. 指针指向的空间释放——会导致野指针。

#include <stdio.h>
int* test()
{
	int a = 100;
//报错:返回局部变量或临时变量的地址: a
	return &a;//返回的地址是a的地址,是整型的地址,所以返回类型就是int*
}
int main()
{
	int *p=test();//p里存的是a的地址,*p=a;
	printf("%d\n", *p);
	return 0;
}//100

这是错误的代码。

分析:

进入main()函数然后调用test()函数,进入test()函数,创建了局部变量a,a在内存中开辟了空间,里面放的是100,若假设a的地址是0x0012ff40,则返回的地址就是它,返回主函数后由p指针变量接收,(进入主函数的时候p变量就已经产生了),p得到的值是0x0012ff40,即p指向了a的空间,但是遗憾的是a变量有它自己的生命周期,进入它作用域的时候a创建空间,即a在内存中申请了4个字节的空间,是属于test()函数使用的,出作用域的时候,a的生命周期到了就会销毁,销毁的意思不是这块内存空间不在了而是这块内存空间的使用权限被收回去了,不在属于test()函数了,a是局部变量被销毁之后,p还记着这个地址还去访问这块空间,会出问题。当在主函数通过p里面是地址找到这块空间的时候,而访问的该空间已经不属于当前程序的了,此时p就是野指针。如果之前该空间的内容100没被改过则打印出100是正常的,如果里面的数据发生了改变打印不出100才是常态。

如打印不出100:

#include <stdio.h>
int* test()
{
	int a = 100;
	return &a;
}
int main()
{
	int *p=test();
	printf("hehe ");
	printf("%d\n", *p);
	return 0;
}//hehe 5

3.2 如何规避野指针

1. 指针初始化

①:指针初始化,即明确知道指针指向的对象。

int a = 10;
int* pa = &a;

希望pa指向a,取地址a放到pa变量里,pa就是整型指针,这里取地址a就是对指针变量pa的初始化。

②:不知道指向谁,通常给它赋值为空指针NULL。

int* p = NULL;

以上是常见的两种指针初始化形式,当知道明确指向谁的时候就存谁的地址,不知道指向谁的时候就指向空指针NULL。

指针初始化的时候一定要注意:

在使用指针之前一定要判断指针的有效性(指针不是空指针),只有在指针不是空指针的情况下才去使用这个指针。

#include <stdio.h>
int main()
{
	int a = 10;
	int* pa = &a;
	if (pa != NULL)
	{
//pa指向了一块有效的空间,不为空就进入if语句,此时就可以访问a,这里改变了a的内容改成了20
		*pa = 20;
	}

	int* p = NULL;
//这里p为空指针
	if (p != NULL)
	{
		*p = 20;//而p为空的时候这个if语句不会进入,这里没有问题
	}
	return 0;
}

总结:

对指针要初始化,要么是初始化为有效的地址,要么初始化为空指针,只有在指针不是空指针的情况下才去使用指针为空就不使用——做到相对安全。

注意理解:

int* p = NULL;

NULL是空指针,转到定义发现其实是0,或者是把0进行强制类型转化为void*, #define NULL ((void *)0),所以NULL本质上就是0,没有写成0而写成NULL是因为NULL更具有辨识度,更容易和理解。而:

int* p = NULL;
*p = 100;//这一行是错误的代码,警告:取消对NULL指针"p"的引用

指针变量p放的是0,0作为一个地址的时候找到这块空间放100进去时,这块空间是不允许访问的。只有当p不是空指针的时候才去访问它,即:

int* p = NULL;
if (p != NULL)
{
   *p = 100;//这里正确
}

创建空指针的意义:不赋值就没办法判断这个指针是否有效,赋值为NULL后知道了它当前不能被使用。当一个指针不用的时候可以赋值为空指针。

解引用的作用:通过指针里面的地址找它所指向的空间,然后给它赋值。

2. 小心指针越界

3. 指针指向空间释放及时置为空指针NULL

int arr[10] = { 0 };
int* q = arr;
//……用q访问这个数组

//当不想用q就把它赋值为空指针
q = NULL;

当不想使用一个指针的时候,就把它赋值为空指针。因为指针使用之前也都会判断一下,若为空就不使用了。

4. 避免返回局部变量的地址

5. 指针使用之前检查有效性

常见的错误一般都是指针使用不当或越界等导致的问题。


4. 指针运算

  1. 指针 +-  整数
  2. 指针 - 指针
  3. 指针的关系运算

4.1 指针+-整数

#include <stdio.h>
int main()
{
	int arr[10] = { 0 };
	int* p = arr;//用p访问arr数组
	int i = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);
	for (i = 0; i < sz; i++)
	{
		*(p + i) = i;//p是整型指针,p+i表示跳过i个整型找到数组i元素的地址
	}//整个过程中p没变,p还是那个地址
	
	for (i = 0; i < sz; i++)
	{
	    printf("%d ", *(p + i));//0 1 2 3 4 5 6 7 8 9
	}//*(p + i)访问的还是数组的元素

//倒着打印:
//如何能拿到数组最后一个元素的地址呢?
//数组名是arr,是首元素的地址,(首元素的地址+元素个数)表示跳过这么多元素个数个指向
//最后一个元素的后面空间,需要-1就可以访问到最后一个元素
	int* q = arr + sz - 1;//最后一个元素的地址赋给变量q;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *q);
		q--;
	}//9 8 7 6 5 4 3 2 1 0	
	
	//或者:
	
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *q--);
//这里是后置--,q是先解引用,使用,后--
	}//9 8 7 6 5 4 3 2 1 0

	//或者:
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(q - i));
	}//9 8 7 6 5 4 3 2 1 0
	return 0;
}

指针 +-  整数的底层原理:指针+1是加几个字节取决于指针的类型,如整型指针+1会跳过一个整型找到下一个整型,字符整型+1会跳过一个字符找到下一个字符。

4.2 指针-指针

指针-指针,可以得到中间的数据;但是指针+指针是没有意义的,所以没有这种运算。

#include <stdio.h>
int main()
{
	int a[10] = { 0 };
	printf("%d\n", &a[9] - &a[0]);
	printf("%d\n", &a[0] - &a[9]);
	return 0;
}
//9——高地址-低地址,是正数,中间有9个元素
//-9——小地址-大地址,是负数,中间有9个元素

两个地址相减之后得到值的绝对值是两个地址间元素的个数,有大小和正负的关系。

不是任意的指针就可以相减的,指针相减的前提是两个指针指向同一个元素。
指针-指针前提是:两个指针指向同一块空间(指向的是同一块连续的空间)

int a = 10;
char c = 'w';
&a - &c;//error

没办法计算,写法错误。(编译器可能不会报错,编译器也是人写的,能报错就报错,不报错也不一定是正确的,没检测之前不也会报错)

温故而知新一下:

#include <stdio.h>
#include <string.h>
//brr数组把数组名传给了s,因为数组名是数组首元素的地址,
//则s是a的地址,s存的是a的地址,s指向了a
int my_strlen_1(char* s)
{
	int count = 0;//计数器
	//while (*s != 0)//写法错误
	while(*s!='\0')
	{
		count++;
		s++;
	}
	return count;
}

int my_strlen_2(char* s)
{
	//*s就是a
	//把起始位置保存起来记为start,因为一会需要s++往后走则:
	//s是个变的值,这里start不变就是原来s的地址
	char* start = s;
	while (*s != '\0')
	{
		s++;
	}
	//当*s==\0,说明s指向了\0
	return s - start;
}

//求一个字符串长度:
int main()
{
//方法1:
	int len = strlen("abc");
	//int len = strlen(abc);//写法错误
	printf("%d\n", len);//3

	char arr[] = "abc";
	int ren = strlen(arr);
	printf("%d\n", ren);//3
	
//方法2:计数器的方法
	char brr[] = "abc";
	int ben = my_strlen_1(brr);
	printf("%d\n", ben);//3

//方法3:递归(之前学过并写过)这里不再写了

//方法4:指针-指针
	//思路:拿到首元素的地址,拿到\0的地址,\0的地址-首元素的地址就是元素的个数
	char crr[] = "abc";
	int cen = my_strlen_2(crr);
	printf("%d\n", cen);//3
	
	return 0;
}

4.3 指针的关系运算

指针的关系运算——两个指针比较大小

看一段代码:

#include <stdio.h>
int main()
{
	float values[N_VALUES];
	float* vp;
	for (vp = &values[N_VALUES]; vp > &values[0];)
	{
		*--vp = 0;
	}
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		printf("%.1lf  ", *(vp + i));
	}
	return 0;
}//0.0  0.0  0.0  0.0  0.0

分析代码:

#define N_VALUES 5//N_VALUES这个符号是5
float values[N_VALUES];//values这个数组有5个元素
float* vp;//这里指针vp没有初始化(在下面for循环中初始化了)

for (vp = &values[N_VALUES]; vp > &values[0];)//指针在这里初始化的:vp = &values[N_VALUES];
//vp被赋值为下标为5的元素的地址,而values数组中最高只有下标是4的元素,
//而这里只是指向(指向下标为5的元素的一块连续的空间),
//没有访问所以并没有越界
//所以这里vp只是存放了下标为5的元素的地址
//判断部分:vp > &values[0];为真
//这里for循环中调整部分省略掉了

//条件为真,进入循环内部:
{
	*--vp = 0;
//vp先--,vp是float型指针,-1则跳过一个float值;
//然后再解引用把0放进去
}
//这里实现了倒着把数组元素内容初始化为0
//这里涉及了两个指针的大小比较,即指针的关系运算

代码简化:

#define N_VALUES 5
#include <stdio.h>
int main()
{
	float values[N_VALUES];
	float* vp;
	for (vp = &values[N_VALUES]; vp >= &values[0];vp--)
	{
		*vp = 0;
	}
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		printf("%.1lf  ", *(vp + i));
	}
	return 0;
}//成功运行,但本电脑编译器提示数组values出错损坏

这样写代码易于理解,在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行

C语言标准规定:(严谨)

允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较;但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。

对此的理解:

这只是标准规定,若不按照此规定写代码就是标准未定义行为,这种代码在不同的编译器上会产生不同的结果,或者有些编译器根本不支持这种写法。
但是适当运用,不越界访问,这种代码一般不会产生太大问题。


5. 指针和数组

指针和数组的概念——不同
指针是地址,是指针变量;
数组是一组相同类型的数据。

指针和数组的大小——不同
指针变量的大小在32平台上是4个字节,在64平台上是8个字节,即无非书4或者8字节;
数组的大小创建相对随意。

数组和指针完全是两种事物,但二者总是会放在一起——因为数组在内存中开辟空间,就有了地址,通常就用指针的形式访问数组。

所以:

指针和数组的联系就是——可以通过指针来访问数组

#include <stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	//①arr——数组名——数组首元素的地址,所以赋给p
	int* p = arr;//p是指针变量,它的类型是int*
	//②因为数组在内存中是连续存放的
	//③因为整形指针p+1跳过一个整型,+1跳过一个整型……

	//所以:
	int i = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);
	for (i = 0; i < sz; i++)
	{
		//p存的是1的地址
		//p+i——
		//p+0,指向第一个元素1;1的下标为0
		//p+1,跳过一个整型指向2;2的下标为1
		//p+2,跳过两个整型指向3;3的下标为2
		//p+3,跳过三个整型指向4;4的下标为3
		//……
		//发现:下标为i的元素的地址与p+i相同,是一回事了
		printf("%p\n", p + i);
	}
	return 0;
}
#include <stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;
	int i = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);
	for (i = 0; i < sz; i++)
	{
		printf("%p == %p\n", p + i, &arr[i]);
	}
	return 0;
}
//运行结果:
//00CFFA20 == 00CFFA20
//00CFFA24 == 00CFFA24
//00CFFA28 == 00CFFA28
//00CFFA2C == 00CFFA2C……

所以,当得到数组首元素地址p的时候,p+i得到的就是下标为i元素的地址;所以当访问数组中下标为i的元素的时候,*(p+i)即可。这前提与上上述代码中注释①②③相互联系。

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

//这里发现数组名和指针变量p是一回事
//证明:
	for (i = 0; i < sz; i++)
	{
		printf("%d ", p[i]);//1 2 3 4 5 6 7 8 9 10
	}
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);//1 2 3 4 5 6 7 8 9 10
	}

	return 0;
}

因为数组名本来就是数组首元素的地址,把数组名放到p里,即p里放的也是数组首元素地址,所以数组名和指针变量p是一回事。

p[i]会被编译器编译成*(p+i)就是arr[i],这三者是等价的。
p[i] == *(p+i) == arr[i]

以上是数组和指针的基本关系——数组是通过指针的方式来访问的。


6. 二级指针

注意:

⑴、int* p;常见这样写,但int* p==int *p==int * p意义是一样的。
⑵、int *p1, *p2;连续定义多个指针这样写,若是int *p1, p2定义的是指针变量p1和整型变量p2。

#include <stdio.h>
int main()
{
	int a = 10;
	int* pa = &a;
	int** ppa = &pa;//ppa是二级指针
	int*** pppa = &ppa;//pppa是三级指针
	return 0;
}

a变量在内存中开辟了空间,存储(放进)10;
a的地址(假设是0x0012ff40)放到pa中,pa指向a,假设pa变量是4字节(32平台),pa要存放a的地址则pa也是一块空间,则pa变量在内存中有地址假设为0x0012ff48;
即pa变量有自己的地址,而又存放的别人的地址。是变量就可以取地址,拿到pa的地址即&pa拿到0x0012ff48放到ppa里,则ppa指向pa,ppa变量的类型就是int**,ppa就是二级指针:
若对变量ppa取地址即&ppa放到pppa变量里,则pppa指向ppa,pppa的类型就是int***,pppa就是三级指针。
所以,存在多级指针。

怎么理解多级指针的**呢?

int* pa = &a;
int** ppa = &pa;
int*** pppa = &ppa;

pa的前面有一颗*,这颗*说明pa是个指针,因为pa指向a的,所a的类型是int的时候,pa的类型就是int*——说明pa指向的是int类型的变量;
ppa的前面有两颗**,后一颗*是单独的,告诉我们ppa是指针,前面的*和int合起来是一组,因为ppa指向的是pa,而pa的完整类型是int*,所以这里有int*,(指针指向谁,*的前面就写什么类型);
pppa的前面有三颗***,最后一颗*是单独的,告诉我们pppa是指针,前面的int**合起来看为一组,pppa指向的对象是ppa(因为存的是ppa的地址),ppa的类型是是int**,所以这里是int***

画图分析:

二级指针是用来存放一级指针变量的地址的;
三级指针是用来存放二级指针变量的地址的。

对于二级指针的运算分析:
ppa是可以找到a的:
因为pa=*(ppa);
a=*pa;
所以a=(*(*ppa));
即是a=**ppa;

*ppa 通过对ppa中的地址进行解引用,这样找到的是 pa , *ppa 其实访问的就是 pa 。

int b = 20;
*ppa = &b;//等价于 pa = &b;

**ppa 先通过 *ppa 找到 pa ,然后对 pa 进行解引用操作: *pa ,那找到的是 a 。

**ppa = 30;
//等价于*pa = 30;
//等价于a = 30;
#include <stdio.h>
int main()
{
	int a = 10;
	int* pa = &a;
	int** ppa = &pa;
	**ppa = 20;
	printf("%d\n", a);//20
	return 0;
}

一般情况下,三级指针以上的指针很少使用。


7. 指针数组

指针数组是指针还是数组?
答案:是数组,是存放指针的数组。

int arr1[5];//整型数组——存放整型的数组,每个元素是int类型
char arr2[6];//字符数组——存放字符的数组,每个元素是char类型

所以:指针数组——存放指针(地址)的数组。

若存放整型指针则:

int* arr[5];

这里arr是数组名,它有5个元素,每个元素是int*了类型——所以是存放整型指针的数组,可以存放整型的地址。

int a = 10;
int b = 20;
int c = 30;
int* arr[5] = { &a, &b, &c };
//不完全初始化,其他两位默认是0,即空指针

可以通过arr找到a,b,c三个值。若只访问这个数组的前三个元素:

int i = 0;
for (i = 0; i < 3; i++)
{
	printf("%d ", *(arr[i]));//10 20 30
}

注意:0就是空指针,空指针会无法读取内存。


本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到