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

Dar*_*uez 71 c

我并不想复制关于C不能返回数组的常见问题,而是要深入研究它.

我们不能这样做:

char f(void)[8] {
    char ret;
    // ...fill...
    return ret;
}

int main(int argc, char ** argv) {
    char obj_a[10];
    obj_a = f();
}
Run Code Online (Sandbox Code Playgroud)

但我们可以这样做:

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();
}
Run Code Online (Sandbox Code Playgroud)

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

直接返回数组有什么用?我的意思是,不是在优化或计算复杂性方面,而是在没有结构层的情况下这样做的实际能力方面.

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

Ste*_*mit 97

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

其次,正如您所发现的,编译器很难发出代码来返回(或分配)结构.所以这也不是你不能返回数组的原因.

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

(你的下一个问题可能是,"除了数组,还有其他任何二类数据类型吗?",我认为答案是"不是真的,除非你计算函数".)

与无法返回(或分配)数组这一事实紧密相关的是,也没有数组类型的值.有数组类型的对象(变量),但每当你尝试取值1时,你会得到一个指向数组第一个元素的指针.[脚注:更正式地说,没有数组类型的,尽管数组类型的对象可以被认为是左值,尽管是不可赋值的.)

因此,除了无法分配数组这一事实外,您还无法生成要分配给数组的值.如果你说

char a[10], b[10];
a = b;
Run Code Online (Sandbox Code Playgroud)

就像你写的一样

a = &b[0];
Run Code Online (Sandbox Code Playgroud)

所以我们在右边有一个指针,在左边有一个数组,即使数组以某种方式可分配,我们也会有一个巨大的类型不匹配.同样(从你的例子)我们试着写

a = f();
Run Code Online (Sandbox Code Playgroud)

f()我们所拥有的函数定义中的某个地方

char ret[10];
/* ... fill ... */
return ret;
Run Code Online (Sandbox Code Playgroud)

好像最后一行说的那样

return &ret[0];
Run Code Online (Sandbox Code Playgroud)

而且,我们没有返回和分配的数组值a,只是一个指针.

(在函数调用示例中,我们还遇到了一个非常重要的问题,ret即本地数组,尝试在C中返回是危险的.稍后将详细介绍.)

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

以下是我的解释和我的观点,但这与丹尼斯·里奇在"C语言的发展 "一文中描述的一致.

数组的不可分配性源于三个事实:

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

  2. 数组一直很特殊,尤其是它们与指针的关系; 这种特殊关系是从C的前身语言B中对数组的处理发展而来的.

  3. 结构最初不在C.

由于第2点,不可能分配数组,并且由于第1点,它无论如何都不可能,因为单个赋值运算符=不应该扩展到可能需要N千个周期来复制N 000个元素数组的代码.

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

当C得到结构时,它们最初也不是完全一流的,因为你无法分配或返回它们.但你不能的原因只是第一个编译器不够聪明,起初,生成代码.没有句法或语义障碍,就像阵列一样.

并且一直以来的目标是结构是一流的,这是在相对较早的时候实现的,很快就会出现第一版K&R打印出来的时间.

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

我相信(虽然这是我的更多猜测),思维是这样的:"一流的类型是好的,二等类型是不幸的.我们坚持阵列的二等状态,但我们可以使用结构更好.不昂贵的代码规则实际上不是一个规则,它更像是一个指导.数组通常很大,但结构通常是小的,几十或几百个字节,所以分配它们不会通常是过于昂贵."

因此,不昂贵的代码规则的一致应用就被淘汰了.无论如何,C从未完全规律或一致.(就此而言,也不是绝大多数成功的语言,无论是人类还是人工语言.)

尽管如此,可能值得一提的是,"如果C 确实支持分配和返回数组怎么办?那该怎么办?" 并且答案将涉及某种方式来关闭表达式中数组的默认行为,即它们倾向于变成指向其第一个元素的指针.

回到90年代的某个时候,IIRC,有一个相当经过深思熟虑的提议来做到这一点.我认为它涉及将数组表达式包含在[ ][[ ]]等等中.今天我似乎无法找到任何提及的建议(尽管如果有人可以提供参考,我将不胜感激).现在我要假设一个新的运算符或伪函数arrayval().

我们可以通过执行以下操作来扩展C以允许数组赋值:

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

  2. 删除禁止声明数组值函数的禁令.回到原来的问题,做出char f(void)[8] { ... }合法的.

  3. (这是最重要的.)有一种方法可以在表达式中提及数组,并以数组类型的真值,可赋值(rvalue)结束.如上所述,现在我将提出语法arrayval( ... ).

[附注:今天我们有一个" 关键定义 "

对表达式中出现的数组类型的对象的引用将(有三个例外)衰减到指向其第一个元素的指针中.

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

无论如何,通过这些修改,我们可以编写类似的东西

char a[8], b[8] = "Hello";
a = arrayval(b);
Run Code Online (Sandbox Code Playgroud)

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

鉴于功能原型

char f(void)[8];
Run Code Online (Sandbox Code Playgroud)

我们也可以这样做

a = f();
Run Code Online (Sandbox Code Playgroud)

让我们来看看f假设的定义.我们可能有类似的东西

char f(void)[8] {
    char ret[8];
    /* ... fill ... */
    return arrayval(ret);
}
Run Code Online (Sandbox Code Playgroud)

请注意(除了假设的新arrayval()运算符),这正是Dario Rodriguez最初发布的内容.还要注意 - 在假设的世界中,阵列分配是合法的,并且arrayval()存在类似的东西- 这实际上是有效的!特别是,它不会遇到将很快无效的指针返回到本地数组的问题ret.它会返回一个数组的副本,所以根本不会有任何问题 - 这与显然是合法的完全类似

int g(void) {
    int ret;
    /* ... compute ... */
    return ret;
}
Run Code Online (Sandbox Code Playgroud)

最后,回到"是否还有其他类型的二类?"这一侧面问题,我认为,当数组之类的函数不被自己用作时,它们的地址会自动获取,这不仅仅是巧合(也就是说,作为函数或数组),并且类似地没有函数类型的右值.但这主要是一种空洞的沉思,因为我不认为我曾经在C中听过被称为"二等"类型的功能.(也许他们有,而且我已经忘记了.)

  • @JohnBollinger有数组类型的左值但没有右值. (3认同)
  • +1有效地解释了为什么没有办法扩展具有此功能的语言而不与现有约束冲突. (3认同)
  • @JohnBollinger C 标准使用术语“值”来表示可能被称为“右值”的东西 (2认同)
  • @nm:即使这不是真的 - 在问题中的第二个例子中,`f().arr` 是一个右值数组。 (2认同)
  • @JohnBollinger 这确实有点简化,但我觉得很有用。我的意思更具体,正如 nm 和 MM(你们两个有关系吗?:-))指出的,我现在已经澄清,没有数组类型的右值。(当然,要说 arrys 是否可以是左值,这是一个相当困难的问题。) (2认同)

Joh*_*ger 20

直接返回数组有什么用?我的意思是,不是在优化或计算复杂性方面,而是在没有结构层的情况下这样做的实际能力方面.

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

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

  • 在提出与Java的比较之后,我注意到记录中,术语"对象"的Java含义与该术语的C含义不同. (3认同)
  • _"数组类型的表达式几乎在所有上下文中自动转换为指针."_这是真正的答案,恕我直言.从编译器POV开始,确定你是指整个数组还是指向第一个元素的指针都是_hard_:应该有类似`return(array)b`或`return b的东西`明确区分.显然,`struct`s不能有这种含糊之处. (2认同)

Rom*_*sky 5

对于成为一流对象的数组,您至少希望能够对它们进行赋值。但这需要了解大小,并且 C 类型系统不够强大,无法将大小附加到任何类型。C++ 可以做到这一点,但由于遗留问题而无法做到这一点\xe2\x80\x94它引用了特定大小的数组typedef char (&some_chars)[32] ),但普通数组仍然像 C 中一样隐式转换为指针。C++ 有 std::array 代替,这基本上是前面提到的结构内数组加上一些语法糖。

\n

  • 如果 C 类型系统不够强大,无法将大小附加到类型上,那么“sizeof”运算符如何工作?但它“确实”有效,包括数组类型。 (3认同)