我并没有试图复制关于C无法返回数组的常见问题,而是更深入地研究它。
我们不能这样做:
char f(void)[8] {
char ret;
// ...fill...
return ret;
}
int main(int argc, char ** argv) {
char obj_a[10];
obj_a = f();
}
但我们可以这样做:
struct s { char arr[10]; };
struct s f(void) {
struct s ret;
// ...fill...
return ret;
}
int main(int argc, char ** argv) {
struct s obj_a;
obj_a = f();
}
所以,我正在浏览由gcc -S生成的ASM代码,并且似乎正在使用堆栈,与任何其他C函数返回一样寻址-x(%rbp)
。
直接返回数组有什么用?我的意思是,不是在优化或计算复杂性方面,而是在没有结构层的情况下这样做的实际能力。
额外数据:我在x64 Intel上使用Linux和gcc。
答案 0 :(得分:98)
首先,是的,您可以将数组封装在结构中,然后使用该结构执行任何操作(分配,从函数返回等)。
其次,正如您所发现的那样,编译器很难发出代码来返回(或分配)结构。因此,这也不是您无法返回数组的原因。
你不能这样做的根本原因是,直言不讳地说,数组是C 中的二等数据结构。所有其他数据结构都是一流的。 "一流"的定义是什么?和#34;二等"在这个意义上?只是无法分配第二类类型。
(你的下一个问题可能是,"除了数组之外,还有其他任何二类数据类型吗?",我认为答案是"不是真的,除非你计数功能"。)
与您无法返回(或分配)数组的事实密切相关的是,也没有数组类型的值。有数组类型的对象(变量),但每当你尝试取值1时,你会得到一个指向数组第一个元素的指针。 [脚注:更正式地说,没有数组类型的 rvalues ,尽管数组类型的对象可以被认为是左值,尽管是不可赋值的。
因此,除了您无法将分配给数组之外,您还无法生成要分配给数组的值。如果你说
char a[10], b[10];
a = b;
好像你写过
a = &b[0];
所以我们在右边有一个指针,在左边有一个数组,即使数组以某种方式可分配,我们也会出现大规模的类型不匹配。同样(从你的例子)我们试着写
a = f();
在函数f()
定义内的某处
char ret[10];
/* ... fill ... */
return ret;
就好像最后一行说的那样
return &ret[0];
再次,我们没有数组值可以返回并分配给a
,只是一个指针。
(在函数调用示例中,我们还遇到了一个非常重要的问题,即ret
是一个本地数组,试图在C中返回是危险的。稍后将详细讨论。)
现在,您的部分问题可能是"为什么会这样?",还有"如果您无法分配数组,为什么可以您分配包含数组的结构?"
以下是我的解释和我的观点,但它与Dennis Ritchie在论文 The Development of the C Language 中描述的内容一致。
数组的不可分配性来自三个事实:
C旨在在语法和语义上接近机器硬件。 C中的基本操作应该编译成一个或几个机器指令,占用一个或几个处理器周期。
数组一直很特别,特别是它们与指针的关系;这种特殊的关系是从C语言的前身语言B中对数组的处理发展而来的,并且受到很大的影响。
结构最初并未在C。
由于第2点,它无法分配数组,并且由于第1点,它无论如何都不可能,因为单个赋值运算符=
不应该扩展代码可能需要一千个周期来复制一个N元素数组。
然后我们进入第3点,这最终形成了一个矛盾。
当C得到结构时,他们最初也不是完全一流的,因为你无法分配或归还它们。但你原因并不是因为第一个编译器起初并不够智能,而是生成代码。没有句法或语义障碍,就像阵列一样。
目标始终是结构一流,而且这是在相对较早的时候实现的,很快就会出现第一版K& R打印出来的时间。
但是最大的问题仍然是,如果一个基本操作应该编译成少量的指令和周期,为什么这个参数不允许结构分配呢?答案是肯定的,这是一个矛盾。
我相信(虽然这是我的更多猜测),思维是这样的:"一流的类型是好的,二等类型是不幸的。我们坚持使用数组的第二类状态,但我们可以使用结构更好。不昂贵的代码规则并非真正的规则,它更像是一个指导原则。数组通常很大,但结构通常很小,几十或几百个字节,所以分配它们通常不会太昂贵。"
因此,不昂贵的代码规则的一致应用就被淘汰了。无论如何,C从未完全正常或一致。 (就此而言,绝大多数成功的语言都是人类和人为的。)
如上所述,可能值得一提,"如果C 支持分配和返回数组怎么办?这怎么可能有用?"答案将涉及某种方式来关闭表达式中数组的默认行为,即它们倾向于变成指向其第一个元素的指针。
有时候,在IIRC的90年代,有一个相当经过深思熟虑的提议,要做到这一点。我认为它涉及在[ ]
或[[ ]]
或其他内容中包含数组表达式。今天我似乎无法提及任何提议(尽管如果有人可以提供参考,我会感激不尽)。现在,我将假设一个名为arrayval()
的新运算符或伪函数。
我们可以通过执行以下操作来扩展C以允许数组赋值:
取消禁止在赋值运算符的左侧使用数组。
删除声明数组值函数的禁令。回到原始问题,请char f(void)[8] { ... }
合法。
(这是最重要的。)有一种方法可以在表达式中提及数组,并以数组类型的真值,可赋值( rvalue )结束。如上所述,我现在要提出语法arrayval( ... )
。
[旁注:今天我们有一个" key definition"该
对表达式中出现的数组类型对象的引用将(有三个例外)衰减为指向其第一个元素的指针。
三个例外是当数组是sizeof
或&
运算符的操作数时,或者是字符数组的字符串文字初始值设定项。根据我在这里讨论的假设修改,会有四个例外,arrayval
运算符的操作数被添加到列表中。]
无论如何,通过这些修改,我们可以编写像
这样的内容char a[8], b[8] = "Hello";
a = arrayval(b);
(显然,如果a
和b
的尺寸不同,我们还必须决定该怎么做。)
给出函数原型
char f(void)[8];
我们也可以
a = f();
让我们看一下f
的假设定义。我们可能会有像
char f(void)[8] {
char ret[8];
/* ... fill ... */
return arrayval(ret);
}
请注意(除了假设的新arrayval()
运算符),这就是Dario Rodriguez最初发布的内容。还要注意 - 在假设的世界中,阵列分配是合法的,并且存在类似arrayval()
的东西 - 这实际上是有效的!特别是,不会遇到返回一个很快无效的指向本地数组ret
的指针的问题。它将返回数组的副本,因此根本没有问题 - 它与显然合法的
int g(void) {
int ret;
/* ... compute ... */
return ret;
}
最后,回到"是否有其他二类类型的问题?",我认为函数(如数组)自动拥有其地址不仅仅是巧合当它们没有被用作它们自己时(即,作为函数或数组),并且类似地没有函数类型的右值。但这主要是一种空洞的沉思,因为我不认为我曾经听过被称为"二等" C中的类型(也许他们有,而且我已经忘记了。)
答案 1 :(得分:20)
直接返回数组有什么用?我的意思是,不是在优化或计算复杂性方面,而是在没有结构层的情况下这样做的实际能力。
它与功能本身无关。其他语言确实提供了返回数组的能力,并且您已经知道在C中可以返回带有数组成员的结构。另一方面,其他语言具有与C相同的限制,甚至更多。例如,Java不能从方法返回数组,也不能返回任何类型的对象。它只能返回基元和引用到对象。
不,这只是语言设计的问题。与大多数其他与数组有关的事情一样,这里的设计要点围绕C语言规定,数组类型的表达式几乎在所有上下文中自动转换为指针。 return
语句中提供的值也不例外,因此C甚至无法表达数组本身的返回。可能已经做出了不同的选择,但它根本就没有。
答案 2 :(得分:3)
要使数组成为第一类对象,您至少可以指定它们。但这需要了解大小,而C类型系统的功能不足以将大小附加到任何类型。 C ++可以做到这一点,但由于遗留问题而没有 - 它有引用到特定大小的数组(typedef char (&some_chars)[32]
),但普通数组仍然隐式转换为指针,如C. C ++有std :: array,它基本上就是前面提到的array-within-struct加上一些语法糖。
答案 3 :(得分:-1)
在我看来,我担心的不是对第一类或第二类对象的辩论,而是对深层嵌入式应用程序的良好实践和适用实践的宗教讨论。
返回结构要么意味着根结构在调用序列的深度被隐秘地改变,要么重复数据并且传递大块重复数据。 C的主要应用仍主要集中在深度嵌入式应用程序上。在这些域中,您拥有不需要传递大块数据的小型处理器。您还需要工程实践,这需要能够在没有动态RAM分配的情况下运行,并且堆栈最少且通常没有堆。可以说,结构的返回与通过指针的修改是一样的,但是在语法中被抽象化......我担心我会认为这不是在#34的C哲学中;你所看到的就是你得到的东西"以同样的方式指向一个类型的指针。
就我个人而言,我认为你已经找到了一个循环孔,无论标准是否批准。 C的设计方式使得分配是明确的。您通过良好实践来传递总线大小的对象,通常是在一个有抱负的循环中,指的是在开发人员的控制时间内明确分配的内存。这在代码效率,循环效率方面是有意义的,并提供最大的控制和目的明确。我担心,在代码检查中,我会抛出一个函数,将结构作为不良实践返回。 C不会强制执行许多规则,它在很多方面都是专业工程师的语言,因为它依赖于用户执行自己的规则。仅仅因为你可以,并不意味着你应该...它确实提供了一些非常防弹的方法来处理非常复杂的大小和类型的数据利用编译时的严谨性并最小化足迹和运行时的动态变化。