为什么在C语言中不允许给数组分配值?

7
如果我写下如下内容:
int some_arr[4];
some_arr = {0, 1, 2, 3};

然后我的编译器(在这种情况下是GCC)会抱怨我在{之前没有表达式。因此我需要使用复合字面量,很好:

int some_arr[4];
some_arr = (int[]){0, 1, 2, 3};

现在我们发现不允许将值赋给数组。

什么?

我可以通过像memcpy(some_arr, (int[]){0, 1, 2, 3}, sizeof(int[4]))这样的方式绕过这个问题,或者逐个元素地赋值给some_arr(或通过循环)。我无法想象GCC无法解析我编写的各个赋值操作(一个不关心用户的懒惰编译器甚至可能在预处理器中完成),所以似乎归根结底是“标准说不行”。那么为什么标准会规定这个特定的事情是禁止的呢?

我不是要找出标准中规定不允许的语言,而是要了解该标准的历史背景。


2
有趣的是,如果数组是一个结构体中的字段,那么C语言会很愉快地让你将整个结构体赋值给它,包括数组。(这有时被用来从函数中返回一个堆栈分配的数组。) - ruakh
2
您所请求的不是 C 语言的可用功能。您必须一个一个地分配元素。在声明中,您可以使用 int some_arr[4] = {0, 1, 2, 3}; 一次性初始化整个数组。请注意初始化和分配之间的区别。 - Pierre François
2
C语言是一种古老的编程语言。C语言中的操作被设计成类似汇编语言,将数组的内容赋值给另一个数组是一个复杂的操作,需要多个指令甚至循环才能完成,因此不是原始操作,也没有包含在内。 - Simson
1
也许你的问题可以归结为这个:为什么数组类型对象是不可修改的? - Bob__
2
我猜为什么可修改的左值不能具有数组类型?是我们最好的帖子。我试图从Ritchie的“C语言开发”中找到任何直接引用,但没有找到。此外,这个答案的最后编辑几乎是Ritchie的论文解释理由的精确摘要。 - Lundin
显示剩余11条评论
2个回答

5

来自ISO/IEC 9899:1999的关于赋值运算符限制的规定:

§6.5.16 赋值运算符必须将可修改的左值作为其左操作数。

然后是关于可修改的左值:

§6.3.2.1 可修改的左值是指不具有数组类型、不具有不完整类型、不具有const修饰类型的左值,如果它是一个结构体或联合体,则不具有任何成员(包括所有包含的聚合体或联合体的递归元素)具有const修饰类型。

为什么不能有const修饰?可能是因为数组名称会衰减为指向第一个元素的指针。


然而,一个由结构体包装的数组赋值是被允许的,如下所示:

//gcc 5.4.0

#include  <stdio.h>

struct A
{
    int arr[3];
    int b;
};

struct A foo()
{
    struct A a = {{1, 2, 3},10};
    return a;
}

int main(void)
{
    struct A b = foo();
    for (int i=0; i<3; i++)
          printf("%d\n",b.arr[i]);
    printf("%d\n", b.b);
}

收益率

1
2
3
10

1
可修改的左值是指没有数组类型的左值。我不想听起来像一个一直问“为什么”的幼儿,但是... - c-x-berger
1
@c-x-berger 我认为这会使编译器变得更加复杂和缓慢。 - Tony
好的,我想这样也行吧。对我来说,这似乎是一个奇怪的停顿点,但我只是一个学习语言的人,而不是设计语言的人。 - c-x-berger
1
我认为数组衰减在这里完全不相关。语言定义可以轻松定义数组类型变量的赋值,以避免发生衰减。不这样做是一种选择,既不是必需的也不是显而易见的。 - Konrad Rudolph
2
理由很简单,Ritchie 不想混淆指针赋值和数组赋值。在 B 语言中,所有的数组都与指向第一个元素的指针一起分配。然而这与 C 语言的结构体概念不符,结构体应该直接对应内存。所以 Ritchie 放弃了 B 语言存储地址的方式,引入了数组衰减,也就是当数组在表达式中使用时,你会得到指向第一个元素的指针,而不是 B 语言中第一个元素是指针的方式。这一切都在链接的副本中。 - Lundin

4

tl;dr:

因为C语言决定数组会衰变成指针,并没有提供程序员避免它的方式。

长篇回答:

当你写下

int arr[4];

自此之后,每当你在动态上下文中使用arr时,C都会将arr视为&arr[0],即将数组衰变为指针(也可参见这里这里)。
因此:
arr = (int[]){0, 1, 2, 3};

被认为是
&arr[0] = (int[]){0, 1, 2, 3};

这些内容无法被赋值。编译器可以使用memcpy()实现完整的数组复制,但是C需要提供一种方法告诉编译器何时要降级为指针,何时不要。

请注意,动态上下文和静态上下文是不同的。sizeof(arr)&arr是在编译时处理的静态上下文,在其中将arr视为一个数组。

同样地,初始化

int arr[4] = {0, 1, 2, 3};

或者

int arr[] = {0, 1, 2, 3};

静态上下文 - 这些初始化发生在程序加载到内存之前,甚至在执行之前。标准中的语言是:

除非它是 sizeof 操作符或一元 & 操作符的操作数,或者是用于初始化数组的字符串字面值,否则具有类型“type 的数组”的表达式会被转换为一个具有类型“指向类型的指针”的表达式,该指针指向数组对象的初始元素,并且不是 lvalue。如果数组对象具有寄存器存储类,则行为未定义。

当数组位于结构体内部时,例如:

struct s {
    int arr[4];
};
struct s s1, s2;

再次使用s1.arr就像&s1.arr[0]一样,它不能被赋值。

然而,虽然s1 = s2是动态上下文,但它并不是引用数组。编译器知道它需要复制整个数组,因为它是结构的定义的一部分,并且此赋值是隐式生成的。例如,如果编译器选择使用memcpy()实现结构体赋值,则数组会自动复制。


你可以在引用的语言C11标准-6.3.2.1其他操作数-Lvalues、数组和函数指示器(p3)中回答问题,..具有类型"类型数组"... 并且不是lvalue ' ...' - David C. Rankin
3
这是诉诸假设。语言规范本可以轻易地在静态上下文的定义中添加对可修改左值的例外,并从可修改左值中删除对数组的例外。这样做不会增加语言定义的复杂度... C语言特意作出这种具体选择并没有固有的自然原因。 - Konrad Rudolph
如果我理解正确,问题不是“为什么数组会衰变成指针”或者“标准对左值数组有什么规定”,而是“为什么标准要对左值数组做出这样的规定”,答案是“因为数组会衰变成指针”。 - root
"C语言必须提供一种方法来告诉编译器何时要衰减为指针,何时不需要。" - 它已经提供了,请参见6.3.2.1/2。这可以很容易地修改以排除所讨论的情况。 - M.M
@M.M,你没有选择的余地。例如:int f(int a[3])将接受任何int数组或int指针,没有运算符可以改变这一点。你不能告诉C强制执行数组大小(即不衰减)。我确实同意它可能会产生例外情况,虽然它可能会稍微不一致,但它会非常有用。 - root
@root 我们在讨论赋值表达式,而不是函数参数。 - M.M

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接