在C语言中,数组是指针还是被用作指针?

62

我理解的是数组只是指向一系列数值的常量指针,当你在 C 中声明一个数组时,你实际上是声明了一个指针并为其所指向的序列分配空间。

但这段代码让我困惑:

char y[20];
char *z = y;

printf("y size is %lu\n", sizeof(y));
printf("y is %p\n", y);
printf("z size is %lu\n", sizeof(z));
printf("z is %p\n", z);

使用苹果 GCC 编译得到以下结果:

y size is 20
y is 0x7fff5fbff930
z size is 8
z is 0x7fff5fbff930

(我的机器是64位的,指针长度为8个字节)。

如果“y”是一个常量指针,为什么它的大小为20,就像它所指向的值序列一样?在编译时,变量名“y”是否被替换为内存地址,只要适当就可以进行替换?那么数组是否是C语言中某种语法糖,只是在编译时被转换为指针相关的东西?


2
可能是网站C++-FAQ中的一个问题的重复:在C语言中,数组名是指针吗? - Steve Jessop
可能是重复的问题:什么是数组到指针衰变? 但是,那个问题被标记为C和C++,所以答案也包含了C ++的元素。因此,那个问题可能不是一个好的重复问题。 - Andreas Wenzel
可能是重复的问题:数组衰减不会发生的例外情况? - Andreas Wenzel
6个回答

88

这里是C标准文件(n1256)中的确切语言:

6.3.2.1 左值,数组和函数设计者
...
3 除非它是sizeof运算符或一元&运算符的操作数,或者是用于初始化数组的字符串字面量,否则具有类型“type数组”的表达式将转换为具有类型“指向数组对象初始元素的type指针”的表达式,且不是左值。如果数组对象具有寄存器存储类,则行为未定义。

重要的是要记住,在C术语中,有一个区别在于对象(即占用内存的东西)和用于引用该对象的表达式之间。

当您声明一个数组时,例如

int a[10];

由表达式a指定的对象是一个数组(即足以容纳10个int值的连续内存块),而表达式a的类型为"10个元素的int数组",或int [10]。如果表达式a出现在除sizeof&运算符的操作数之外的上下文中,则其类型会隐式转换为int *,其值为第一个元素的地址。

对于sizeof运算符,如果操作数是类型为T [N]的表达式,则结果是数组对象中的字节数,而不是指向该对象的指针:N * sizeof T

对于&运算符,该值是数组的地址,与数组的第一个元素的地址相同,但是表达式的类型不同:给定声明T a[N];,则表达式&a的类型为T (*)[N],或指向T的N元素数组的指针。该a&a[0]相同(数组的地址与数组中第一个元素的地址相同),但是类型的差异很重要。例如,给定代码:

int a[10];
int *p = a;
int (*ap)[10] = &a;

printf("p = %p, ap = %p\n", (void *) p, (void *) ap);
p++;
ap++;
printf("p = %p, ap = %p\n", (void *) p, (void *) ap);

你将看到类似以下的输出

p = 0xbff11e58, ap = 0xbff11e58
p = 0xbff11e5c, ap = 0xbff11e80

我解释一下,当你使用下标操作符访问数组元素时,实际上计算了该元素相对于数组首地址的偏移量,并将该偏移量加到首地址上,从而得到该元素的地址。例如,在表达式 p[i] 中,p 是一个指向数组第一个元素的指针,i 是要访问的元素的索引。因此,p+i 计算出了第 i 个元素的地址,然后 p[i] 就可以得到该元素的值。需要注意的是,p+i&p[i] 是等价的。
a[i] = 10;

等同于

*((a)+(i)) = 10;

相当于

*((i)+(a)) = 10;

等同于

 i[a] = 10;

是的,在C语言中,数组下标是可交换的;但是请不要在生产代码中这样做。由于数组下标是基于指针操作定义的,因此可以将下标运算符应用于指针类型和数组类型的表达式:
int *p = malloc(sizeof *p * 10);
int i;
for (i = 0; i < 10; i++)
  p[i] = some_initial_value(); 

下面是一个方便的表格,可以帮助您记住一些概念:

声明: T a[N];
表达式 类型 转换为 值 ---------- ---- ------------ ----- a T [N] T * a的第一个元素的地址; 相当于写 &a[0] &a T (*)[N] 数组的地址; 值与上面相同,但类型不同 sizeof a size_t 数组对象中包含的字节数 (N * sizeof T) *a T a[0]处的值 a[i] T a[i]处的值 &a[i] T * a[i]的地址
声明: T a[N][M];
表达式 类型 转换为 值 ---------- ---- ------------ ----- a T [N][M] T (*)[M] 第一个子数组的地址(&a[0]) &a T (*)[N][M] 数组的地址(与上面的值相同,但类型不同) sizeof a size_t 数组对象中包含的字节数(N * M * sizeof T) *a T [M] T * a[0]的值,即第一个子数组的第一个元素的地址(与&a[0][0]相同) a[i] T [M] T * a[i]的值,即第i个子数组的第一个元素的地址 &a[i] T (*)[M] 第i个子数组的地址; 值与上面相同,但类型不同 sizeof a[i] size_t i的子数组对象中包含的字节数(M * sizeof T) *a[i] T 第i个子数组的第一个元素的值(a[i][0]) a[i][j] T a[i][j]处的值 &a[i][j] T * a[i][j]的地址
声明: T a[N][M][O];
表达式 类型 转换为 ---------- ---- ----------- a T [N][M][O] T (*)[M][O] &a T (*)[N][M][O] *a T [M][O] T (*)[O] a[i] T [M][O] T (*)[O] &a[i] T (*)[M][O] *a[i] T [O] T * a[i][j] T [O] T * &a[i][j] T (*)[O] *a[i][j] T a[i][j][k] T

从这里开始,更高维数组的模式应该很清楚了。

因此,总结一下:数组不是指针。在大多数情况下,数组表达式会被转换为指针类型。


拜托了,千万别在正式代码中这样做。哈哈! - Lance E.T. Compte

26

数组并不是指针,尽管在大多数表达式中,数组名会被评估为指向数组第一个元素的指针。因此,非常容易将数组名用作指针。您经常会看到术语“衰减”用于描述这一点,例如“数组衰减为指针”。

一个例外是作为sizeof运算符的操作数,其中结果是数组的大小(以字节为单位,而不是元素)。

与此相关的另外一些问题:

函数的数组参数是虚构的 - 编译器实际上传递了一个普通指针(这不适用于C ++中的引用数组参数),因此您无法确定传递给函数的数组的实际大小 - 您必须以其他方式传递该信息(可能使用显式附加参数或使用类似C字符串的哨兵元素)

此外,获取数组元素数量的常见习惯用法是使用类似以下的宏:

#define ARRAY_SIZE(arr) ((sizeof(arr))/sizeof(arr[0]))

这个问题在于它可以接受数组名称,这样它就能正常工作,或者指针,这样它就会给出一个无意义的结果,而编译器不会发出警告。有更安全的宏版本(特别是针对C++),当它与指针而不是数组一起使用时,将生成警告或错误。请参见以下SO项目:
注意:C99的可变长度数组(VLAs)可能不遵循所有这些规则(特别是,它们可以作为参数传递,并且被调用函数知道数组大小)。我对VLAs的经验很少,据我所知,它们并不广泛使用。但是,我想指出上述讨论可能会在VLAs上有所不同。

所以 sizeof 异常是因为它很有用... 我不知道实际上有一种方法可以知道数组的大小!(虽然它仍然不太有用,因为它只能找到具有固定大小的数组的大小,但我想这比为相同目的定义许多常量要好) - salvador p

6

sizeof 在编译时计算,编译器知道操作数是数组还是指针。对于数组,它给出了数组占用的字节数。你的数组是一个 char[]sizeof(char) 为 1),因此 sizeof 恰好给出了元素的数量。要在一般情况下获取元素的数量,常见的惯用语是(这里以 int 为例):

int y[20];
printf("number of elements in y is %lu\n", sizeof(y) / sizeof(int));

对于指针,sizeof 给出了原始指针类型所占用的字节数。


3

char hello[] = "hello there"
int i;

并且

char* hello = "hello there";
int i;

首先(不考虑对齐),将为hello存储12个字节,分配的空间初始化为hello there,而在第二个实例中,hello there存储在其他地方(可能是静态空间),并且hello被初始化为指向给定字符串。

hello[1]以及*(hello + 1)在两种情况下都将返回e


2

-2
如果'y'是一个常量指针,为什么它的大小像它所指向的值序列一样是20呢?
因为'z'是变量的地址,并且对于您的机器始终返回8。您需要使用解引用指针(&)才能获取变量的内容。
编辑:两者之间的区别很好:http://www.cs.cf.ac.uk/Dave/C/node10.html

他在询问关于y的问题,而你却回答了关于z的问题,这让人感到困惑。很明显z为什么有大小为8,但是对于OP来说,y为什么没有大小还不清楚;你也没有给出答案。此外,在C语言中,&是地址运算符,而解引用运算符是*。 - Peter - Reinstate Monica

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接