不可能返回数组在C中实际上意味着什么?


72

我不是要重复关于C无法返回数组但要更深入地研究的常见问题。

我们不能这样做:

但是我们可以做:

因此,我正在浏览由gcc -S生成的ASM代码,并且似乎正在使用该堆栈,-x(%rbp)并与其他任何C函数返回一样进行寻址。

直接返回数组是什么?我的意思是,不是根据优化或计算复杂度,而是就没有结构层的实际能力而言。

额外数据:我在x64 Intel上使用Linux和gcc。


18
很久以前,在K&R第一版(1978)发行之时,您无法从函数返回结构或将其传递给函数。您必须使用指针。尽管您可以分配包含数组的结构,但仍然不能分配普通数组。这主要是C的设计方式,没有人为使必要的更改标准化而奋斗。
乔纳森·勒夫勒

17
确实,K&R 1st Edn的6.2节说:C结构有许多限制。基本规则是,您可以在结构上执行的唯一操作是获取其地址&并访问其成员之一。这意味着不能将结构分配给一个单元或将其复制,也不能将它们传递给函数或从函数返回。(这些限制将在以后的版本中删除。) …而且,仁慈地,删除了这些限制。但是,对数组分配的限制尚未更改。
乔纳森·勒夫勒

25
请注意,这char f[8](void)是一个函数数组。返回数组的函数看起来像char f(void)[8]
melpomene

4
如果将结构的大小增加到16个字节以上,则会注意到编译器实际上传递了一个额外的隐藏参数,该参数是要返回的结构的地址。
caf

Answers:


101

首先,是的,您可以将数组封装在结构中,然后对该结构执行任何您想要的操作(分配它,从函数返回它,等等)。

第二,您已经发现,编译器发出代码以返回(或分配)结构几乎没有困难。因此,这也不是您不能返回数组的原因。

不能这样做的根本原因是,直言不讳地说,数组是C语言中的第二类数据结构。所有其他数据结构都是一流的。从这个意义上说,“第一等”和“第二等”的定义是什么?只是不能分配第二类类型。

(您的下一个问题很可能是“除了数组,还有其他第二类数据类型吗?”,我认为答案是“不是,除非您对函数进行计数”。)

与您无法返回(或分配)数组这一事实密切相关的是,也没有数组类型的值。存在数组类型的对象(变量),但是每当您尝试获取值1时,都将获得指向数组第一个元素的指针。[注:更正式地讲,没有数组类型的值,尽管可以将数组类型的对象视为左值,尽管,尽管这是不可分配的。]

因此,除了不能分配数组的事实外,还不能生成要分配给数组的值。如果你说

好像你写过

因此,我们在右侧有一个指针,在左侧有一个数组,即使数组以某种方式是可分配的,我们也会遇到巨大的类型不匹配。同样(根据您的示例),如果我们尝试编写

和定义函数内的某处f(),我们有

好像最后一行说

同样,我们没有要返回并分配给的数组值,而a只是一个指针。

(在函数调用示例中,我们还遇到了一个非常重要的问题,ret即本地数组,试图在C中返回是危险的。稍后对此进行更多介绍。)

现在,你的问题的一部分可能是“为什么会这样?”,也“如果你不能分配数组,为什么可以包含数组分配结构?”

接下来是我的解释和看法,但这与Dennis Ritchie在《 C语言的发展》一文中描述的一致。

数组的不可分配性来自三个事实:

  1. C旨在在语法和语义上接近机器硬件。C语言中的基本操作应编译为一个或几个机器指令,占用一个或几个处理器周期。

  2. 数组一直很特别,特别是在它们与指针相关的方式上。这种特殊的关系是从C的前身语言B中对数组的处理演变而来,并受到其严重影响。

  3. 结构最初不是用C语言编写的。

由于第2点,不可能分配数组,而由于第1点,则无论如何都不可能,因为单个赋值运算符=不应扩展为可能花费N千个周期来复制N千个元素数组的代码。

然后我们到达第3点,这实际上最终形成了一个矛盾。

当C获得结构时,它们最初也不是完全一流的,因为您无法分配或返回它们。但是,您之所以不能做到这一点,仅仅是因为第一个编译器起初不足以生成代码。没有数组的语法或语义障碍。

一直以来,我们的目标都是使结构达到一流,这是相对较早实现的,大约是在第一版K&R即将出版时。

但是,仍然存在一个大问题,如果基本操作应该编译为少量的指令和周期,那么为什么该参数不禁止结构分配呢?答案是,是的,这是一个矛盾。

我相信(尽管这更多是我的猜测)这种想法是这样的:“第一类类型是好的,第二类类型是不幸的。我们在数组中处于第二类状态,但是我们可以在结构上做得更好。无昂贵代码规则并不是真正的规则,它更像是一个准则。数组通常会很大,但是结构通常会很小,几十个或几百个字节,因此分配它们不会通常是过于昂贵。”

因此,一致地应用免费代码规则就被抛在了一边。无论如何,C从来都不是完全规则或一致的。(就此而言,绝大多数成功的语言,无论是人为的还是人为的)。

有了这一切说,它可能是值得询问“如果C支撑分配和返回数组?如何可能的工作?” 答案必须包括某种方式来关闭表达式中数组的默认行为,即它们倾向于变成指向其第一个元素的指针。

在20世纪90年代的某个时候,IIRC提出了一个经过深思熟虑的提议来做到这一点。我认为这涉及封闭的阵列式[ ][[ ]]什么的。今天,我似乎找不到任何关于该提议的提法(尽管我很感谢有人可以提供参考)。无论如何,我相信我们可以通过执行以下三个步骤来扩展C以允许进行数组分配:

  1. 取消禁止在赋值运算符的左侧使用数组。

  2. 取消禁止声明数组值函数。回到最初的问题,使之char f(void)[8] { ... }合法。

  3. (这是最大的问题。)有一种方法可以在表达式中提及一个数组,并以一个真正的,可分配的数组类型值(rvalue)结束。为了论证,我将放置一个名为的新运算符或伪函数arrayval( ... )

[旁注:今天,我们有了数组/指针对应关系的“键定义”,即:

出现在表达式中的对数组类型对象的引用会衰减(除三个例外外)指向其第一个元素的指针。

这三个例外是当数组是一个sizeof运算符或一个&运算符的操作数,或者是字符数组的字符串文字初始值设定项时。在我在这里讨论的假设修改下,将有四个例外,其中将运算arrayval符的操作数添加到列表中。

无论如何,有了这些修改,我们可以编写如下内容

(显然,我们还必须决定做什么,如果ab是不一样的大小。)

给定功能原型

我们也可以做

让我们看一下f的假设定义。我们可能有类似的东西

请注意(假设的新arrayval()运算符除外),这与Dario Rodriguez最初发布的内容相同。还要注意-在假设的世界中,数组分配是合法的,并且arrayval()存在类似的东西-这实际上是可行的!特别是,它不会遇到将即将失效的指针返回到本地数组的问题ret。它会返回该数组的副本,因此完全没有问题-几乎完全类似于合法的法律


最后,回到旁边的问题“还有其他第二类类型吗?”,我认为函数(例如数组)在不被使用时自动获取其地址(这是巧合,这绝非偶然)。作为函数或数组),并且类似地没有函数类型的右值。但这主要是一种无聊的沉思,因为我认为我从来没有听说过C语言中称为“第二类”类型的函数(也许它们已经被我忘记了)。


附注:由于编译器愿意分配结构,通常知道如何发出有效的代码这样做的,它曾经是一个有点招人喜爱增选编译器的结构,复制,以复制从点任意字节机械指向b。特别是,您可以编写这个看起来有些奇怪的宏:

的行为或多或少地与的优化内联版本完全相同memcpy()。(实际上,这个技巧仍然可以在当今的现代编译器中编译和运行。)


4
@JohnBollinger有数组类型的左值,但没有右值。
n。代词

2
@JohnBollinger C标准使用术语“值”来表示可能称为“ rvalue”的内容
MM

2
@nm:即使这不是真的-在问题的第二个例子中,f().arr是一个右值数组。
caf

4
+1可以有效地解释为什么在不与现有约束冲突的情况下无法使用这种功能来扩展语言。
R .. GitHub停止帮助ICE

2
@JohnBollinger确实有点简化,但我觉得有用。正如nm和MM(你们两个有关系吗?(当然,要说arrys是否可以成为左值,这是一个相当著名的难题。)
Steve Summit

21

直接返回数组是什么?我的意思是,不是根据优化或计算复杂度,而是就没有结构层的实际能力而言。

本身与功能无关。其他语言确实提供了返回数组的功能,并且您已经知道在C语言中您可以返回带有数组成员的结构。另一方面,其他语言具有与C相同的限制,甚至更多。例如,Java不能从方法返回数组,也不能返回任何类型的对象。它只能返回原语和对对象的引用

不,这只是语言设计的问题。与其他大多数与数组有关的事情一样,这里的设计要点围绕C的规定,即C规定数组类型的表达式在几乎所有上下文中都自动转换为指针。return语句中提供的值也不例外,因此C甚至无法表达数组本身的返回值。可以做出其他选择,但事实并非如此。


3
与Java进行了比较之后,我记录了一下,即Java术语“对象”的含义不同于C术语的含义。
John Bollinger

2
“几乎在所有上下文中,数组类型的表达式都会自动转换为指针。” 真正的答案,恕我直言。从编译器POV来看,很难确定是将数组作为整体还是作为第一个元素的指针:应该有类似的东西return (array) breturn b要明确区分。显然,structs不能有这种歧义。
edmz

“本来可以做出不同的选择,但事实并非如此。” -对我来说就是这样。
Dario Rodriguez

3

对于要成为一流对象的数组,您至少希望能够对其进行分配。但这需要了解大小,而C类型系统的功能不足以将大小附加到任何类型。C ++可以做到这一点,但并非出于遗留问题-它引用了特定大小的数组(typedef char (&some_chars)[32]),但普通数组仍像C中一样隐式转换为指针。C++则使用std :: array,基本上前面提到的结构内数组加上一些语法糖。


3
如果C类型系统的功能不足以将大小附加到类型上,那么sizeof操作员将如何工作?但是它确实可以使用,包括使用数组类型。
John Bollinger

@JohnBollinger sizeof是在编译时计算的,而数组的大小是编译器已知的。
西蒙B

1
因为C是一种编译语言,所以编译时间是所有类型中唯一的真正问题。将没有运行时类型系统。然后,C类型系统确实将大小附加到类型上。
Dario Rodriguez

1
sizeof工作正常,@ RomanOdaisky。该函数的参数列表的含义显然使您感到惊讶。根据标准,尽管x声明了参数char *,但仍将其声明为,而不是数组,并带有括号的大小。这与以下事实完全一致:首先没有办法将数组作为函数参数传递(因为在函数调用的参数列表中,就像在大多数其他地方一样,将具有数组类型的表达式转换为指针) 。
John Bollinger

1
是的,@ Gaius,您是对的。您已经对Roman的断言提出了另一个反例,该断言是“ C类型系统的功能不足以将大小附加到任何类型。” 实际上,当您有指向数组的指针时,您的反例甚至覆盖了数组类型的大小(示例:)int (*p)[3]; printf("%uz\n", sizeof(*p));
John Bollinger

-1

在我看来,恐怕不是关于一流或第二类对象的辩论,而是关于良好实践和适用于深度嵌入式应用程序的实践的宗教性讨论。

返回结构要么意味着根结构被调用序列深度的隐身性所改变,要么意味着数据的重复以及大量重复数据的传递。C的主要应用程序仍然主要集中在深度嵌入式应用程序周围。在这些域中,您有小型处理器,不需要传递大量数据。您还具有工程实践,这需要能够在没有动态RAM分配的情况下进行操作,并且堆栈最少且通常没有堆。可以说结构的返回与通过指针的修改是相同的,但是在语法上是抽象的……恐怕我认为这不是C语言中“所见即所得”的哲学。指向类型的指针的方式相同。

就个人而言,我认为您发现了一个漏洞,无论标准与否。C以明确分配的方式设计。按照惯例,您将地址总线大小的对象传递给您,通常是在一个有抱负的周期中进行,引用的是已在开发人员ken中的受控时间显式分配的内存。就代码效率,循环效率而言,这是有意义的,并提供了最大程度的控制和目的明确性。恐怕在代码检查中,我会抛出一个返回结构的函数,这是一种不好的做法。C并没有执行很多规则,它是专业工程师使用的一种语言,因为它依赖于用户执行自己的学科。仅仅因为您可以,并不意味着您应该...


2
我认为这离主题还有很长的路要走。这似乎是一种意见而不是答案。这也扭曲了主要话题,因为即使大多数C应用程序都用于“深度嵌入式应用程序”,也不能将讨论变成“良好实践和适用于深度嵌入式应用程序的实践”之一。讨论确实以完全不同的方式进行。
Dario Rodriguez

我将看到“ C仍然主要集中在深层嵌入式应用程序上……没有动态RAM分配,并且具有最小的堆栈,通常没有堆”,并引起您“ C是用于操作的通用计算机编程语言。系统,图书馆,游戏和其他高性能作品”-引用自SO自己的标签信息
史蒂夫·萨米特

您绝对正确的是,通过代码审查或样式指南来严格约束嵌入的工作,结构返回函数可能会被强制拒绝(理应如此)。(由于各种原因,样式指南总是不允许使用。)
Steve Summit

正如我试图在答案中探索的那样,结构分配和数组分配最终落在合法性的对立面上,这是由于不一致目标的应用(即不使用“昂贵”的代码,而使用干净,一致,可表达的语言) 。
史蒂夫·萨米特
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.