C语言指针(下)(指针深入理解)(野指针)(指针运算)(二级指针)

指针下篇,我想要把指针的基本知识全部说明白,所以本篇博客会比较长,有的部分文字也比较多,文字多有文字多的好处,可以帮助读者更加清楚的理解指针,篇幅较长,每一个举例代码都是在平台进行测试过的,希望读者也可以边阅读边测试理解过程。有什么问题可以在下面评论,多多指教。互相学习。

指针深入理解

我们已经在指针(中)篇介绍到了指针变量,凡是一个有类型的地址,都是可赋给同类型的指针变量。类型不同,编译器则可能会报出错误。
这里是一个很重要的一点,我们已经知道了可以自己定义一个指针类型的变量来存放指针
但是以下方式是不允许的:

char* p = (char*)0x12345678; //0x12345678 是数值,(char*)0x123456 是指针 
	*p = a;
	printf("%c", *p);

这种操作是不允许的,当然程序不会有任何结果,结果就是崩掉了,原因就是,我们整个内存并不是所有的空间在创建变量的时候能够访问到的,之后我们会讲到内存的具体分区。其中有一部分是操作系统的内核区,内核去的内容是不允许用户访问的,而如果你直接给一个指针一个地址号,那么这个地址号很有可能是用户不允许访问的内存区域,那么我们之前已经讲到,我们可以定义指针变量对于内存进行操作,那么如果是不允许访问的区域使用指针变量进行了修改,会带来致命性的错误,所以编译器直接禁止通过以上方式初始化指针变量(我们的初始化可以直接理解为赋值,只不过这里赋值的是一个有类型的地址及就是指针)所以通常作法是,把一个己经开辟空间的变量的地址赋给指针变量。

int a = 100;
	int b = 200;
	int* pa = &a;
	int* pb = &b;
	printf("*pa = %d\t&a = %x\n", *pa,&a);
	*pb = 200;
	printf("*pb = %d\t&b = %x\n", *pa, &b);

打印结果为:
在这里插入图片描述
那么在这里,我们已经通过以上打印找到了a和b的地址,刚好通过这里把指针用图解方式给大家演示一下,使得我们的认识更加清楚。
解释指针
那么这里我们只是标出来了变量的地址,并没有直接写出指针地址号,这里的指针a和b也一定是有地址的,因为我们说过,指针是一个变量,是变量就有地址和大小,指针存放的是其他变量的地址,我们直接说明就是,指针pa指向的是a,a是一个int类型,指针pa是一个int*,那么int便是寻址能力为int类型大小,所以指针pa能够寻址到的大小就和a的类型大小相等,一个是int 一个是int 。那么就可以通过pa访问到变量a的值,因为pa就是解引用,也就是取值,pa保存的是a的地址,所以就是取pa保存的地址的值。
那么指针保存的是变量地址,那么我们如果让指针保存的地址改变,那么相应的我们在解引用的时候取到的变量就会改变。

int num = 10;
	int num2 = 20;
	int *p = #
	printf("%d\n",*p);
	p = &num2;
	printf("%d\n",*p);

打印结果为:
在这里插入图片描述
只要指针保存的地址发生改变,那么解引用的是指,保存的变量必然也发生改变,保存的是那个变量的地址,解引用的时候就取到的是那个变量的值,这点很容易理解。就像你手机里面记住的是001房间,如果把001房间改成002房间,那么你寻找到的时候进入的就是002房间。

野指针

接下来我们介绍一种很危险的指针,操作不当造成的后果不可估量一个指针变量,如果,指向一段无效的空间,则该指针称为野指针,也称为无效指针。常见情型有两种,一种是未初化的指针,一种 是指向一种己经被释放的空间。

对野指针的读或许崩溃尚可忍受,对野指针的写入成功,造成的后果是不可估
量的。对野指针的读写操作,是危险而且是没有意义的。世上十之八九最难调的 bug 皆跟它有关系。

int *pa;
	printf("%x",*pa);

这样的操作会直接崩掉,但是造成的损失还不算很严重,因为只是读取内容,可能读取的内存地址不确定,很有可能读取到内核区域,操作系统会直接拦截。

int *pa;
	*pa = 100;
	printf("%x",*pa);

这个操作也会崩掉,但是这个操作影响就直接会修改对应位置的值,当你的位置不确定的时候,这是非常可怕的事情。所以操作系统也会直接拦截。崩溃的情况是正常的,系统会保护,不允许写入,因为一旦对于野指针写入成功了,这就是一个可怕的事情!!!
所以我们在定义指针的时候要有一个良好的习惯就是在定义的时候一定要把指针置为NULL
那么NULL是什么呢?
我们向定义指针为空,然后通过编译器跳转进行查看:

int *p = NULL;

在这里插入图片描述
上面解释是C++解释,但是这里理解不影响
在vs2019我们跳转到源码可以看到,NULL一个宏定义的 0 ,如果现在不知道是宏定义的话,那么就理解为0就可以。
NULL 是一个宏,俗称空指针,他等价于指针(void*)0。(void*) 0 是一个很特别的
指针,因为他是一个计算机黑洞,既读不出东西,也不写进东西去。所以被赋值 NULL
的指针变量,进行读写操作,是不会有内存数据损坏的。所以我们就通过NULL起一个标记的作用。
c 标准中是这样定义的:

define NULL ((void *)0) 

故常用 NULL 来给临时不需要初初始化的指针变量来进行初始化。或对己经被释
放指向内存空间的指针赋值。
可以理解为 C 专门拿出了 NULL(零值无类型指针),用于作标志位使用。
接下来我们说明一下void,我们在见到的或者自己用到的函数在定义返回值的时候,或者参数里面都会有用到void,具体的功能我们会在函数部分说明其功能,我们都知道char,int或者其他类型,void叫做无类型,我们首先提出
①:void*指针可以赋值给任何类型的指针
②:void代表内存的最小单元,那么最小单元就是一个字节

那么我们为什么没有用char类型来取代呢?将来有可能char一个字节不够用了,我们可能就要升级为2个字节,但是void永远是一个字节。我们强调一点:void在 32 位机上地位等同于 char。

printf("sizeof(char) = %d\n",sizeof(char));	
printf("sizeof(void) = %d\n",sizeof(void));

大家可以通过以上代码测试void的大小,但是并不是所有平台能都测出来,vs2019编译不能通过,Qt平台可以直接打印出来结果。我们在这里直接给出打印结果:
在这里插入图片描述
在这里我们需要强调一个小细节:
我们之前定义:

int a,b;

定义的是两个int类型的变量
如果我们:

Int * pa,pb;

那么这里,我们是不是顶一个两个指针变量呢?
我们通过测试他们的大小来进行说明。

int *p,q;
	printf("sizeof(p) = %d\n",sizeof(p));
	printf("sizeof(q) = %d\n",sizeof(q));

打印结果为:
在这里插入图片描述
两个类型大小相等,所以是两个指针变量,真实情况是这样吗?
我们改变以下类型:

char* p, q;
	printf("sizeof(p) = %d\n", sizeof(p));
	printf("sizeof(q) = %d\n", sizeof(q));

打印结果为:
在这里插入图片描述
说明前面是指针变量,后面是一个char类型变量,那么为什么会出现之前的结果呢?因为我们在32位机上面测试指针,大小为4个字节,int类型的大小也为4个字节,所以打印出来的结果相同。
所以我们如果要定义两个类型相同的指针变量就既可以这样:

char* p, *q;
	printf("sizeof(p) = %d\n", sizeof(p));
	printf("sizeof(q) = %d\n", sizeof(q));

打印结果为:
在这里插入图片描述
因为32位机指针的大小都是4个字节,所以char类型可以替换成任意的基本类型,打印结果都不会改变。

指针运算

我们开始的时候还是要再次学习一下指针常量和指针变量的区别和用法,在学习之前我们先区分一下之前在我们计算的过程中的一个很简单的概念:我们已经知道了在运算符中例如i++和++i的以及j–以及–j的使用(关于这两个运算符需要注意的点已经在运算符博客中关于用法及注意作以说明,此处下面还是会讲一下),这种运算我们都能够轻松的使用,那么这里我们就把这种方式类比到指针当中来更加清楚的理解指针常量和指针变量。
在说到i++和++i的使用的时候,我们通过多个变量给大家,我们在这里再给大家一种比较精简的验证方法大家可以自己分析结果,这里我们不给出解释,需要理解的话可以去(一维数组)博客有讲解。

int num = 10;
	int num1 = 10;
	num++;
	printf("%d\t", num);
	printf("%d\t", num1++);
	printf("%d", num1);

打印结果为:
在这里插入图片描述
指针运算包括,赋值,算术和关系
不兼容类型赋值会发生类型丢失。为了避免隐式转化带来可能出现的错误,最好用
强制转化显示的区别。下面我们作以说明 C语言比较灵活。例如以下:
我们在C语言这里可以直接这样写:

int data = 0x12345678;
	char* p = &data;
	printf("%d\n", *p);

在C语言中是可以运行的
但是在C++中必须这样:

int data = 0x12345678;
	char* p = (char *)&data;
	printf("%x\n", *p);

打印结果为:
在这里插入图片描述
接下我们介绍指针的算数运算:
指针的乘法运算和除法运算时没有意义的,有兴趣的读者可以自行测试。
在这里我们学习指针的加法运算和减法运算,但是在加法运算中,我们指针+指针也是没有意义的。
接下来我们说明指针的加法运算
首先我们给出这样的代码:

int* p = (int*)0x0001;
	int pData = 0x0001;
	printf("p = %x         p+1 = %x\n", p, p + 1);
	printf("pData = %x  pData+1 = %x", pData, pData + 1);

我们在这里的目的就是定义一个指针变量,然后打印出来指针变量+1的结果进行分析。
打印结果为:
在这里插入图片描述
这里我们对比来看,我们定义的数值加1是直接+1,指针加1的时候直接加上了4。
我们已经说过很多次:指针就是类型+地址,我们之前的解释是寻址能力,今天我们可以了解为步长(也就是一次+1能够变化的最小单元)也就是说,不同类型的变量占几个字节,+1之后就加上几个字节。我们通过代码进行解释:

int* p = (int*)0x0001;
	int pData = 0x0001;
	char* p1 = (char*)0x0001;
	short* p2 = (short*)0x0001;
	float* p3 = (float*)0x0001;
	double* p4 = (double *)0x0001;
	long long* p5 = (long long*)0x0001;
	printf("pData = %x  pData+1 = %x\n", pData, pData + 1);
	printf("(char *)p1 = %x         p1+1 = %x\n", p1, p1 + 1);
	printf("(short *)p2 = %x         p2+1 = %x\n", p2, p2 + 1);
	printf("(int *)p = %x             p1+1 = %x\n", p, p + 1);
	printf("(float *)p3 = %x         p3+1 = %x\n", p3, p3 + 1);
	printf("(double *)p4 = %x         p4+1 = %x\n", p4, p4 + 1);
	printf("(long long *)p5 = %x         p5+1 = %x\n", p5, p5 + 1);

打印结果为:
在这里插入图片描述
最上面直接是数字+1,pData是一个数字 char类型+1 short类型+1 float类型+1 double类型+1 long long类型+1。 我们再通过图解方式给大家解释:
在这里插入图片描述
那么接下来我们介绍指针的减法:
指针-1类似与指针+1 我们就不用过多解释。我们看一种新的情况
指针-指针:表示间隔的单元个数(正,负)
先算出间隔的字节数/sizeof(指针去掉一个
)
*
那么如果来解释呢
我们通过一维数组来解决:

int arr[10];//x
	int* p = &arr[1];//x+4
	int* q = &arr[9];//x+36
	printf("%d\n", p - q);//-8
	printf("%d\n", q - p);//8
	printf("%d\n", (short*)q - (short*)p);//16
	printf("%d\n", (long long*)q - (long long*)p);//4
	printf("%d\n", (double**)q - (double**)p);//8
	printf("%d\n", (char*)q - (char*)p);//32
	printf("%d\n", (long)q - (long)p);//32

我们在后面注释出来结果和我们打印出来的结果进行对比:
在这里插入图片描述
我们知道内存的编址顺序,当然也就很容易理解会出现负数,我们还是通过上面给出的方法计算和理解,间隔的单元个数,在数组中表示也就是中间间隔了几个数组。计算方法是,先计算出相差的字节数然后除以指针的类型大小,这里需要强调,既然是除以指针类型大小,那么这里必须首先是指针类型,然后才可以知道类型占几个字节。我们拆分上面代码逐个分析:
我们首先知道,int类型的数组,第一个元素和第九个元素相差32个字节。也就是8个int类型的变量。

printf("%d\n", p - q);//-8   
printf("%d\n", q - p);//8

那么这里为什么打印结果是8呢?我们刚才已经说过相差32个字节,当p-q,或者q-p的时候,我们就要用字节数除以去掉*之后的大小,那么我们定义的时候就是指针类型int *p和int q,去掉之后为int,所以在计算的时候就用 32/4 = 8;所以一个结果为8一个结果为-8。

printf("%d\n", (short*)q - (short*)p);//16
	printf("%d\n", (long long*)q - (long long*)p);//4

这里的我们也很容易解释:
间隔字节数为32 short去点* short大小为2个字节 所以是32/2 = 16
间隔字节数为32 long long去点* long long大小为8个字节 所以是32/8 = 4

printf("%d\n", (double**)q - (double**)p);//8
	printf("%d\n", (char*)q - (char*)p);//32

这里我们需要特别注意:double** 去掉一个之后为double 而double是4个字节
所以结果为 32/8 =8。
间隔字节数为32 char去点
cjar大小为2个字节 所以是32/1 = 32

printf("%d\n", (long)q - (long)p);//32

最后一点需要我们更加注意强转里面没有*可以去掉,那么不管里面是什么类型全部按照1字节处理所以结果为32/1 == 32

int arr[10];//x
	int* p = &arr[1];//x+4
	int* q = &arr[9];//x+36
	printf("%d\n", (long)q - (long)p);//32
	printf("%d\n", (int)q - (int)p);//32
	printf("%d\n", (short)q - (short)p);//32
	printf("%d\n", (char)q - (char)p);//32

打印结果为:
在这里插入图片描述
得以证明。
我们这里帮助理解一下:
指针+i的含义:+i个格子,则为指针+isizeof(指针去掉一个)个字节
指针-i的含义:-i个格子,则为指针-isizeof(指针去掉一个)个字节
如果直接没有*直接当作1个字节

   int *p = (int *)2000;
printf("%d\n",p+1);//2004
printf("%d\n",(char *)p+1);//2001
printf("%d\n",(long *)p+1);//2004
printf("%d\n",(float *)p+1);//2004
printf("%d\n",(double *)p+1);//2008
printf("%d\n",(short **)p+1);//2004
printf("%d\n",(char ***)p+1);//2004
printf("%d\n",(unsigned long long)p+1);//2001

打印结果为:
在这里插入图片描述
再举一个例子进行理解:

int* p = (int*)0x2010;
	printf("%x\n", p - 2);//2008
	printf("%x\n", (short*)p - 2);//200c
	printf("%x\n", (long*)p - 2);//2008
	printf("%x\n", (float*)p - 2);//2008
	printf("%x\n", (double*)p - 2);//2000
	printf("%x\n", (char**)p - 2);//2008
	printf("%x\n", (char*)p - 2);//200e
	printf("%x\n", (unsigned long)p - 2);//200e

打印结果为:
在这里插入图片描述
P-1 还是之前的int类型, char**去掉一个之后还是一个指针类型所以大小为4字节,追后一行没有*,所以不管是什么类型,都只是一个字节(我们之前有例子专门说明和解释)
我们已经在这里用很多例子说明这一点,读者可以多次复习查看不断理解。
我们在这里需要注意很重要的一点就是:只有当指针指向一串连续的存储单元时,指针的移动才有意义。才可以将一个指针变量与一个整数 n 做加减运算。
关系运算
我们先讨论一下指针相等的问题
指针相等有两个条件:
①类型相同
②数值相等

我们通过代码来验证:

int * p = (int *)0x0001;
	int * q = (int *)0x0005;
	if (p + 1 == q)
	{
		printf("p+1 = q");
	}
	else
	{
		printf("p+1 != q");
	}

打印结果为:
在这里插入图片描述
说明两者是相等的。
p+1本质就 p + 1sizeof(int) 也就是把之前的类型大小。
我们再给出一些例子:


```c
char * p = (char *)0x0001;
	int * q = (int *)0x0001;
	if (p == q)
	{
		printf("p+1 = q");
	}
	else
	{
		printf("p+1 != q");
	}

这个代码,我们可以看到是写在C++里面的,我们这里只考虑是否相等。

char * p = 0x0001;
	int * q = 0x0001;
	if (p == q)
	{
		printf("p+1 = q");
	}
	else
	{
		printf("p+1 != q");
	}

这个代码是在C语言里面执行的,执行的结果为相等。可以理解为char转为int,所以我们尽量避免不同类型的之间的比较。
我们再给出下面的例子:

int *ptrnum1, *ptrnum2; 
int value = 1; 
ptrnum1 = &value; 
value += 10; 
ptrnum2 = &value; 
if (ptrnum1 == ptrnum2) 
printf("\n 两个指针指向同一个地址\n"); 
else 
printf("\n 两个指针指向不同的地址\n");

打印结果为:
在这里插入图片描述
这个对于我们理解的程度来说已经很基础了,不管数值怎么变化,只要地址不变,两个指针指向同一个地址是可以的,就像不同的手机里面可以存同一个人的电话号码。但是不允许同一个电话号码表示两个人,否则就会出错,指针类似,通过一个指针指向不能同时指向两个地址,这也很容易理解。
指针的大小比较这里就不多说明,我们已经知道了内存的编址方式,指针的比较就很容易理解了。

二级指针

接下来我们了解以下二级指针
我们定义一级指针的时候用:
int num = 10;
Int * p = #
printf(“%d”,p);
我们知道一级指针保存的是变量的地址,我们之前也已经提到过,指针也是变量,也有地址,那么二级指针就是存储一级指针的地址,我们通过图解方式加以理解:
我们先给出代码:
int a = 10;
int b = 20;
int
p = &a;
int* q = &b;
int** pp = &p;
int** qq = &q;
printf("*p = %d\n",*p);
printf("**pp = %d\n", ** pp);
printf("*q = %d\n", *q);
printf("qq = %d\n", qq);
打印结果为:
在这里插入图片描述
下来我们给出图解:
在这里插入图片描述
二级指针我们只是在这里提出,具体的应用会在数据结构的知识讲解到,这里我们首先见到知识作为补充理解,之后很多知识都会用到,所以我们必须在开始的这一部分就要理解清楚,为之后更加深入的学习和应用做准备。

后我们给出两个需要特变注意的点:

指针的运算只能发生在同类型或整型之间,否则会报错或是警告。
2.指针的运算,除了数值以外,还有类型在里面。

到这里所有C语言指针的内容就已经讲完,整个内容比较多,读者可以多次阅读复习,不断理解。有问题的地方也欢迎大家在评论区留言一起交流学习。

发布了84 篇原创文章 · 获赞 71 · 访问量 9086

猜你喜欢

转载自blog.csdn.net/qq_43648751/article/details/102848150