上一篇涉及指针指向的数据类型的大小,有没有一种可能,数组也是一种数据类型,那么一个数组有多大呢?

前置知识:

  1. 指针类型和指针运算
  2. 一维数组
  3. sizeof的使用

数组的大小

C语言提供的数组用于存储特定个数的相同类型元素,每个元素都有着相同的大小(占用的字节数),数组作为一个整体,当然也有着其大小,显然,数组的大小==元素的大小*数组元素的个数.

例如有int a[10];数组的元素为int类型,占用4个字节(sizeof(int)),那么整个数组a就占用40个字节(sizeof(a)):

1
2
3
4
5
6
7
#include <stdio.h>

int main() {
int a[10] = {0}; // 使用大括号初始化, 未初始化的元素默认为0
printf("%llu\n", sizeof(a)); // 40
return 0;
}

输出为40,说明数组的总长度就是所有元素加起来的总长度.

使用指针来访问数组

前面已经看到了简单的例子,让一个指针指向数组中的某个元素,并且随着指针的自增/自减,指针的指向按顺序向后或向前移动到相邻的元素.

我们围绕使用指针访问数组这一话题来编写示例代码.

那么,如果我们使用一个循环来让指针逐次自增,那么就可以做到访问数组的效果:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main() {
// 那么,如果我们使用一个循环来让指针逐次自增,那么就可以做到访问数组的效果:
int a[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int *p = &a[0]; // p指向数组a的第一个元素
for (int i = 0; i < 10; ++i) {
printf("%d ", *p); // 输出当前p指向的元素
++p; // p指向下一个元素
}
return 0;
}

这个程序中使用一个循环变量i来控制循环.考虑到指针变量也可以进行比较,我们可以这样控制循环:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main() {
// 那么,如果我们使用一个循环来让指针逐次自增,那么就可以做到访问数组的效果:
int a[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int *p = NULL; // p初始化为NULL
for (p = &a[0]; p < &a[0] + 10; p++) {
printf("%d ", *p);
}
return 0;
}

这种写法没有引入变量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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

int main() {
int a[5] = {1, 2, 3, 4, 5};
// 我们在使用一个指针来访问数组的时候,例如遍历一个数组,需要对指针进行赋值,一般标准的写法是:
int *p = &a[0];
// 但是我们也可以使用一种简单的写法:
int *q = a;
// 这里发生了一个隐式转换,数组a隐式转换为指向数组的第一个元素的指针,也就是&a[0],所以可以这样来进行赋值
for (int i = 0; i < 5; i++) {
printf("%d ", p[i]);
}
printf("\n");
for (int i = 0; i < 5; i++) {
printf("%d ", q[i]);
}
return 0;
}

这是因为该处发生了一次数组到指针的转换,即数组a隐式转化为指向其首元素的指针,指向数组的第一个元素,既然是"指向数组的第一个元素",那么其类型自然就是int *,所以我们可以直接使用表达式a来进行赋值.

这就是数组名的退化问题,它会"退化"为指向其首元素的指针.实际上是一个隐式类型转换.不仅是在这种情景,包括函数传参也会发生.

根据cppreference中描述,当不是以下语境的一种时,就会发生这种转换,结果为非左值:

  • 作为取地址运算符的操作数
  • 作为sizeof运算符的操作数
  • 作为typeof和typeof_unqual的操作数
  • 作为用于数组初始化的字符串字面量

根据此特性,我们前面所有的&a[0]都可以直接用a来替代;同理,&a[0]+10这样的表达式也可以直接换成a+10进行简化.读者请自行尝试.

指针数组

数组可以存储一系列特定类型的元素,指针类型自然也在其列.我们可以声明一个指针数组,存储一系列指针.

声明指针数组

我们可以这样声明一个长度为4的int指针数组,来存储4个int变量的指针:

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main() {
int i=1,j=2,k=3,l=4;
int *p[4] = {&i,&j,&k,&l}; // 依次初始化为指向i,j,k,l
for(int i=0;i<4;++i)
printf("%d ",*p[i]);
return 0;
}

运行结果如下:

image-20240114213748862

需要注意的是,int *p[4]有优先级的问题,由于下标运算符[]的优先级高于指针运算符*,因此p首先与[]结合,代表它是一个数组,然后int *指明p的每一个元素都是一个int类型的指针.

后面printf()中的*p[i]也是一样,p[i]先获取第i个元素的值,他是一个指针,然后使用*解引用来访问其指向的int变量.

其他的指针数组同理,不再赘述.

数组指针

既然数组也是一种复合的数据类型,那么自然可以(应该)有一种指针可以指向一个数组.也就是数组指针,亦即(指向)数组(的)指针.

声明数组指针

若我们有一个int a[5];则使用如下方式声明并初始化一个数组指针指向a:

1
2
int a[5] = {1, 2, 3, 4, 5};
int (*p)[5] = &a; // 注意这里的括号是必须的!

由于下标运算符[]的优先级比指针运算符*高(当然这里的各个运算符是用于声明的,而不是表达式求值),因此为了说明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
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

int main() {
// 看一个例子,使用一个数组指针遍历数组的各个元素
int a[5] = {1, 2, 3, 4, 5};
int (*p)[5] = &a;
for (int i = 0; i < 5; i++) {
printf("%d ", (*p)[i]); // *p就是a,因为[]的优先级高于*,所以要加括号
// printf("%d ",a[i]); // 也可以直接用a[i]来访问,和上面的等价
}
// (*p)[i]和*p[i]完全不同,前者是访问p指向的数组的第i个元素;后者则是访问p+i指向的元素,
// 即整个数组a后面的第i个数组,这个数组是不存在的,所以发生了越界访问
return 0;
}

必须注意的是,(*p)[i]*p[i]完全不同,前者是访问p指向数组的第i个元素;后者则是访问p+i指向的元素,即整个数组a后面的"第i个数组",这个数组是不存在的,所以会发生越界访问.

数组指针指向二维数组的每一行

如果a不是一个一维数组,而是一个二维数组,那么可以使用一个(一维)数组指针指向a的每一行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

int main() {
// 数组指针指向二维数组的每一行
int a[3][4] = {{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}};
int (*p)[4] = a; // p指向a的第一行,这里a作为表达式,会退化为指向第一行(第一个元素)的指针
// int (*p)[4] = &a[0]; // 与上面等价
// 每一行都是一个长度为4的一维数组,所以p是一个指向长度为4的一维数组的指针
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++)
printf("%d ", (*p)[j]);
printf("\n");
p++; // 指向下一行,p的值实际上加了4个int的长度,即16
}
return 0;
}

使用p++让p指向下一个数组,也就是说,p的值足足增加了16,因为p指向的数组总大小为4*sizeof(int)也就是16个字节.

需要注意对p初始化的那行代码,前面说的数组名的退化问题同样适用于二维数组,由于二维数组就是数组的数组,即其元素为一维数组,那么这里的数组首元素的地址就是二维数组第一行的首地址&a[0],其类型为int (*)[4].


数组和指针的基本关系如上,当然他们的联系不仅仅于此.

上面讲述的指针数组数组指针,读者务必分清楚其区别,两者没有任何关系.

注:下面的文章内容有误

如果你对于这方面仍然感到困惑,可以阅读这篇文章:

指针和数组的关系

---WAHAHA,2023.9.24



上一篇:C语言教程-13_2-指针类型与指针运算

下一篇:C语言教程-13_4-函数指针