c - 无法返回数组在 C 中实际上意味着什么?

标签 c

我不是要复制关于 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。

最佳答案

首先,是的,您可以将数组封装在一个结构中,然后对该结构执行您想做的任何操作(分配它,从函数中返回它等)。
其次,正如您所发现的,编译器发出返回(或分配)结构的代码几乎没有困难。所以这也不是你不能返回数组的原因。
你不能这样做的根本原因是,坦率地说,数组是 C 中的二等数据结构。所有其他数据结构都是一等的。在这个意义上,“一流”和“二流”的定义是什么?简单地说,不能分配二等类型。
(你的下一个问题可能是,“除了数组,还有其他二等数据类型吗?”,我认为答案是“不是真的,除非你计算函数”。)
与不能返回(或分配)数组的事实密切相关的是,也没有数组类型的值。有数组类型的对象(变量),但是每当您尝试取值时,您都会立即获得指向数组第一个元素的指针。 [脚注:更正式地说,没有数组类型的右值,尽管数组类型的对象可以被认为是左值,尽管是不可赋值的。]
因此,除了您无法分配给数组的事实之外,您甚至无法生成尝试分配的值。如果你说

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 中返回是危险的。稍后将详细介绍这一点。)
现在,您的问题的一部分可能是“为什么是这样?”,以及“如果您不能分配数组,为什么您可以分配包含数组的结构?”
以下是我的解释和我的观点,但它与丹尼斯·里奇在他的论文 The Development of the C Language 中描述的内容一致。
数组的不可分配性源于三个事实:
  • C 旨在在语法和语义上接近机器硬件。 C 语言中的基本操作应该编译为一个或几个机器指令,需要一个或几个处理器周期。
  • 数组一直很特别,尤其是它们与指针的关系;这种特殊关系源自 C 的前身语言 B 中对数组的处理,并深受其影响。
  • 结构最初不在 C 中。

  • 由于第 2 点,不可能分配数组,而由于第 1 点,无论如何都不可能,因为单个赋值运算符 = 不应扩展为可能需要 N 千个周期来复制 N 千个元素数组的代码.
    然后我们到了第 3 点,这最终导致了矛盾。
    当 C 获得结构时,它们最初也不是完全一流的,因为您无法分配或返回它们。但你不能这样做的原因很简单,第一个编译器起初不够聪明,无法生成代码。没有语法或语义障碍,就像数组一样。
    一直以来的目标都是建筑一流,而且这个目标实现的比较早。编译器 catch 了,并学会了如何发出代码来分配和返回结构,大约在 K&R 第一版即将出版的时候。
    但问题仍然存在,如果一个基本操作应该编译为少量指令和周期,为什么该参数不允许结构赋值?答案是,是的,这是一个矛盾。
    我相信(虽然这更多是我的猜测)这种想法是这样的:“一流的类型是好的,二等的类型是不幸的。我们被困在数组的二等地位,但我们可以用结构体做得更好。无昂贵代码规则并不是真正的规则,它更像是一个准则。数组通常很大,但结构体通常很小,几十或几百个字节,因此分配它们不会通常太贵了。”
    因此,无昂贵代码规则的一致应用被搁置了。无论如何,C 从来都不是完全规则或一致的。 (就此而言,绝大多数成功的语言也不是人类的,也不是人工的。)
    说了这么多,也许值得一问,“如果 C 确实支持分配和返回数组怎么办?这会如何工作?”答案必须涉及关闭表达式中数组的默认行为的某种方式,即它们倾向于变成指向其第一个元素的指针。
    早在 90 年代的某个时候,IIRC 就有一个经过深思熟虑的提议来做到这一点。我认为它涉及在 [ ][[ ]] 或其他东西中包含一个数组表达式。今天我似乎找不到任何提及该提案的内容(尽管如果有人可以提供引用,我将不胜感激)。无论如何,我相信我们可以通过以下三个步骤扩展 C 以允许数组分配:
  • 取消禁止在赋值运算符的左侧使用数组。
  • 取消禁止声明数组值函数。回到最初的问题,使 char f(void)[8] { ... } 合法。
  • (这是大问题。)有一种方法可以在表达式中提及数组,并以数组类型的真实可分配值(右值)结束。为了论证起见,我将假设一个名为 arrayval( ... ) 的新运算符或伪函数。

  • 【旁注:今天我们有一个“key definition”的数组/指针对应关系,即:

    A reference to an object of array type which appears in an expression decays (with three exceptions) into a pointer to its first element.


    三个异常(exception)是当数组是 sizeof 运算符或 & 运算符的操作数,或者是字符数组的字符串文字初始值设定项时。在我在这里讨论的假设修改下,会有第四个异常(exception),即当数组是这个新的 arrayval 运算符的操作数时。]
    不管怎样,有了这些修改,我们就可以写出类似的东西
    char a[8], b[8] = "Hello";
    a = arrayval(b);
    
    (显然,如果 ab 的大小不同,我们还必须决定该怎么做。)
    给定函数原型(prototype)
    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 中被称为“二等”类型的函数。(也许他们听说过,但我已经忘记了。)

    脚注:因为编译器愿意分配结构,并且通常知道如何为此发出有效的代码,所以它曾经是一种比较流行的技巧,即选择编译器的结构复制机制以从 a 点复制任意字节点b。特别是,您可以编写这个看起来有些奇怪的宏:
    #define MEMCPY(b, a, n) (*(struct foo { char x[n]; } *)(b) = \
                             *(struct foo *)(a))
    
    其行为或多或少与 memcpy() 的优化内联版本完全相同。 (事实上​​,这个技巧今天仍然可以在现代编译器下编译和工作。)

    关于c - 无法返回数组在 C 中实际上意味着什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50808782/

    相关文章:

    c - 奇怪的C编码风格

    c - 需要解释 "Towers of Hanoi"中的递归调用

    c - 函数可能是属性 ‘noreturn’ 的候选者

    c - SSL BIO 和 FFI

    c++ - ##(双哈希)在预处理器指令中起什么作用?

    c - Linux 上的系统调用参数类型是什么?

    c - 将指向结构体的指针数组传递给函数

    c - 在c中打印二维数组

    c - 如何将我的 iSCSIinitiator 连接到 Windows 中的 scsi 子系统

    c - 如何检查变量是否包含字符串