我不想重复通常的问题,即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).

直接返回数组是什么意思?我的意思是,不是在优化或计算复杂度方面,而是在没有 struct 层的情况下实现这一点的实际能力方面.

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

推荐答案

首先,是的,你可以将数组封装在一个 struct 中,然后用这个 struct 做任何你想做的事情(分配它,从函数返回它,等等).

其次,正如您所发现的,编译器在发出代码以返回(或分配) struct 方面没有什么困难.所以这也不是不能返回数组的原因.

你不能这样做的根本原因是,直截了当地说,是arrays are second-class data structures in C.所有其他数据 struct 都是一流的.在这个意义上,"一流"和"二流"的定义是什么?简单地说,不能分配二类类型.

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

与不能返回(或分配)数组密切相关的是,也没有数组类型的值.有数组类型的对象(变量),但每当您try 获取其中一个的值时,就会立即得到指向数组第一个元素的指针.[脚注:更正式地说,数组类型没有rvalues,尽管数组类型的对象可以被视为lvalue,尽管它是不可分配的对象.]

所以,除了不能赋值给数组之外,你甚至不能生成一个试图赋值的值.如果你说

char a[10], b[10];
a = b;

就好像你写了

a = &b[0];

我们在左边有一个数组,但在右边有一个指针,即使数组是可赋值的,我们也会有大量的类型不匹配.同样(从你的例子中)如果我们试着写

a = f();

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

char ret[10];
/* ... fill ... */
return ret;

就好像最后一句话说

return &ret[0];

同样,我们没有要返回的数组值并将其赋给a,只有一个指针.

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

现在,你的部分问题可能是"为什么会这样?",还有"如果你不能分配数组,为什么要分配包含数组的 struct ?"

下面是我的解释和观点,但这与丹尼斯·里奇在他的100篇论文中所描述的是一致的.

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

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

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

  3. struct 最初不是用C语言编写的.

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

然后我们到了第三点,这真的导致了一个矛盾.

当C获得 struct 时,它们最初也不是完全一流的,因为您无法分配或返回它们.但你不能这么做的原因很简单,第一个编译器一开始不够聪明,无法生成代码.与数组一样,没有语法或语义障碍.

一直以来,我们的目标都是让建筑达到一流水平,这一点在相对较早的时候就实现了.在《K&;R打算打印.

但是问题仍然存在,如果一个基本操作应该编译成少量的指令和循环,为什么这个参数不允许 struct 赋值呢?答案是,是的,这是一个矛盾.

我相信(虽然这对我来说是更多的猜测)这种 idea 是这样的:"第一类类型是好的,第二类类型是不幸的.我们被困在数组的第二类状态,但我们可以用 struct 做得更好.不需要昂贵的代码规则并不是一个真正的规则,它更多的是一个指导原则.数组通常是大的,但 struct 通常是小的,几十或几百个字节,所以分配它们通常不需要e too很贵."

因此,一致地应用"无需昂贵代码"规则就被搁置了.无论如何,C从来都不是完美的规则或一致的.(就这一点而言,绝大多数成功的语言,无论是人类语言还是人工语言,都不是.)

尽管如此,也许值得一问:"如果Cdid支持分配和返回数组呢?这是怎么回事?"答案必须包括某种方式来关闭表达式中数组的默认行为,即它们倾向于变成指向第一个元素的指针.

早在20世纪90年代的某个时候,IIRC就有一个相当深思熟虑的建议来实现这一点.我认为它包括用[ ][[ ]]或类似的格式将数组表达式括起来.今天,我似乎找不到任何提到这个建议的地方(尽管如果有人能提供参考,我会非常感激).无论如何,我相信我们可以通过执行以下三个步骤来扩展C语言,以允许数组赋值:

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

  2. 取消声明数组值函数的禁令.回到原来的问题,让char f(void)[8] { ... }合法.

  3. (这才是关键.)有一种方法可以在表达式中提到一个数组,并以数组类型的一个真正的、可赋值的值(rvalue)结束.为了便于论证,我将假定一个名为arrayval( ... )的新运算符或伪函数.

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

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

当数组是sizeof运算符或&运算符的操作数,或是字符数组的字符串文字初始值设定项时,会出现三种例外情况.根据我在这里讨论的假设修改,将有第四个例外,即当数组是这个新的arrayval运算符的操作数时.]

总之,有了这些修改,我们可以写

char a[8], b[8] = "Hello";
a = arrayval(b);

(显然,如果ab的大小不同,我们还必须决定怎么做.)

给出了功能原型

char f(void)[8];

我们也可以

a = f();

让我们看看f的假设定义.我们可能会有

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

请注意(除了假想的新arrayval()运营商),这正是达里奥·罗德里格斯最初发布的内容.还要注意的是,在一个假设的世界里,数组赋值是合法的,大约有arrayval()个数组,这实际上是可行的!尤其是,它将not遇到返回指向本地数组ret的即将失效的指针的问题.它将返回数组中的copy个字符,因此根本不会有问题——它将与显然合法的

int g(void) {
    int ret;
    /* ... compute ... */
    return ret;
}

最后,回到次要问题"是否还有其他第二类类型?",我认为这不仅仅是巧合,函数(如数组)在不被用作自身(即,作为函数或数组)时自动获取其地址,并且同样没有函数类型的右值.但这主要是一种无聊的沉思,因为我认为我从来没有听说过在C中被称为"第二类"类型的函数.(也许他们有,但我忘了.)


脚注:由于编译器is愿意分配 struct ,并且通常知道如何为此发出有效的代码,因此,为了将任意字节从a点复制到b点,使用编译器的 struct 复制机制是一种颇受欢迎的技巧.特别是,您可以编写这个看起来有些奇怪的宏:

#define MEMCPY(b, a, n) (*(struct foo { char x[n]; } *)(b) = \
                         *(struct foo *)(a))

它的行为或多或少与memcpy()的优化在线版本完全相同.(事实上,这种技巧至今仍能在现代编译器下编译和工作.)

C++相关问答推荐

Bison解析器转移/减少冲突

malloc实现:判断正确的分配对齐

如何跨平台处理UTF-16字符串?

在一个小型玩具项目中实现终端历史记录功能

C语言中的strstr问题

使用错误的命令执行程序

对重叠字符串使用MemMove

FRIDA-服务器成为端口扫描的目标?

在另一个函数中使用realloc和指针指向指针

我的C函数起作用了,但我不确定为什么

将变量或参数打包到 struct /联合中是否会带来意想不到的性能损失?

For循环不会迭代所有字符串字符吗?(初学者问题)

C:如何将此代码转换为与数组一起使用?

不带Malloc的链表

在文件描述符上设置FD_CLOEXEC与将其传递给POSIX_SPOWN_FILE_ACTIONS_ADCLOSE有区别吗?

共享目标代码似乎不能在Linux上的进程之间共享

在C中使用字符串时是否不需要内存分配?

execve 不给出which命令的输出

为什么 Linux 共享库 .so 在内存中可能比在磁盘上大?

使用共享变量同步多线程 C 中的函数