为什么C中的数组会衰减到指针?

[这是一个受到其他地方最近讨论的启发的问题,我会用它来提供答案。]

我想知道数组“衰减”指针的奇怪C现象,例如当用作函数参数时。 这似乎是不安全的。 用它明确地传递长度也是不方便的。 我可以通过其他类型的聚合 – 结构 – 完全符合价值; 结构不会腐烂。

这个设计决定背后的理由是什么? 它如何与语言集成? 为什么结构有区别?

合理

让我们检查一下函数调用,因为那里的问题很明显: 为什么数组不是简单地作为数组传递给函数作为副本?

首先有一个纯粹实用的原因:arrays可能很大; 可能不建议按值传递它们,因为它们可能超过堆栈大小,尤其是在20世纪70年代。 第一批编译器写在PDP-7上,内存大约为9 kB。

根植于该语言还有一个更为技术性的原因。 使用大小在编译时未知的参数为函数调用生成代码是很困难的。 对于所有数组,包括现代C中的可变长度数组,只需将地址放在调用堆栈上。 地址的大小当然是众所周知的。 即使具有携带运行时大小信息的精巧数组类型的语言也不会传递堆栈上正确的对象。 这些语言通常会传递“句柄”,这也是C已经有效地完成了40年。 请参阅Jon Skeet ,并在此处参考(原文如此)。

现在,一种语言可以要求数组总是具有完整的类型; 即无论何时使用,其包括尺寸的完整声明必须是可见的。 毕竟,这就是C对结构的要求(当它们被访问时)。 因此,结构可以按值传递给函数。 要求数组的完整类型也可以使函数调用易于编译,并且无需传递额外的长度参数: sizeof()仍然可以在被调用者中按预期工作。 但想象一下这意味着什么。 如果大小实际上是数组参数类型的一部分,那么我们需要为每个数组大小设置一个不同的函数:

 // for user input. int average_Ten(int arr[10]); // for my new Hasselblad. int average_ThreeTrillionThreehundredninetythreeBillionNinehundredtwentyeightMillionEighthundredsixthousandfourhundred(int arr[16544*12400]); // ... 

事实上,它与传递结构完全相当,如果它们的元素不同(例如,一个结构有10个int元素,一个结构有16544 * 12400),它们的类型不同。 很明显,arrays需要更大的灵活性。 例如,如所示,人们无法合理地提供通常可用的带有数组参数的库函数。

事实上,当一个函数引用一个数组时,这个“强类型难题”就是C ++中发生的事情。 这也是没有人这样做的原因,至少没有明确说明。 除了针对特定用途的情况外,在通用代码中使用它是完全不方便的:C ++模板提供了C中没有的编译时灵活性。

如果在现有的C中,确实应该通过值传递已知大小的数组,则始终可以将它们包装在结构中。 我记得Solaris上的一些与IP相关的头文件定义了包含数组的系列结构,允许复制它们。 因为结构的字节布局是固定的并且是已知的,所以这是有意义的。

在某些背景下,阅读丹尼斯·里奇的C语言发展也很有意思C.C的前身BCPL的起源没有任何数组; 内存只是同构的线性内存,带有指针。

这个问题的答案可以在Dennis Ritchie的“C语言的发展”一文中找到(参见“胚胎C”部分)

根据Dennis Ritchie的说法,C的新生版本直接inheritance/采用了B和BCPL语言的数组语义–C的前身。在这些语言中,数组实际上是作为物理指针实现的。 这些指针指向独立分配的包含实际数组元素的内存块。 这些指针在运行时初始化。 即回到B和BCPL天数arrays被实现为“二进制”(二分)对象:指向独立数据块的独立指针。 除了自动初始化数组指针这一事实外,这些语言中的指针和数组语义没有区别。 在任何时候都可以在B和BCPL中重新分配一个数组指针,使其指向其他地方。

最初,这种对数组语义的方法得到了C的inheritance。然而,当struct类型被引入语言时(B和BCPL都没有),它的缺点立即变得明显。 而这个想法是结构应该自然能够包含数组。 然而,继续坚持B / BCPLarrays的上述“二分”性质将立即导致结构的许多明显的并发症。 例如,内部有数组的struct对象在定义时需要非平凡的“构造”。 复制这样的结构对象是不可能的 – 原始的memcpy调用将复制数组指针而不复制实际数据。 一个人无法使用malloc结构对象,因为malloc只能分配原始内存并且不会触发任何非平凡的初始化。 等等等等。

这被认为是不可接受的,这导致了Carrays的重新设计。 Ritchie没有通过物理指针实现数组,而是决定完全摆脱指针。 新arrays实现为单个立即内存块,这正是我们今天在C中所拥有的。 但是,出于向后兼容性原因,B / BCPL数​​组的行为尽可能在表面上保留(模拟):新的C数组很容易衰减为临时指针值,指向数组的开头。 其余的arraysfunction保持不变,依赖于现有的衰减结果。

引用前面提到的论文

该解决方案构成了无类型BCPL和类型C之间的演化链中的关键跳跃。它消除了存储中指针的实现,而是在表达式中提到数组名称时导致指针的创建。 在今天的C中幸存的规则是,数组类型的值在表达式中出现时转换为指向构成数组的第一个对象的指针。

尽管语言的语义发生了潜在的变化,但本发明使大多数现有的B代码能够继续工作。 为数组名称分配新值以调整其原点的少数程序 – 可能在B和BCPL中,在C中无意义 – 很容易修复。 更重要的是,新语言保留了对数组语义的连贯且可行(如果不寻常)的解释,同时为更全面的类型结构开辟了道路。

因此,对“为什么”问题的直接回答如下:C中的数组被设计为衰减指针,以便模拟 (尽可能接近)B和BCPL语言中数组的历史行为。

花时间机器回到1970年。开始设计编程语言。 您希望以下代码编译并执行预期的操作:

 size_t i; int* p = (int *) malloc (10 * sizeof (int)); for (i = 0; i < 10; ++i) p [i] = i; int a [10]; for (i = 0; i < 10; ++i) a [i] = i; 

与此同时,您需要一种简单的语言。 很简单,你可以在1970年代的计算机上编译它。 “a”衰变为“指向a的第一个元素”的规则可以很好地实现。