首先,是的,您可以将数组封装在结构中,然后对该结构执行任何您想要的操作(分配它,从函数返回它,等等)。
第二,您已经发现,编译器发出代码以返回(或分配)结构几乎没有困难。因此,这也不是您不能返回数组的原因。
不能这样做的根本原因是,直言不讳地说,数组是C语言中的第二类数据结构。所有其他数据结构都是一流的。从这个意义上说,“第一等”和“第二等”的定义是什么?只是不能分配第二类类型。
(您的下一个问题很可能是“除了数组,还有其他第二类数据类型吗?”,我认为答案是“不是,除非您对函数进行计数”。)
与您无法返回(或分配)数组这一事实密切相关的是,也没有数组类型的值。存在数组类型的对象(变量),但是每当您尝试获取值1时,都将获得指向数组第一个元素的指针。[注:更正式地讲,没有数组类型的右值,尽管可以将数组类型的对象视为左值,尽管,尽管这是不可分配的。]
因此,除了不能分配给数组的事实外,还不能生成要分配给数组的值。如果你说
char a[10], b[10];
a = b;
好像你写过
a = &b[0];
因此,我们在右侧有一个指针,在左侧有一个数组,即使数组以某种方式是可分配的,我们也会遇到巨大的类型不匹配。同样(根据您的示例),如果我们尝试编写
a = f();
和定义函数内的某处f(),我们有
char ret[10];
return ret;
好像最后一行说
return &ret[0];
同样,我们没有要返回并分配给的数组值,而a只是一个指针。
(在函数调用示例中,我们还遇到了一个非常重要的问题,ret即本地数组,试图在C中返回是危险的。稍后对此进行更多介绍。)
现在,你的问题的一部分可能是“为什么会这样?”,也“如果你不能分配数组,为什么可以包含数组分配结构?”
接下来是我的解释和看法,但这与Dennis Ritchie在《 C语言的发展》一文中描述的一致。。
数组的不可分配性来自三个事实:
C旨在在语法和语义上接近机器硬件。C语言中的基本操作应编译为一个或几个机器指令,占用一个或几个处理器周期。
数组一直很特别,特别是在它们与指针相关的方式上。这种特殊的关系是从C的前身语言B中对数组的处理演变而来,并受到其严重影响。
结构最初不是用C语言编写的。
由于第2点,不可能分配数组,而由于第1点,则无论如何都不可能,因为单个赋值运算符=不应扩展为可能花费N千个周期来复制N千个元素数组的代码。
然后我们到达第3点,这实际上最终形成了一个矛盾。
当C获得结构时,它们最初也不是完全一流的,因为您无法分配或返回它们。但是,您之所以不能做到这一点,仅仅是因为第一个编译器起初不足以生成代码。没有数组的语法或语义障碍。
一直以来,我们的目标都是使结构达到一流,这是相对较早实现的,大约是在第一版K&R即将出版时。
但是,仍然存在一个大问题,如果基本操作应该编译为少量的指令和周期,那么为什么该参数不禁止结构分配呢?答案是,是的,这是一个矛盾。
我相信(尽管这更多是我的猜测)这种想法是这样的:“第一类类型是好的,第二类类型是不幸的。我们在数组中处于第二类状态,但是我们可以在结构上做得更好。无昂贵代码规则并不是真正的规则,它更像是一个准则。数组通常会很大,但是结构通常会很小,几十个或几百个字节,因此分配它们不会通常是过于昂贵。”
因此,一致地应用免费代码规则就被抛在了一边。无论如何,C从来都不是完全规则或一致的。(就此而言,绝大多数成功的语言,无论是人为的还是人为的)。
有了这一切说,它可能是值得询问“如果C做支撑分配和返回数组?如何可能的工作?” 答案必须包括某种方式来关闭表达式中数组的默认行为,即它们倾向于变成指向其第一个元素的指针。
在20世纪90年代的某个时候,IIRC提出了一个经过深思熟虑的提议来做到这一点。我认为这涉及封闭的阵列式[ ]或[[ ]]什么的。今天,我似乎找不到任何关于该提议的提法(尽管我很感谢有人可以提供参考)。无论如何,我相信我们可以通过执行以下三个步骤来扩展C以允许进行数组分配:
取消禁止在赋值运算符的左侧使用数组。
取消禁止声明数组值函数。回到最初的问题,使之char f(void)[8] { ... }合法。
(这是最大的问题。)有一种方法可以在表达式中提及一个数组,并以一个真正的,可分配的数组类型值(rvalue)结束。为了论证,我将放置一个名为的新运算符或伪函数arrayval( ... )。
[旁注:今天,我们有了数组/指针对应关系的“键定义”,即:
出现在表达式中的对数组类型对象的引用会衰减(除三个例外外)指向其第一个元素的指针。
这三个例外是当数组是一个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];
return arrayval(ret);
}
请注意(假设的新arrayval()运算符除外),这与Dario Rodriguez最初发布的内容相同。还要注意-在假设的世界中,数组分配是合法的,并且arrayval()存在类似的东西-这实际上是可行的!特别是,它不会遇到将即将失效的指针返回到本地数组的问题ret。它会返回该数组的副本,因此完全没有问题-几乎完全类似于合法的法律
int g(void) {
int ret;
return ret;
}
最后,回到旁边的问题“还有其他第二类类型吗?”,我认为函数(例如数组)在不被使用时自动获取其地址(这是巧合,这绝非偶然)。作为函数或数组),并且类似地没有函数类型的右值。但这主要是一种无聊的沉思,因为我认为我从来没有听说过C语言中称为“第二类”类型的函数(也许它们已经被我忘记了)。
附注:由于编译器是愿意分配结构,通常知道如何发出有效的代码这样做的,它曾经是一个有点招人喜爱增选编译器的结构,复制,以复制从点任意字节机械指向b。特别是,您可以编写这个看起来有些奇怪的宏:
#define MEMCPY(b, a, n) (*(struct foo { char x[n]; } *)(b) = \
*(struct foo *)(a))
的行为或多或少地与的优化内联版本完全相同memcpy()。(实际上,这个技巧仍然可以在当今的现代编译器中编译和运行。)