【无痛C指针】——初阶篇

目录

前言

一、剧情回顾

二、指针和指针类型

1.指针+-整数

2.指针的解引用

三.野指针

3.1野指针的成因

1.指针未初始化

2. 指针越界访问

3. 指针指向的空间释放

3.2 如何规避野指针

1. 指针初始化

2. 小心指针越界

3. 指针指向空间释放即使置NULL

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

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

四.指针运算

1.指针-指针

2.指针的关系运算 

五.二级指针

总结


 


前言

大家好呀,我们又见面了,这次我带来了大家期待已久的指针初阶篇,在这一章节里面我们会继续深入学习指针,当然难度会略有提升哦,并且内容量也将提升,建议收藏后反复观看。那么废话不多说,开始学习这一章的内容吧。


 

一、剧情回顾

在讲今天的内容前我们先来回顾一下入门篇里面讲的内容吧。学习了入门篇后相信大家对内存有了一定的了解,为了有效的使用内存,我们把内存划分成一个个小的内存单元,每个内存单元都有一个编号,而这些编号就是地址。为了存储地址就需要用到我们的指针,而我们口语中说的指针通常指的是指针变量,可以用来存储地址。详细内容可以看看我前面发的入门篇。

二、指针和指针类型

我们都知道,变量有不同的类型,整形(int),浮点型(float)等。那指针有没有类型呢? 准确来说是有的,在上一章里面我们就提到过,下面我就来详细讲解一下。

请大家看这几个代码

char  *p1 = NULL;
int   *p2 = NULL;
short *p3 = NULL;
long  *p4 = NULL;
float *p5 = NULL;
double *p6 = NULL;

这里可以看到,指针的定义方式是: 类型 + * + 变量名,至于这个NULL是什么后面会讲到,这里先重点讲解指针的类型。

例如:char *p1,类型就是char*,* 号表示 p1(变量名)是一个指针。就这样由 类型(需要存储的那个地址的类型) + * + 变量名组成了一个指针。

每个不同类型的指针,都用来存储与自己类型相同的变量的地址。

例如:

char* 类型的指针是为了存放 char 类型变量的地址。

short* 类型的指针是为了存放 short 类型变量的地址。

int* 类型的指针是为了存放 int 类型变量的地址。

但是在上一章中我们也说过,在64位平台下指针的大小固定位8byte,在32平台下指针的大小固定位4byte,既然无论是什么类型的指针,它的大小都是固定的,而指针又只是用来存储一个地址的,为什么还要搞这个多种类型呢?用一个类型的指针来存储所有类型变量的地址不好吗?

下面我就来给大家解释一下为什么会有这么多的指针类型。

1.指针+-整数

大家看见标题后,肯定马上就会猜到我们要讲什么了,我们接下来就要介绍当一个指针(也就是指针变量p)加上或者减去一个整数,会出现什么现象呢?

我们先来看这段代码:watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN5LiA5qC355qE54Of54GrYQ==,size_19,color_FFFFFF,t_70,g_se,x_16

 这里的%p表示打印的是一个地址(就像%d说明打印的是一个整形)。由上面的代码,我们可以看到整形指针p1存储着整形变量n的地址(p1 = &n),(注意:千万不要把p1的地址当成了n的地址,p1是一个指针变量,它存储着n的地址,然后内存中又有一个地址处单独存放着p1这个变量,就像整形变量n存储着整形数字6,内存中有个地址处放着n这个变量。这是一个易错点大家要注意哦。)这时候我们接着讲,我们看见p1打印出来的结果是00BDF7B0,当我们将p1加1的时候,我们再打印p1+1这个地址为00BDF7B4,我们发现地址居然直接跳过了4个字节(byte)(相当于直接跳过了4个地址),而后面的p2+1跳过了1个地址,p3+1跳过了8个地址。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN5LiA5qC355qE54Of54GrYQ==,size_20,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN5LiA5qC355qE54Of54GrYQ==,size_20,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN5LiA5qC355qE54Of54GrYQ==,size_18,color_FFFFFF,t_70,g_se,x_16上面我们介绍的是用与变量相同类型的指针来存储该变量的地址时的情况。(注意,我这里说的地址都是变量所占字节的第一个字节的地址。)那如果我们用一种类型的指针来存储不同类型变量的地址,然后再将指针加1会发生什么现象呢?接下来让我们看看下面这个代码。

 watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN5LiA5qC355qE54Of54GrYQ==,size_15,color_FFFFFF,t_70,g_se,x_16

 上面这个代码里面,我们用char类型的指针来存储int类型变量的地址。发现将指针变量(p)加1后,地址只向后移动了一位。(注意:这里将int类型a的地址存储在char*类型的指针里面需要将&a强转换成char类型变量的地址,将(char*)放在&a前面就可以将&a强转换成char类型变量的地址,如果不这么写的话还是可以运行,但是编译器会出现像下面这样的警告。)

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN5LiA5qC355qE54Of54GrYQ==,size_13,color_FFFFFF,t_70,g_se,x_16

 将&a强转换成char*类型就不会出现警告了。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN5LiA5qC355qE54Of54GrYQ==,size_19,color_FFFFFF,t_70,g_se,x_16

 

这时候我们就可以得出一个结论,指针的类型决定了指针向前或者向后走一步有多大(距离)。(char*类型指针+1向后移动1个地址,int*类型的指针+1向后移动4个地址,double*类型的指针+1向后移动8个地址。)这就是指针类型的一个用法。

2.指针的解引用

我们在上一章里面也介绍过指针的解引用但只是入门介绍而已,下面我们来深入了解一下指针的解引用。我们先来看看这段代码

这是将pa解引用前的结果:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN5LiA5qC355qE54Of54GrYQ==,size_20,color_FFFFFF,t_70,g_se,x_16

 将pa解引用后:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN5LiA5qC355qE54Of54GrYQ==,size_20,color_FFFFFF,t_70,g_se,x_16

我们这时发现,将pa解引用后,并将0赋值给*pa,a的值直接变成了0。这就说明int*类型的指针解引用后访问的是4个字节(byte)的内容,也就是4个地址。 

我们又来看看下面这个代码:(我们用char*类型的指针pb来存储int类型变量a的地址)

这是将pb解引用前的代码:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN5LiA5qC355qE54Of54GrYQ==,size_20,color_FFFFFF,t_70,g_se,x_16

 这是将pb解引用之后的代码:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN5LiA5qC355qE54Of54GrYQ==,size_20,color_FFFFFF,t_70,g_se,x_16

 我们这时发现,将pb解引用后,并将0赋值给*pb,a变量中的数据只被改变了1个字节(byte),而后面3个字节(byte)的数据没有被改变,这是因为我们是用char*类型的指针来存储int类型变量的地址,所以pb就把存储在它这里的地址当成了char类型变量的地址,于是pb解引用就只访问1个字节(byte)的地址,pb解引用也就只能找到这1个地址处存储的数据。所以就只改变了a里面1个字节(byte)的数据,剩下3个字节(byte)都不改变。至于为什么a里面的数据是一个字节一个字节倒着存储的,可以去看看我的另外一篇文章数据的存储——C语言里面很详细的介绍了数据的存储。

总结: 指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。 比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。

三.野指针

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

3.1野指针的成因

1.指针未初始化

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN5LiA5qC355qE54Of54GrYQ==,size_7,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN5LiA5qC355qE54Of54GrYQ==,size_13,color_FFFFFF,t_70,g_se,x_16

 

像上面这样就是指针未初始化,局部变量指针未初始化,默认为随机值,p里面就存储着一个随机的地址,虽然我们解引用后能找到这个地址的空间,但是这个空间不是我们的,我们如果访问这个空间的话就是非法访问,至于这么解决呢,我们后面马上就会将到。

2. 指针越界访问

在讲解指针越界访问前,我先给大家讲讲指针和数组的之间有什么联系,因为一般的初学者都是学完数组再来学指针的,所以我在这里就当大家已经学过数组了。

我们来看看这段代码:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN5LiA5qC355qE54Of54GrYQ==,size_20,color_FFFFFF,t_70,g_se,x_16

学过数组的都知道数组名arr表示数组首元素的地址,只有两种情况是例外:

1. sizeof(数组名),计算整个数组的大小,sizeof内部单独放一个数组名,数组名表示整个数组。

2. &数组名,取出的是数组的地址。&数组名,数组名表示整个数组。

除此1,2两种情况之外,所有的数组名都表示数组首元素的地址。

在上面的代码中我们把数组首元素的地址放到一个指针里面,然后将指针慢慢+1,运行后发现打印出来指针里面存储的地址和数组中每个元素的地址是一样的。这是因为整形指针每次+1都是跳过4个字节,而我们的数组又是整形数组,数组中每个元素都是占4个字节的整形,最开始我们的指针里面存储着数组首元素的地址,每次指针+1都跳过4个字节,刚好跳到数组下一个元素的地址。

明白了这个后,那我们就可以直接通过指针来访问数组。例如下面这个代码:

 watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN5LiA5qC355qE54Of54GrYQ==,size_20,color_FFFFFF,t_70,g_se,x_16

上面这个代码我们就通过指针将数组中所有的元素都赋值成了0。

说完了数组和指针之间的联系,我们这个时候就可以来说说指针越界访问了

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN5LiA5qC355qE54Of54GrYQ==,size_19,color_FFFFFF,t_70,g_se,x_16

上面这个代码就是指针越界访问的经典案例。下面为图解:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN5LiA5qC355qE54Of54GrYQ==,size_20,color_FFFFFF,t_70,g_se,x_16

 当指针指向的范围超出数组arr的范围时,p就是野指针

3. 指针指向的空间释放

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN5LiA5qC355qE54Of54GrYQ==,size_10,color_FFFFFF,t_70,g_se,x_16

 在上面这个代码里面,我们创建了一个返回一个地址的函数test(),我们进入test函数的时候创建了局部变量a,当这个函数结束的时候返回a的地址到main()函数里面,然后我们在main()函数里面创建了一个指针p来存储a的地址,然后又将p解引用访问了a地址处的空间。虽然这看上去没上面问题,但是编译器会报警告。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN5LiA5qC355qE54Of54GrYQ==,size_19,color_FFFFFF,t_70,g_se,x_16

 这里的警告说test()函数返回的是局部变量a的地址,我们知道局部变量是在一个函数开始的时候被创建,当该函数结束的时候这个变量就会被销毁,意思就是说test()函数返回的是一个被销毁的变量的地址,a地址处的空间在test()函数中可以被访问,出了test()函数后就不能被访问了。当我们在main()函数里面用指针将它存储起来,然后又去访问这个地址处的空间就属于非法访问了。这时这个指针就是一个野指针。

3.2 如何规避野指针

1. 指针初始化

如果你明确自己要存储一个地址的话就可以像下面这样创建一个指针。这样就将p初始化了

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN5LiA5qC355qE54Of54GrYQ==,size_8,color_FFFFFF,t_70,g_se,x_16​  

 如果说你想先创建一个指针,等以后在用的话,你就可以像下面这样创建一个空指针。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN5LiA5qC355qE54Of54GrYQ==,size_8,color_FFFFFF,t_70,g_se,x_16

NULL就表示0,0是一个地址,而0地址是不能被访问的,也就是说空指针是不能被访问的,如果访问的话编译器就会报错。当你哪天需要用到p这个指针的时候,你将你需要存储的地址赋给p就可以了,这时p这个指针就可以用了。

下面就是这种用法:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN5LiA5qC355qE54Of54GrYQ==,size_20,color_FFFFFF,t_70,g_se,x_16

 像这样就可以很有效的避免野指针的产生 。

2. 小心指针越界

就像上面讲指针越界访问的时候,指针不能一直无限的+1,指针只能在指定的区域内访问,注意这点也可以减少野指针的出现。

3. 指针指向空间释放即使置NULL

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN5LiA5qC355qE54Of54GrYQ==,size_15,color_FFFFFF,t_70,g_se,x_16​  

就像上面这个代码,你开始想让指针p指向a的地址,后面你玩腻了,不想让p指向a的地址时,你就可以将p设置为空指针。 

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

刚刚上面已经说过返回局部变量的地址的案例,后面你只需要多多注意即可。

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

指针在使用前,要先确定一下该指针是不是空指针,如果是空指针的话就不要用。

注意到以上几点就可以很好的减少野指针的出现。

四.指针运算

1.指针-指针

上面我们介绍了指针+-整数,这会儿我们来说一说指针-指针是什么。

语法规定的,指针减指针可以得到两个指针中间的元素个数。(注意:得到的不是字节数哦)

 

例如:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN5LiA5qC355qE54Of54GrYQ==,size_20,color_FFFFFF,t_70,g_se,x_16

 

 这里的&arr[9]和&arr[0]就相当于指针,因为他们都是地址。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN5LiA5qC355qE54Of54GrYQ==,size_18,color_FFFFFF,t_70,g_se,x_16

如果把这个数组的首元素单独存储在一个指针里面可能能会更容易理解一点。就像下面这个代码。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN5LiA5qC355qE54Of54GrYQ==,size_20,color_FFFFFF,t_70,g_se,x_16

 

 这样写估计大家就明白为什么这个指针减指针等于9了。

2.指针的关系运算 

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

这句话是什么意思呢,我们先来看看下面这两个代码,并由我为大家讲解。

代码1:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN5LiA5qC355qE54Of54GrYQ==,size_20,color_FFFFFF,t_70,g_se,x_16

 

代码2:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN5LiA5qC355qE54Of54GrYQ==,size_20,color_FFFFFF,t_70,g_se,x_16

 

 

我们这里可以看到,两个代码的for循环条件不同,内部执行的情况也不同,但是运行的结果却是一样的。而是我们更建议代码1的写法,下面我来解释这是为什么

代码1:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN5LiA5qC355qE54Of54GrYQ==,size_15,color_FFFFFF,t_70,g_se,x_16

 

代码2: 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN5LiA5qC355qE54Of54GrYQ==,size_15,color_FFFFFF,t_70,g_se,x_16

通过图解,我们可以发现两个代码的区别就在于p进循环所指向的位置和循环结束时p所指向的位置不同 。

代码1中p一进循环所指向的位置是arr[5]的地址,但是数组最后一个元素的地址是arr[4]的地址,这时p就是指向数组最后一个元素后面的那个内存的位置。看起来好像是p越界访问了,但是p没有对该地址解引用,对p解引用的时候先将p减1了,这时候p就指向了arr[4]的地址,所以是没有越界访问的。循环结束时p指向的位置是arr[0]的地址。这里最关键的一点就是用&arr[0]跟数组最后一个元素后面的那个内存位置的指针进行了比较。

代码2中p一进循环所指向的位置是arr[4]的地址,循环结束时p指向的位置是arr[-1]的地址,这时p就是指向数组第一个元素之前的那个内存的位置。看起来好像是p越界访问了,但其实p还是没有越界访问,因为在p访问这个地址之前,这个循环就已经结束了。这里最关键的一点就是用&arr[0]跟数组第一个元素之前的那个内存位置的指针进行了比较。

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

 这就是我推荐代码1的原因了,但是为什么代码2还是可以运行呢?这是因为:实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。

这就是指针的关系运算的介绍。

五.二级指针

既然指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里?指针变量的地址就存在二级指针里面,我们称我们最开始学的指针为一级指针(也就是存储普通变量的地址的指针),二级指针就是存储一级指针的地址的指针。

例如下面这个图解:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN5LiA5qC355qE54Of54GrYQ==,size_20,color_FFFFFF,t_70,g_se,x_16

 在图解上面我们看到整形二级指针的写法是 int**。下面我来详细写一些二级指针的写法。

下面为步骤:

步骤1:因为ppa是一个指针所以先写成 *ppa,*号代表ppa是一个指针,这还是和一级指针一样。

步骤2:我们知道ppa是一个指针后,我们就需要知道这个指针存储的地址是什么类型的,我们知道这个指针是存储一级整形指针地址的,所以它的存储的地址的类型就是int*。我们将int*加在*ppa前面就变成了int**ppa,而int**ppa就是二级指针,这也遵循一级指针的定义方式,也就是  (类型(需要存储的那个地址的类型)+ * + 变量名)。还可以根据这样的形式定义出三级指针,四级指针,但是这些都不常用,了解就可以了。后面我们还会介绍更多类型的指针,定义方式也跟这个差不多。

我们将二级指针解引用后就可以得到一级指针存储的内容,例如:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5LiN5LiA5qC355qE54Of54GrYQ==,size_17,color_FFFFFF,t_70,g_se,x_16

这里我们就可以看到这个代码结果就跟上面那个图解一样。

ppa = &pa,*paa = &a,**ppa = a。 

 这就是二级指针的讲解。

这就是初阶篇的全部内容了,期待后续文章的小伙伴可以顺手点点关注,以免后面就找不到了。


 

总结

在这一章里面,我们又更进一步的学习了指针。如果觉得对你有用的话,希望能点点免费的赞哦,感谢大家,最后祝考研的同学都能上岸,找工作的小伙伴都能进大厂。

 

  • 47
    点赞
  • 51
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 45
    评论
评论 45
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不一样的烟火a

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值