10

总是这样吗,我的意思是,数组名称始终是指向数组第一个元素的指针。为什么会这样?它是某种实现还是语言特性?

4

3 回答 3

16

数组名本身不是指针,而是在大多数情况下衰减为指向数组第一个元素的指针。之所以如此,是因为语言是这样定义的。

从 C11 6.3.2.1 Lvalues, arrays, and function designators开始,第 3 段:

除非它是运算sizeof符、运算符_Alignof或一元&运算符的操作数,或者是用于初始化数组的字符串字面量,否则类型为“array of type ”的表达式将转换为类型为“pointer to type ”的表达式" 指向数组对象的初始元素,不是左值。

您可以从comp.lang.c FAQ的Arrays and Pointers部分了解有关此主题的更多信息(以及许多有关所涉及的微妙行为)。

抛开社论不谈:C++ 中也会发生同样的行为,尽管语言对其的规定略有不同。作为参考,来自我在这里的 C++11 草案,4.2 Array-to-pointer conversion,第 1 段:

N T“array of ”或“array of unknown bound of ”类型的左值或右值T可以转换为“pointer to T”类型的右值。结果是指向数组第一个元素的指针。

于 2013-08-29T18:18:50.387 回答
10

这种行为的历史原因可以在这里找到。

C 源自一种名为 B 的早期语言(见图)。B 是一种无类型语言,内存被视为“单元”的线性数组,基本上是无符号整数。

在 B 中,当您声明一个 N 元素数组时,如

auto a[10];

为数组分配了 N 个单元,并留出了另一个单元来存储绑定到变量 的第一个元素的地址a。与在 C 中一样,数组索引是通过指针算法完成的:

a[j] == *(a+j)

这在 Ritchie 开始向 C 中添加结构类型之前工作得很好。他在论文中给出的示例是一个假设的文件系统条目,它是一个节点 id 后跟一个名称:

struct {
  int inumber;
  char name[14];
};

他希望 struct 类型的内容与磁盘上的数据相匹配;2 个字节表示整数,后跟 14 个字节表示名称。没有好地方来存放指向数组第一个元素的指针。

所以他摆脱了它。他没有为指针留出存储空间,而是设计了这种语言,以便从数组表达式本身计算指针值。

顺便说一句,这就是为什么数组表达式不能成为赋值目标的原因。它实际上与写作相同3 = 4;——你会尝试将一个值分配给另一个值。

于 2013-08-29T19:31:24.397 回答
2

Carl Norum 给出了语言律师对这个问题的回答(并得到了我的支持),下面是实现细节的答案:

对于计算机而言,内存中的任何对象都只是一个字节范围,并且就内存处理而言,由第一个字节的地址和以字节为单位的大小唯一标识。即使你有一个int内存,它的地址也只不过是它的第一个字节的地址。大小几乎总是隐含的:如果您将指针传递给 an int,编译器就会知道它的大小,因为它知道该地址处的字节将被解释为int. 结构也是如此:它们的地址是它们的第一个字节的地址,它们的大小是隐含的。

现在,语言设计者可以用数组实现与结构类似的语义,但他们没有一个很好的理由:与只传递指针相比,复制比现在效率更低,结构已经通过使用大多数时候是指针,而数组通常意味着很大。过大到无法通过语言对它们施加值语义。

因此,通过指定数组的名称实际上等同于指针,数组在任何时候都被迫成为内存对象。为了不破坏数组与其他内存对象的相似性,大小再次被认为是隐式的(对于语言实现,而不是程序员!):编译器在传递数组时可能会忘记数组的大小在其他地方并依靠程序员知道数组中有多少对象。

这样做的好处是数组访问非常简单。它们衰减到指针算术问题,将索引乘以数组中对象的大小并将该偏移量添加到指针。这就是为什么a[5]与 完全相同的原因5[a],它是 的简写*(a + 5)

另一个与性能相关的方面是从数组中创建子数组非常简单:只需要​​计算起始地址。没有什么会迫使我们将数据复制到新数组中,我们只需要记住使用正确的大小......

所以,是的,就实现简单性和性能而言,数组名称以它们的方式衰减为指针是有深刻原因的,我们应该为此感到高兴。

于 2013-08-29T19:04:24.340 回答