为什么在C语言中存在结构体赋值但不存在数组赋值操作?

9
int a[10];
int b[10];

a = b; // illegal

typedef struct {
    int real;
    int imag;
    } complex;

complex c,d;
c = d; //legal

[我意识到在第一种情况下a和b是地址,而在第二种情况下是符号]


7
因为在C语言中,数组不是lvalue。这就是这门语言的特性。 - cnicutar
一个反例:int foo[10]; &foo; @cnicutar - Keith Thompson
1
@cnicutar 一个数组是一个左值。但数组不是一个可修改的左值。 - ouah
@ouah:数组是一个对象;lvalue 是表达式的一种。数组表达式可以是(通常也是)lvalue,但在大多数上下文中,它会被隐式转换为非值指针表达式。 - Keith Thompson
在C99的6.3.2.1p1中,数组被描述为不可修改的左值,“可修改的左值是指没有数组类型的左值,[...]”。 - ouah
显示剩余2条评论
7个回答

12

关于历史信息,这可能会有趣:http://cm.bell-labs.com/who/dmr/chist.html

B语言中,声明数组将为数组分配内存,就像C语言一样,但变量的名称用于定义指向该数组的指针。Ritchie在C语言中对此进行了更改,因此名称“is”数组,但在使用时可以衰减为指针:

今天的C保留了规则,即数组类型的值在表达式中出现时将转换为指向组成数组的第一个对象的指针。

这个发明使得大多数现有的B代码能够继续工作,尽管语言语义发生了基本变化。那些为了调整其起始位置而给数组名称赋新值的少数程序——这在B和BCPL中是可能的,在C中毫无意义——很容易修复。

如果在那个非常早期的阶段,Ritchie定义a = b来复制数组,那么他试图从B到C移植的代码将不会那么容易修复。由他定义的那个代码将会出错,然后他可以修复它。如果他让C复制数组,那么他将默默地改变代码复制数组的含义,而不是重新设置用于访问数组的名称。

仍然有一个问题,“为什么这个特性在40年后没有被添加”,但我认为这就是为什么一开始没有它。实现它需要付出努力,而这种努力实际上会使早期版本的C更糟,因为将B和BCPL代码移植到C略微困难。所以Ritchie当然没有这样做。


谢谢。这是一次非常有趣的C语言历史之旅! - kkm

5
因为C 语言规定了“可修改的左值(lvalue)是一个不含有数组类型,也不是不完整类型”的左值。所以,数组无法被赋值。
此外,在将数组名用于a = b;等值语境时,数组名 a 和 b 都表示 &a[0]&b[0] 的地址。这通常被称作数组“衰退”为指针。
然而,数组并不是指针,因此试图通过指针来赋值数组是没有意义的。

好的,我理解了那部分。语言作者本可以选择这样的行为,即没有地址运算符的array_var表示整个数组,就像没有&的struct_var表示整个结构体一样。这样就可以进行赋值操作了! - Hari

3
主要原因当然是标准。在赋值运算符约束条件中,它说:
(C99,6.5.16p2)“赋值运算符的左操作数必须是可修改的lvalue”。
它定义了可修改lvalue为
(C99,6.3.2.1p1)“可修改的lvalue是一个不具有数组类型的lvalue”,
所以不允许对数组进行赋值。
但主要原因是历史原因,在那个时代,将数组复制到硬件上被认为是不合适的(旧的PDP系统)。请注意,即使在C的第一版中,也不允许对结构类型对象进行赋值。它后来被添加到语言中,但是为了对数组进行赋值,需要改变语言的许多部分。

3
首先要理解的是,数组不是指针。请阅读comp.lang.c FAQ的第6部分内容。我等待一下。
...
好了吗?不,回去把整篇文章都读完。
...
太好了,谢谢。
一般来说,在C语言中,数组是二等公民。有数组类型、数组对象和数组值,但几乎总是通过指向数组元素的指针来操作数组。
这需要程序员做更多的工作(正如你所见,你不能只赋值给数组),但它也给了你更大的灵活性。处理数组的程序通常需要处理不同大小的数组,甚至大小在执行时无法确定。即使允许数组赋值,由10个int组成的数组和由20个int组成的数组是不同的且不兼容的类型。如果你有一个固定大小的数组,就像你问题中的代码一样,只有部分元素当前是相关的;你可能有一个10个元素的数组,但你只当前使用了前5个元素。逐个处理此类数组元素使得更容易只处理当前处于活动状态的元素(这是你自己要跟踪的)。
另一方面,对于结构体,当定义类型时确定成员的数量和类型。你不能像处理数组那样通过移动指针来遍历结构体的成员,因为成员通常是不同类型的。数组和结构体是不同的东西,它们有不同的操作集合。
语言中有几个规则试图使这些任务更容易完成,具体包括:
  • 在大多数但不是所有情况下,数组表达式会隐式转换为指向数组第一个元素的指针。例外情况包括:
    • 当数组表达式是&(地址)运算符的操作数时;
    • 当它是sizeof的操作数时;以及
    • 在初始化器中作为字符串文字使用,用于初始化数组对象。
  • 声明的数组参数,例如int func(char s[]);调整为指针参数:int func(char *s);
(人们可以争论说这些规则给人们带来的困惑比防止困惑更多,但这就是语言的定义方式。)
现在,我认为语言可以被定义或重新定义,以便在有意义的情况下允许数组赋值:
int a[10];
int b[10];
/* ... */
a = b; /* Why not? */.

也许这样的更改甚至可以在不破坏现有代码的情况下进行。但这需要另一个针对数组到指针转换规则的特殊情况。而且它只对像a和b这样的固定大小数组有用,虽然它们在初级编程练习中非常常见,但在生产代码中并不常见。

-1

数组名是一个常量指针,因此您无法更改它所指向的内容。

假设您在最后一行的意思是 c = d 是合法的,那么这只是将一个非常量变量复制到另一个非常量变量,这是完全合法的。


2
不,数组不是指针,只是在某些情况下会衰变成指针。 - Blagovest Buyukliev
@BlagovestBuyukliev:他写的是数组名,而不是数组。我认为打负分有点过分。尤其是因为C语言中数组和指针之间的微妙差别是一个高级主题。与一个正在努力理解指针的初学者讨论这个问题,可能产生有限的教学价值。 - Lundin
@Lundin:我认为,在尽早的阶段理解数组和指针之间的区别非常重要;StackOverflow上有关将多维数组传递到函数中等问题的数量证明了这一点! - Oliver Charlesworth

-1

a 实际上是数组的第一个元素的“指针”,它是一个常量“指针”,因此您正在尝试分配一个 l-“指针”。

您可以通过以下方式实现您想要做的事情:

struct arraystruct
{
  int t[10];
};


struct arraystruct a,b;

a=b;

编辑:我忘了提到有一些例外情况,其中数组不应被视为指针:

- 您可以使用sizeof(array),但不能使用sizeof(pointer)

- 字面字符串的数组

- a和&a是相同的


@OliCharlesworth:但在这种情况下,a会衰变为struct arraystruct* const,因为a被用于表达式中。因此答案并不是真正的错误,只是简化了。 - Lundin

-2

这是因为你使用的数组类型是所谓的静态数组,即其内存位于堆栈上。如果你使用动态数组(带指针)则你的赋值将是合法的(但可能会出现内存泄漏)。这将是一次浅拷贝。

另请参见C++中的静态数组与动态数组


“动态数组”在语言术语中并不是真正的数组,而是指向动态分配的内存块的指针。 - Blagovest Buyukliev
@BlagovestBuyukliev: 你怎么判断呢?我认为这取决于使用malloc的指针是如何声明的。如果它像这样声明int (*array)[10] = malloc(10*sizeof(int));,那么我肯定会认为它是语言中的数组。此外,这些评论并没有帮助到OP。 - Lundin
@Lundin:不,你的例子中的array仍然是一个指向数组的指针,而不是数组本身。sizeof(array)将给出指针的大小,而不是10 * sizeof(int)的大小。 - Blagovest Buyukliev
@BlagovestBuyukliev:我的意思是,在我的例子中,指针所指向的内容只能被视为一个数组,而不是“一块内存块”。对于数组指针来说,使用名称“数组”可能是一个不好的选择。 - Lundin
@Lundin,int (*array)[10] = malloc(10*sizeof(int));是否少了一个*?我的理解是,这段代码创建了一个包含10个指向int值的静态指针数组,并且应该为int (*array)[10] = malloc(10*sizeof(int*)); - fredo
@fredo:这段代码声明了一个指向动态分配的包含10个int的数组的_array指针。我认为你把它和广泛使用的指向指针语法搞混了,该语法在动态分配2D数组时(不幸地)很常见。 - Lundin

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