【问题标题】:In C, are arrays pointers or used as pointers?在 C 中,数组是指针还是用作指针?
【发布时间】:2011-06-04 04:12:41
【问题描述】:

我的理解是数组只是指向一系列值的常量指针,当你在 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);

当使用 Apple GCC 编译时,结果如下:

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

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

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

【问题讨论】:

标签: c arrays pointers


【解决方案1】:

这是来自 C 标准 (n1256) 的确切语言:

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

这里要记住的重要一点是,object(在 C 术语中,意思是占用内存的东西)和用于引用的 expression 之间是有区别的到那个对象。

当你声明一个数组比如

int a[10];

表达式 a 指定的对象 是一个数组(即,一块大到足以容纳 10 个 int 值的连续内存块),而表达式 a 的类型是“int 的 10 元素数组”,或 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

IOW,前进p 将sizeof int (4) 添加到原始值,而前进ap 添加10 * sizeof int (40)。

更标准的语言:

6.5.2.1 数组下标

约束

1 一个表达式的类型应该是''pointer to object type'',另一个表达式应该是整数类型,结果的类型是''type''。

语义

2 后缀表达式后跟方括号[] 中的表达式是数组对象元素的下标名称。下标运算符[]的定义是E1[E2]等同于(*((E1)+(E2)))。由于适用于二进制+ 运算符的转换规则,如果E1 是一个数组对象(等效地,一个指向数组对象初始元素的指针)并且E2 是一个整数,E1[E2] 指定E2-E1 的第一个元素(从零开始计数)。

因此,当您为数组表达式下标时,幕后发生的事情是计算与数组中第一个元素的地址的偏移量并取消引用结果。表达式

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(); 

这里有一个方便的表格来记住其中的一些概念:

声明:Ta[N]; 表达式类型转换为值 ---------- ---- ------------ ----- a T [N] T * a中第一个元素的地址; 等同于写 &a[0] &a T (*)[N] 数组的地址;值是一样的 同上,但类型不同 sizeof a size_t 数组中包含的字节数 对象(N * sizeof T) *a[0] 处的 T 值 a[i] a[i] 处的 T 值 &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] a[i][j] 处的 T 值 &a[i][j] T * a[i][j] 的地址 声明:T a[N][M][O]; 表达式类型转换为 ---------- ---- ------------ 一个 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]

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

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

【讨论】:

    【解决方案2】:

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

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

    与此相关的另外几个问题:

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

    此外,获取数组中元素数量的常用习惯用法是使用如下宏:

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

    这有一个问题,即接受一个数组名称,它会在哪里工作,或者一个指针,它会在没有编译器警告的情况下给出一个无意义的结果。存在更安全的宏版本(特别是对于 C++),当它与指针而不是数组一起使用时会生成警告或错误。请参阅以下 SO 项:


    注意:C99 VLA(可变长度数组)可能不遵循所有这些规则(特别是,它们可以作为被调用函数已知的数组大小的参数传递)。我对 VLA 的经验很少,据我所知,它们并没有被广泛使用。但是,我确实想指出,上述讨论可能对 VLA 有所不同。

    【讨论】:

    • 所以 sizeof 例外是因为它很有用......我不知道实际上有一种方法可以知道数组的大小! (虽然它仍然不是那么有用,因为它只能找到具有固定大小的数组的大小,但我想它比为同一目的定义许多常量要好)
    【解决方案3】:

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

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

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

    【讨论】:

      【解决方案4】:

      在

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

      和

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

      在第一个实例(折扣对齐)中,将为 hello 存储 12 个字节,分配的空间初始化为 hello there,而在第二个实例中,hello there 存储在其他位置(可能是静态空间)和hello 被初始化为指向给定的字符串。

      hello[2] 和 *(hello + 2) 在这两种情况下都将返回“e”。

      【讨论】:

        【解决方案5】:

        【讨论】:

          【解决方案6】:

          如果'y'是一个常量指针,为什么它的大小是20,就像它指向的值序列一样?

          因为z 是变量的地址,并且总是为你的机器返回8。您需要使用解引用指针 (&) 来获取变量的内容。

          编辑:两者之间的区别:http://www.cs.cf.ac.uk/Dave/C/node10.html

          【讨论】:

          • 他在问y,而你在回答z,这令人困惑。很清楚为什么 z 的大小为 8。OP 不清楚为什么 y 没有;对此你没有回答。此外,&amp; 是 C 语言中的地址运算符;解引用运算符是*。
          猜你喜欢
          • 2011-04-24
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2014-08-15
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-02-18
          相关资源
          最近更新 更多