【问题标题】:Why do arrays in C decay to pointers?为什么 C 中的数组会衰减为指针?
【发布时间】:2016-01-22 08:34:50
【问题描述】:

[这个问题的灵感来自最近在其他地方的讨论,我将立即提供答案。]

我想知道数组“衰减”到指针的奇怪 C 现象,例如当用作函数参数时。这似乎太不安全了。明确地传递长度也是不方便的。而且我可以很好地传递另一种类型的聚合——结构——值;结构不会衰减。

这个设计决策背后的基本原理是什么?它是如何与语言相结合的?为什么结构有区别?

【问题讨论】:

  • 它节省了大量(不必要的)复制。 C 是为速度而不是安全而设计的。
  • 因为在大多数情况下复制作为参数传递的数组会很昂贵且不必要。此外,C 最初不支持将结构体作为函数参数传递,因此没有明确的设计选择来使数组与结构体不同。
  • @Kninnug 当然,如果需要,仍然可以传递地址,就像其他任何事情一样。
  • 这种设计选择有特定的原因,所以我不会说它会因为“主要基于意见”而关闭。
  • 如果它采用另一种方式,一些开发人员现在会发布“我将我的 50MB 视频缓冲区传递给 someFunc() 进行处理,它被复制进来,然后我处理它,因为调用者需要结果而不是原始,它必须在返回中复制回来。我 90% 的 CPU 周期都浪费在无意义地复制大型视频缓冲区上。多么愚蠢的设计决定!'

标签: c arrays pointers


【解决方案1】:

基本原理

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

首先有一个纯粹实用的原因:数组可以很大;可能不建议按值传递它们,因为它们 可能会超过堆栈大小,尤其是在 1970 年代。第一个编译器是在具有大约 9 kB RAM 的 PDP-7 上编写的。

还有一个植根于语言的技术原因。很难为带有在编译时大小未知的参数的函数调用生成代码。对于所有数组,包括现代 C 中的可变长度数组,只需将地址放在调用堆栈上。 地址的大小当然是众所周知的。即使具有带有运行时大小信息的复杂数组类型的语言也不会在堆栈上正确传递对象。这些语言通常会传递“句柄”,这也是 C 40 年来有效完成的工作。请参阅 Jon Skeet here 和他引用的插图说明(原文如此)here

现在,一种语言可以要求数组始终具有完整的类型;即,无论何时使用它,它的完整声明(包括大小)都必须可见。毕竟,这是 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 的 The Development of the C Language 了解 C 的起源也很有趣。C 的前身 BCPL 没有任何数组。内存只是带有指针的同构线性内存。

【讨论】:

  • 优秀的答案。我要补充的是,将数组“分解”为指针是有意义的,因为它们本质上是相同的......即只是一种从给定地址开始迭代内存中sizeof(int)字节的方法。
  • Cf. 信号的有趣用法,可能是 AccordSeeSee also i> 可能更合适,这取决于 Skeet 的链接提供的直接支持。*Cf* 表示与主要点的差异偏离,但有足够的类似支持来保证比较。
  • @DavidC.Rankin 我不知道这一点。的确,我现在认为“看”是我想说的。
  • 标记“(原文如此)”表示您输入的单词是直接引用他人的,并且原文中存在奇怪的措辞、拼写错误或不正确的语法,这不是您的错,您只是按照最初编写的方式引用它。您输入的文字是对 Jon Skeet 的直接引用吗?我猜不是。
  • 尽管“C++ 模板提供了 C 中不具备的编译时灵活性”,但使用模板指定特定大小的数组引用参数不仅会导致代码膨胀,还会导致代码爆炸。对于传递给该函数的每个不同大小的数组,编译器会很高兴地为该数组大小插入一个新的函数副本。假设您的函数在编译后只有 490 字节长。但它用于大型程序并从 200 个位置调用,每个位置都有唯一的数组大小。因此,您有 200 个副本,总共浪费了 96 KB 的代码空间在该函数上。
【解决方案2】:

这个问题的答案可以在 Dennis Ritchie 的 "The Development of the C Language" 论文中找到(参见“胚胎 C”部分)

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

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

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

引用上述论文

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

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

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

【讨论】:

  • 非常好的信息。我想直到现在我才完全理解你引用的这段话。
【解决方案3】:

乘坐时光机回到 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的第一个元素”的规则很好地实现了这一点。

【讨论】:

  • 当时其他语言可以做到(Algol68)。如果您阅读 Ritchie 的论文,“设计”的整个想法似乎有点偏离——它更像是进化;-)
猜你喜欢
  • 2011-11-14
  • 1970-01-01
  • 2016-02-17
  • 2020-10-07
  • 2021-12-01
相关资源
最近更新 更多