C语言教程-13_3-初探指针和数组的关系
上一篇涉及指针指向的数据类型的大小
,有没有一种可能,数组也是一种数据类型,那么一个数组有多大呢?
前置知识:
- 指针类型和指针运算
- 一维数组
- sizeof的使用
数组的大小
C语言提供的数组
用于存储特定个数的相同类型元素,每个元素都有着相同的大小(占用的字节数),数组作为一个整体,当然也有着其大小,显然,数组的大小==元素的大小*数组元素的个数.
例如有int a[10];
数组的元素为int类型,占用4个字节(sizeof(int)
),那么整个数组a就占用40个字节(sizeof(a)
):
1 |
|
输出为40,说明数组的总长度就是所有元素加起来的总长度.
使用指针来访问数组
前面已经看到了简单的例子,让一个指针指向数组中的某个元素,并且随着指针的自增/自减,指针的指向按顺序向后或向前移动到相邻的元素.
我们围绕使用指针访问数组
这一话题来编写示例代码.
那么,如果我们使用一个循环来让指针逐次自增,那么就可以做到访问数组的效果:
1 |
|
这个程序中使用一个循环变量i
来控制循环.考虑到指针变量也可以进行比较,我们可以这样控制循环:
1 |
|
这种写法没有引入变量i
,而是使用指针运算来控制循环.
&a[0]
为第一个元素的地址(类型为int*),那么&a[0]+10
即指向最后一个元素的后一个地址(注意下标为0-9).
换句话说,&a[0]+9
指向最后一个元素,而&a[0]+10
则不指向数组中的元素,而是指向数组后面的一个不存在的元素.我们可以计算这个不存在的元素的地址,但是不能访问该地址,因为该地址并不属于数组,它可能存储任何值,并且对其进行访问可能导致程序崩溃.对于我们的p来说,&a[0]+10
是一个不允许访问的地址.
在循环中,当p的值小于&a[0]+10
时说明仍然指向存在的元素,但是在最后一次循环后,p的值将自增到&a[0]+10
,此时p和&a[0]+10
相等,循环结束.
应该意识到,我们说这个地址不允许访问
,和不允许存储是两回事,p可以存储该值(本例中自增到该值),但是切记这时不允许出现*p
解引用操作,即不允许访问该地址.
注:实际上,访问这个地址称为访问溢出
,无意地访问溢出可能会导致意想不到的结果,甚至会导致访问到禁止访问的内存导致程序崩溃.有关此问题的细节请移步栈溢出
获取详细内容.
数组名的退化问题
该问题十分重要,其实也可以认为是C语言的一个设计缺陷.
我们在使用一个指针来访问数组的时候,例如遍历一个数组,需要对指针进行赋值,经常可以发现有2种写法:
1 |
|
这是因为该处发生了一次数组到指针的转换
,即数组a隐式转化为指向其首元素的指针,指向数组的第一个元素,既然是"指向数组的第一个元素",那么其类型自然就是int *
,所以我们可以直接使用表达式a
来进行赋值.
这就是数组名的退化问题
,它会"退化"为指向其首元素的指针.实际上是一个隐式类型转换.不仅是在这种情景,包括函数传参也会发生.
根据cppreference中描述,当不是以下语境的一种时,就会发生这种转换,结果为非左值:
- 作为取地址运算符的操作数
- 作为sizeof运算符的操作数
- 作为typeof和typeof_unqual的操作数
- 作为用于数组初始化的字符串字面量
根据此特性,我们前面所有的&a[0]
都可以直接用a
来替代;同理,&a[0]+10
这样的表达式也可以直接换成a+10
进行简化.读者请自行尝试.
指针数组
数组可以存储一系列特定类型的元素,指针类型自然也在其列.我们可以声明一个指针数组,存储一系列指针.
声明指针数组
我们可以这样声明一个长度为4的int指针数组,来存储4个int变量的指针:
1 |
|
运行结果如下:
需要注意的是,int *p[4]
有优先级的问题,由于下标运算符[]
的优先级高于指针运算符*
,因此p
首先与[]
结合,代表它是一个数组,然后int *
指明p的每一个元素都是一个int类型的指针.
后面printf()中的*p[i]
也是一样,p[i]
先获取第i个元素的值,他是一个指针,然后使用*
解引用来访问其指向的int变量.
其他的指针数组同理,不再赘述.
数组指针
既然数组也是一种复合的数据类型,那么自然可以(应该)有一种指针可以指向一个数组.也就是数组指针
,亦即(指向)数组(的)指针
.
声明数组指针
若我们有一个int a[5];
则使用如下方式声明并初始化一个数组指针指向a:
1 | int a[5] = {1, 2, 3, 4, 5}; |
由于下标运算符[]
的优先级比指针运算符*
高(当然这里的各个运算符是用于声明的,而不是表达式求值),因此为了说明p是一个指针,需要使用括号()
来改变优先级.
首先(*p)
说明p是一个指针,接下来就是确定p指向什么类型的变量.因为我们要让p能够指向a,也就是一个长度为5的int数组,所以p指向的类型应该是int [5]
,而根据C语言的声明语法,[5]
应该放到标识符p的后面,所以最终的声明就是int (*p)[5]
.
需要注意,这个长度需要给出,因为编译器需要确定指向的数组究竟有多长(见指针指向的数据类型的大小
一节),否则这个声明将会是一个不完整的类型
,后续会给出例子.
这个声明指出,p是一个指向长度为5的int数组的指针.如果这样不够清晰,换个语序:
p是一个指针,它指向的数据类型为"长度为5的int数组".
解引用数组指针
既然p是指向数组的指针,那么对p解引用的结果自然就是一个特定长度的数组,注意这个长度必须给出.
看一个例子,使用一个数组指针遍历数组的各个元素:
1 |
|
必须注意的是,(*p)[i]
和*p[i]
完全不同,前者是访问p指向数组的第i个元素;后者则是访问p+i指向的元素,即整个数组a后面的"第i个数组",这个数组是不存在的,所以会发生越界访问.
数组指针指向二维数组的每一行
如果a不是一个一维数组,而是一个二维数组,那么可以使用一个(一维)数组指针指向a的每一行:
1 |
|
使用p++
让p指向下一个数组,也就是说,p的值足足增加了16,因为p指向的数组总大小为4*sizeof(int)
也就是16个字节.
需要注意对p
初始化的那行代码,前面说的数组名的退化问题
同样适用于二维数组,由于二维数组就是数组的数组,即其元素为一维数组,那么这里的数组首元素的地址
就是二维数组第一行的首地址&a[0]
,其类型为int (*)[4]
.
数组和指针的基本关系如上,当然他们的联系不仅仅于此.
上面讲述的指针数组
和数组指针
,读者务必分清楚其区别,两者没有任何关系.
注:下面的文章内容有误
如果你对于这方面仍然感到困惑,可以阅读这篇文章:
---WAHAHA,2023.9.24
下一篇:C语言教程-13_4-函数指针