首先,是的,你可以将数组封装在一个 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篇论文中所描述的是一致的.
数组的不可分配性来自三个事实:
C在语法和语义上都接近机器硬件.C语言中的一个基本操作应该编译成一条或几条机器指令,占用一个或几个处理器周期.
数组一直都很特别,尤其是在它们与指针的关系上;这种特殊的关系是从C的前一种语言B中对数组的处理演变而来的,并受到很大的影响.
struct 最初不是用C语言编写的.
由于第2点,不可能赋值数组,而由于第1点,无论如何都不可能赋值,因为单个赋值运算符=
不应该扩展到可能需要N个千个周期才能复制N个千个元素的数组的代码.
然后我们到了第三点,这真的导致了一个矛盾.
当C获得 struct 时,它们最初也不是完全一流的,因为您无法分配或返回它们.但你不能这么做的原因很简单,第一个编译器一开始不够聪明,无法生成代码.与数组一样,没有语法或语义障碍.
一直以来,我们的目标都是让建筑达到一流水平,这一点在相对较早的时候就实现了.在《K&;R打算打印.
但是问题仍然存在,如果一个基本操作应该编译成少量的指令和循环,为什么这个参数不允许 struct 赋值呢?答案是,是的,这是一个矛盾.
我相信(虽然这对我来说是更多的猜测)这种 idea 是这样的:"第一类类型是好的,第二类类型是不幸的.我们被困在数组的第二类状态,但我们可以用 struct 做得更好.不需要昂贵的代码规则并不是一个真正的规则,它更多的是一个指导原则.数组通常是大的,但 struct 通常是小的,几十或几百个字节,所以分配它们通常不需要e too很贵."
因此,一致地应用"无需昂贵代码"规则就被搁置了.无论如何,C从来都不是完美的规则或一致的.(就这一点而言,绝大多数成功的语言,无论是人类语言还是人工语言,都不是.)
尽管如此,也许值得一问:"如果Cdid支持分配和返回数组呢?这是怎么回事?"答案必须包括某种方式来关闭表达式中数组的默认行为,即它们倾向于变成指向第一个元素的指针.
早在20世纪90年代的某个时候,IIRC就有一个相当深思熟虑的建议来实现这一点.我认为它包括用[ ]
或[[ ]]
或类似的格式将数组表达式括起来.今天,我似乎找不到任何提到这个建议的地方(尽管如果有人能提供参考,我会非常感激).无论如何,我相信我们可以通过执行以下三个步骤来扩展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()
运营商),这正是达里奥·罗德里格斯最初发布的内容.还要注意的是,在一个假设的世界里,数组赋值是合法的,大约有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()
的优化在线版本完全相同.(事实上,这种技巧至今仍能在现代编译器下编译和工作.)