[这是一个受到最近在其他地方的讨论启发的问题,我会马上提供答案。]
我想知道有关C语言中数组“衰变”的奇怪现象,例如当作为函数参数使用时。 这似乎非常不安全。同时,需要显式传递长度也很麻烦。而且我可以通过值完美地传递另一种类型的聚合体——结构体;结构体不会衰变。
这个设计决策背后的理念是什么?它如何融入语言中?为什么与结构体有所区别?
[这是一个受到最近在其他地方的讨论启发的问题,我会马上提供答案。]
我想知道有关C语言中数组“衰变”的奇怪现象,例如当作为函数参数使用时。 这似乎非常不安全。同时,需要显式传递长度也很麻烦。而且我可以通过值完美地传递另一种类型的聚合体——结构体;结构体不会衰变。
这个设计决策背后的理念是什么?它如何融入语言中?为什么与结构体有所区别?
最初,这种数组语义的方法被C所继承。然而,当结构体类型引入该语言时(这是B和BCPL都没有的),它的缺点立即变得明显起来。这个想法是结构体应该自然地能够包含数组。然而,继续坚持B/BCPL数组的上述“二分”特性将立即导致与结构体相关的一些明显的复杂性问题。例如,带有数组的结构对象需要在定义点进行非平凡的“构造”。将无法复制这样的结构对象——一个原始的memcpy
调用将复制数组指针而不是实际的数据。无法使用malloc
为结构体对象分配内存,因为malloc
只能分配原始内存且不触发任何非平凡初始化等等。
原理
让我们来看看函数调用,因为问题在那里很容易看到:为什么数组不直接作为数组传递给函数,按值传递,作为副本呢?
首先有一个纯粹实用的理由:数组可能很大;如果通过值传递它们,可能不明智,因为它们可能超过堆栈大小,特别是在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拿起你的时光机,回到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的第一个元素的指针”的规则非常好地实现了这一点。