计算机初级选手的成长历程——指针(3)

初阶指针

计算机初级选手的成长历程——指针(3)_C语言

导言

大家好,很高兴又和大家见面啦!!!

经过前面两个篇章的介绍,大家对于指针的基础内容应该都已经掌握了。在上一篇中我们还通过对strlen函数的模拟实现来强化了对指针运算的使用。

不知道大家在使用指针和数组时有没有疑惑一个问题:数组名代表的是数组首元素的地址,指针存储的也是地址,数组在传参是可以通过指针来接收,那它们两个究竟是什么关系呢?

在今天的章节中,咱们就会给大家揭晓指针与数组之间的爱恨情仇。

六、指针和数组

对于数组名,我们在数组的篇章中有介绍过以下的知识点:

  1. 数组名存放的是数组首元素的地址;
  2. 当使用sizeof(arr_name)来计算数组大小时,此时的数组名代表的是整个数组;
  3. 当使用&arr_name来取地址或者是传参时,此时的数组名代表的是整个数组;
  4. 我们可以使用arr_name[num]来通过下标num访问数组的各个元素;

有细心的小伙伴就会发现,在数组篇章中,我曾给出过自己从指针的角度来看待数组名时,对数组名的理解:

  • 数组名此时就相当于是存放了首元素地址的一个指针,所以我们可以通过数组名来访问元素的地址。

那究竟是不是这样呢?下面我们来做一个测试——我们来通过指针+下标来访问数组的元素:

计算机初级选手的成长历程——指针(3)_指针数组_02

可以看到,此时我们是可以通过指针+数组下标来访问数组元素的;

之所以能通过指针访问数组元素,就是因为数组元素在内存中是从低地址到高地址进行连续存放的,相邻两个地址之间的差值刚好是一个数据元素对应的数据类型所占空间大小。在前面进行模拟实现strlen的时候咱们也证实过了。

通过这个例子,我们就能得到结论:

数组名[下标]<==>*(指针+下标)

下面我们来进行第二个测试,通过sizeof来分别计算数组名与指针:

计算机初级选手的成长历程——指针(3)_指针数组_03

可以看到,在sizeof中数组名此时代表的是整个数组,但是,在sizeof中指针代表的依旧是数组首元素的地址;

那如果我们将整个数组的地址存入指针又会如何呢?

计算机初级选手的成长历程——指针(3)_指针数组_04

可以看到,此时就算是将整个数组的地址存放进指针,通过sizeof计算的指针大小依旧是一个元素所占空间的大小;

这是为什么呢?如果数组名就是指针的话,那我们此时应该是计算的整个数组大小所占空间大小才对呀?下面为了解开这个疑惑,我们通过图像来进行理解:

计算机初级选手的成长历程——指针(3)_二级指针_05

在反汇编界面我们找到了指针、变量以及数组的位置,接下来我们通过这里提供的信息来分析一下:

  • 对于指针来说,它们指向的只能是一个地址;
  • 但是对于数组名来说,我们通过取地址操作符取出来的是整个数组的地址,只不过这个地址是数组的起始地址;
  • 在sizeof中我们可以看到

存储的是14h,转换成十进制就是20;

存储的都是4,也就是一个元素所占空间大小;

从前面的知识中我们已经知道的是:

  • sizeof在计算空间大小时,计算的是操作对象所占空间的大小;
  • 指针此时指向的是数组首元素的地址;

也就是说数组首元素的地址并不能代表整个数组,但是数组名也是数组首元素地址,用sizeof计算数组名时却是得到的整个数组所占空间大小,这个又怎么解释呢?

对于这个问题,我们可以理解为用sizeof计算数组名时,数组名表示的是数组所占空间的起始地址

指针

指向的是整个数组的起始地址,而我们通过sizeof计算

时,计算的是起始地址所占空间大小

计算机初级选手的成长历程——指针(3)_指针数组_11

如上图所示,sizeof在计算数组名时,计算的实质上是数组中5个元素所占空间的大小总和,在计算指针时,计算的实质上是数组首元素所占空间大小;

因此对于指针与数组的关系我们就有了以下结论:

  • 数组名[下标]<==>*(指针+下标);
  • 在代表数组首元素地址时,数组名<==>指针;
  • 在计算数组所占空间大小时
  • 数组名代表的是数组所占空间大小的起始地址,计算空间大小时是从起始地址开始计算,计算的是整个数组所占空间大小
  • 指针指向的是数组所占空间大小的起始地址,计算空间大小时计算的是起始地址所占空间大小

现在我们已经介绍完了指针与数组之间的关系,不知道大家有没有注意,在上面的例子中的反汇编界面的截图中,对于指针

来说,它们自己所在的空间也是有一个地址,并且地址并不相同,唯一相同的是地址里存放的内容,都是数组首元素的地址。

对于变量也好、数组也好,它们的地址我们可以存放在指针中,那如果我想存放指针的地址,又应该怎么处理呢?

下面我们就来介绍一下存放指针地址的指针——二级指针;

七、二级指针

指针变量实质上也是一个变量,既然是变量,那如果存放地址,也是需要存放在指针中,只不过唯一不同的是,存放指针变量的指针我们将其称为——二级指针;

7.1 二级指针的创建

二级指针与指针一样,都是通过type* name的格式进行创建的,只不过有区别的是此时的name是指针,所以它本身带有一颗*,所以二级指针的创建格式为:

代码语言:javascript
复制
//二级指针的创建格式
type** name;
//type——指针对应的数据类型
//type*——指针类型,这里的*代表此时的变量类型是指针类型
//*name——指针变量名,这里的*代表此时的变量是一个指针
//name——变量名

在知道创建的格式后,我们就来尝试着创建一个二级指针变量;

代码语言:javascript
复制
//二级指针的创建
int main()
{
	int a = 10;
	//将a的地址存放进一级指针中
	int* pa = &a;
	//将一级指针pa的地址存放进二级指针中
	int** ppa = &pa;
	return 0;
}

现在我们就创建好了一个二级指针。此时可能就有朋友的思维开始发散了——既然存放一级指针的指针为二级指针,那存放二级指针的指针是不是就是三级指针,以此类推,就能得到四级指针、五级指针……

这个想法非常正确,对于三级指针、四级指针以及多级指针的创建就是如二级指针一样,如下所示:

代码语言:javascript
复制
//二级指针的创建
int main()
{
	int a = 10;
	//将a的地址存放进一级指针中
	int* pa = &a;
	//将一级指针pa的地址存放进二级指针中
	int** ppa = &pa;
	//将二级指针ppa的地址存放进三级指针中
	int*** pppa = &ppa;
	//将三级指针pppa的地址存放进四级指针中
	int**** ppppa = &pppa;
	//……
	return 0;
}

这时我们都是可以通过解引用操作得到变量a中存放的内容:

计算机初级选手的成长历程——指针(3)_二级指针_14

多级指针与一级指针相比,只是多了解引用的次数,本质上都是一样的——存放地址的变量。但是我们几乎很少使用到二级指针以上的指针,所以对于多级指针,大家只需要理解其实质以及知道如何创建就行。

7.2 二级指针的工作原理

既然二级指针存放的是指针的地址,那我们又应该怎么使用它呢?为了搞清楚这个问题,我们首先要了解二级指针的工作原理:

计算机初级选手的成长历程——指针(3)_指针和数组_15

二级指针存放的是一级指针的地址,所以我们对二级指针进行第一次解引用时找到的是一级指针存放的内容——变量a的地址; 对二级指针进行第二次解引用时,就相当于对一级指针进行解引用,所以找到的是变量a存放的内容; 因此我们可以得到结论:

既然存放指针地址的指针被称为一级指针,那么如果有一个数组它的数组元素为指针,那这样的数组又是什么呢?

八、指针数组

数组的定义是一组相同数据类型元素的集合,那如果元素的数据类型为指针类型,这样的集合就被称为——指针数组

指针数组,本质上是一个数组,只不过此时的数组元素为指针

那对于这个数组元素为指针的数组,我们应该如何创建呢?

8.1 指针数组的创建

一个数组的创建格式为:

代码语言:javascript
复制
//数组的创建格式
type arr_name[size];
//type——数组元素的数据类型
//type[size]——数组的数据类型
//arr_name——数组名
//size——数组大小

现在我们已经知道了数组元素的数据类型为指针类型,也就是type*,也就是说对于指针数组的创建格式应该是:

代码语言:javascript
复制
//指针数组的创建格式
type* arr_name[size];
//type*——数组元素的数据类型
//type*[size]——数组的数据类型
//arr_name——数组名
//size——数组大小

如果我需要创建一个数组名为arr,数组大小为3的整型指针数组,那我就可以根据格式创建这个指针数组:

代码语言:javascript
复制
//指针数组的创建
int main()
{
	int* arr[3];
	//int*——数组元素类型为整型指针类型
	//int*[3]——数组类型为大小为3的整型指针类型
	//arr——数组名
	//[3]——数组大小/数组元素个数
	return 0;
}

现在我们已经创建好了一个整型指针数组,接下来我们就需要对这个数组进行初始化了。

8.2 指针数组的初始化

对于指针数组的初始化我们有三种方式:

  • 通过指针变量进行初始化;
  • 通过取地址操作符进行初始化;
  • 通过空指针NULL进行初始化;

如下所示:

代码语言:javascript
复制
//指针数组的初始化
int main()
{
	int a = 1;
	int b = 2;
	int* pa = &a;//变量a的指针
	int* arr[3] = { pa,&b,NULL };//指针数组初始化
	//NULL——空指针
	return 0;
}

指针数组在进行初始化时,有以下三种情况:

  1. 如果明确数组的各个元素,可以通过指针变量或者取地址的方式进行完全初始化
  2. 只知道数组的部分元素,并进行不完全初始化时,未被初始化的元素会自动初始化为空指针(NULL)
  3. 对数组元素不明确时,我们可以通过空指针(NULL)进行初始化

下面我们通过监视窗口来看一下这三种情况下的初始化:

计算机初级选手的成长历程——指针(3)_指针数组_18

从监视窗口中我们可以看到:

对于完全初始化的指针数组arr1来说,数组内的元素都是明确的; 对于不完全初始化的arr2和arr3来说,不管是通过指针变量、取地址还是空指针对其进行不完全初始化,未被初始化的元素会自动初始化为空指针; 对于未初始化的arr4来说,指针内存放的值为0xcccccccc这样的随机值,并且这个随机值的类型为int*; 也就是说,此时的地址为一个随机的地址,前面我们提到过,当地址为随机值时,此时它时野指针;

我们现在也知道野指针会引发的问题,所以为了避免指针数组内存放的为野指针,我们就必须要对指针数组进行初始化。

结语

现在大家应该对指针和数组、二级指针与指针数组有了一个初步的认识了,接下来为了让大家更加深刻的理解指针的相关内容,我们将开始进一步剖析指针,对指针的内容进行深入的探讨。大家记得关注哦!

最后感谢各位的翻阅,咱们下一篇再见!!!