为什么 C 语言中的数组会衰变成指针?

26

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

我想知道有关C语言中数组“衰变”的奇怪现象,例如当作为函数参数使用时。 这似乎非常不安全。同时,需要显式传递长度也很麻烦。而且我可以通过值完美地传递另一种类型的聚合体——结构体;结构体不会衰变。

这个设计决策背后的理念是什么?它如何融入语言中?为什么与结构体有所区别?


6
它节省了大量(不必要的)复制。C语言旨在追求速度,而非安全。 - Kninnug
1
因为大多数情况下复制传递的数组是昂贵且不必要的。此外,C最初不支持将结构体作为函数参数传递,因此没有明确的设计选择使数组与结构体不同。 - Filipe Gonçalves
4
当然,如果有需要的话,仍然可以传递地址,就像其他任何事情一样。 - Peter - Reinstate Monica
1
有具体的设计原因,所以我不会说它是基于“主观看法”的关闭。 - edmz
2
如果情况反过来,某些开发人员现在可能会发布“我将我的50MB视频缓冲区传递给someFunc()进行处理,它被复制进去,然后我对其进行处理,由于调用者需要的是结果而不是原始数据,因此必须将其复制回返回值。90%的CPU周期都浪费在毫无意义的大型视频缓冲区复制上了。多么愚蠢的设计决策!” - Martin James
显示剩余6条评论
3个回答

25
这个问题的答案可以在Dennis Ritchie的"The Development of the C Language"论文中找到(请参见“Embryonic C”部分)。
据Dennis Ritchie称,C语言的早期版本直接从B和BCPL语言中继承/采用了数组语义 - C语言的前身。在那些语言中,数组被直接实现为物理指针。这些指针指向独立分配的内存块,其中包含实际的数组元素。这些指针在运行时初始化。也就是说,在B和BCPL时代,数组被实现为“二进制”(双分区)对象:一个独立的指针指向一个独立的数据块。除了数组指针自动初始化之外,这些语言中没有指针和数组语义之间的区别。在任何时候,都可以重新分配B和BCPL中的数组指针,使其指向其他位置。

最初,这种数组语义的方法被C所继承。然而,当结构体类型引入该语言时(这是B和BCPL都没有的),它的缺点立即变得明显起来。这个想法是结构体应该自然地能够包含数组。然而,继续坚持B/BCPL数组的上述“二分”特性将立即导致与结构体相关的一些明显的复杂性问题。例如,带有数组的结构对象需要在定义点进行非平凡的“构造”。将无法复制这样的结构对象——一个原始的memcpy调用将复制数组指针而不是实际的数据。无法使用malloc为结构体对象分配内存,因为malloc只能分配原始内存且不触发任何非平凡初始化等等。

这被认为是不可接受的,导致 C 数组的重新设计。Ritchie 决定完全摒弃物理指针来实现数组。新数组被实现为单个即时内存块,这正是我们今天在 C 中所拥有的。然而,出于向后兼容性的考虑,在表面上尽可能地保留了 B/BCPL 数组的行为(模拟)。新的 C 数组会立即“衰变”为一个临时指向数组开头的指针值。其余的数组功能保持不变,依赖于该衰变的可用结果。引用前述论文:
这个解决方案是从无类型 BCPL 到有类型 C 进化链中的关键跃迁。它消除了指针在存储中的实例化,而是在表达式中提到数组名时创建指针。今天的 C 中仍然存在这个规则,即当数组类型的值出现在表达式中时,将其转换为组成数组的对象的第一个元素的指针。这个发明使得大多数现有的 B 代码可以继续工作,尽管语言语义的基础发生了变化。在 B 和 BCPL 中可能调整其起点的数组名称分配新值以进行调整的少数程序很容易修复。更重要的是,新语言保留了一致且可行(虽然不寻常)的数组语义解释,同时打开了更全面的类型结构的途径。
所以,对于你的“为什么”问题的直接答案如下:C 中的数组被设计为衰减为指针,以尽可能地模拟 B 和 BCPL 语言中数组的历史行为。

1
非常好的信息。我认为现在我完全理解了你引用的那段话。 - Peter - Reinstate Monica
@Peter-ReinstateMonica,接受这个答案作为正确的吗? - Lover of Structure

24

原理

让我们来看看函数调用,因为问题在那里很容易看到:为什么数组不直接作为数组传递给函数,按值传递,作为副本呢?

首先有一个纯粹实用的理由:数组可能很大;如果通过值传递它们,可能不明智,因为它们可能超过堆栈大小,特别是在1970年代。最早的编译器是在一台具有约9 kB RAM的PDP-7上编写的。

还有一个更技术性的原因,根源于语言。对于参数大小在编译时未知的函数调用生成代码会很困难。对于所有数组(包括现代C中的可变长度数组),只需将地址放在调用堆栈上。地址的大小当然是众所周知的。即使具有携带运行时大小信息的复杂数组类型的语言也不会在堆栈上传递对象本身。这些语言通常传递“句柄”,这也是C在40年中有效地做到的。请参见Jon Skeet 这里和他引用的插图解释这里

现在,一种语言可以要求数组始终具有完整类型;即每当使用它时,必须可见其完整声明,包括大小。毕竟,这就是C对结构(当它们被访问时)的要求。因此,结构可以通过值传递给函数。同样要求数组的完整类型将使函数调用易于编译,并省去传递额外长度参数的必要性:sizeof()仍然可以在被调用方内部正常工作。但请想象一下这意味着什么。如果大小确实是数组参数类型的一部分,我们将需要为每个数组大小创建一个不同的函数:

// for user input.
int average_ten(int arr[10]);

// for my new Hasselblad.
int average_twohundredfivemilliononehundredfourtyfivethousandsixhundred(int arr[16544*12400]);
// ...

事实上,将不同类型的结构传递给函数进行比较是完全可以比较的,只要它们的元素不同(例如,一个有10个int元素和一个有16544*12400个元素的结构)。很明显,数组需要更多的灵活性。例如,正如所示,不能合理地提供可通用的库函数来接受数组参数。

这种“强类型困境”实际上就是在C++中当函数使用数组引用时发生的情况;这也是为什么没有人这样做的原因,至少不会明确地这样做。这是完全不方便的,甚至可以说是无用的,除非是针对具体用途的情况,在通用代码中:C++模板提供了在C中不可用的编译时灵活性。

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

有关背景,阅读Dennis Ritchie的《C语言的发展》对于C的前身BCPL没有任何数组的情况也很有趣;内存只是具有指向其中的指针的同质线性内存。


非常好的回答。我想补充的是,数组“分解”成指针是有道理的,因为它们本质上是相同的东西...即只是一种在给定地址开始迭代sizeof(int)字节内存的方法。 - Byte Lab
1
有趣的使用Cf.信号,也许根据Skeet的链接提供的直接支持,AccordSeeSee also可能更加合适。Cf表示与主要观点的差异偏离,但具有足够类比的支持以进行比较。 - David C. Rankin
@DavidC.Rankin 我之前并不知道这点。现在我认为 "see" 才是我想要表达的意思。 - Peter - Reinstate Monica
标记“(sic)”表示您输入的单词是直接引用他人的话,奇怪的措辞、拼写错误或不正确的语法存在于原始文本中,而不是您的错,您只是按照原样引用它。您输入的话是Jon Skeet的直接引用吗?我猜不是。 - phonetagger
尽管“C++模板提供了编译时的灵活性,而在C语言中则不可用”,但使用模板来指定特定大小的数组引用参数不仅会导致代码膨胀,而且可能会导致代码爆炸。每当你传递一个不同大小的数组给该函数时,编译器都会乐意为该数组大小插入一个新的函数副本。假设你的函数一旦被编译后只有490字节长。但它在一个大型程序中使用,并从200个地方调用,每个地方都有独特的数组大小。所以你有200个副本,总共浪费了96 Kbytes的代码空间在那个函数上。 - phonetagger
@phonetagger,我认为“[sic]”(拉丁语“thus”)更普遍地表明前面的文本是一个有意选择而不是巧合。正如你所说,它通常用于标记有意保留的引用中的特殊之处,但这并不是唯一的用途。 - Peter - Reinstate Monica

-1

拿起你的时光机,回到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的第一个元素的指针”的规则非常好地实现了这一点。


8
当时(Algol68)其他语言可以做到这一点。如果您阅读里奇的论文,整个“设计”思想似乎有些不对,因为它更多的是演化;-) - Peter - Reinstate Monica

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