为什么C和C ++支持在结构中对数组进行成员分配,但通常不支持?


87

我了解不支持数组的成员分配,因此以下操作将不起作用:

int num1[3] = {1,2,3};
int num2[3];
num2 = num1; // "error: invalid array assignment"

我只是接受这一事实,认为该语言的目的是提供一个开放式框架,并让用户决定如何实现某些功能,例如复制数组。

但是,以下方法确实有效:

struct myStruct { int num[3]; };
struct myStruct struct1 = {{1,2,3}};
struct myStruct struct2;
struct2 = struct1;

该阵列num[3]是构件明智从它的实例在分配struct1到其实例在struct2

为什么结构支持数组的成员分配,但通常不支持?

编辑在结构中std :: string线程中Roger Pate的注释-复制/分配问题?似乎指出了答案的总体方向,但我自己知道的还不够。

编辑2:许多出色的回应。我之所以选择Luther Blissett,是因为我主要想知道该行为背后的哲学或历史原理,但是James McNellis对相关规范文档的引用也很有用。


6
我使它同时具有C和C ++作为标记,因为它源自C。而且,这是一个很好的问题。
GManNickG 2010年

4
可能值得注意的是,很早以前在C语言中,通常不可能进行结构分配,因此必须使用memcpy()或类似方法。
ggg 2010年

仅供参考,... boost::arrayboost.org/doc/libs/release/doc/html/array.html)和现在std::arrayen.cppreference.com/w/cpp/container/array)是STL兼容的替代品凌乱的旧C数组。它们支持复制分配。
Emile Cormier 2013年

@EmileCormier他们是-tada!-数组周围的结构。
彼得-恢复莫妮卡

Answers:


46

这是我的看法:

C语言的发展为C中数组类型的演变提供了一些见识:

我将尝试概述数组的内容:

C的先行者B和BCPL没有明显的数组类型,声明如下:

auto V[10] (B)
or 
let V = vec 10 (BCPL)

将声明V为一个(无类型的)指针,该指针被初始化为指向内存的10个“单词”的未使用区域。就像今天的C / C ++一样,B已经用于*指针解引用,并且具有[] 简写的*(V+i)含义V[i],即。但是,V它不是数组,它仍然是必须指向某些内存的指针。当Dennis Ritchie尝试用结构类型扩展B时,这引起了麻烦。他希望数组成为结构的一部分,就像今天的C语言一样:

struct {
    int inumber;
    char name[14];
};

但是使用B,BCPL数​​组作为指针的概念,这将要求name字段包含一个指针,该指针必须在运行时初始化为该结构内14个字节的内存区域。最终,通过对数组进行特殊处理来解决初始化/布局问题:编译器将跟踪数组在结构中,在堆栈等上的位置,而无需实际实现指向数据的指针(涉及数组的表达式除外)。这种处理使几乎所有B代码仍然可以运行,并且是“如果您看一下数组,数组就会转换为指针”规则的来源。这是一个兼容性黑客,事实证明它非常方便,因为它允许数组具有开放大小等。

这就是我为什么不能分配数组的猜测:由于数组是B中的指针,因此您可以简单地编写:

auto V[10];
V=V+5;

重新设定“数组”的基础。现在这已经没有意义了,因为数组变量的基不再是左值。因此,这种分配是不被允许的,这有助于抓住一些在声明的数组上进行重新引用的程序。然后这个想法就陷入了僵局:由于数组从未被设计为C类型系统的一等奖,因此它们大多被当作特殊的野兽对待,如果使用它们,它们会变成指针。从某种角度(忽略了C数组是一个烂摊子)来看,禁止分配数组还是有一定道理的:将开放数组或数组函数参数视为没有大小信息的指针。编译器没有信息来为其生成数组分配,并且出于兼容性原因需要使用指针分配。

/* Example how array assignment void make things even weirder in C/C++, 
   if we don't want to break existing code.
   It's actually better to leave things as they are...
*/
typedef int vec[3];

void f(vec a, vec b) 
{
    vec x,y; 
    a=b; // pointer assignment
    x=y; // NEW! element-wise assignment
    a=x; // pointer assignment
    x=a; // NEW! element-wise assignment
}

1978年C的修订版增加了结构赋值(http://cm.bell-labs.com/cm/cs/who/dmr/cchanges.pdf)时,情况并没有改变。即使记录在C中不同的类型,也无法在早期K&R C中进行分配。您必须使用memcpy逐成员复制它们,并且只能将指向它们的指针作为函数参数传递。现在,将赋值(和参数传递)简单地定义为该结构的原始内存的memcpy,由于这不会破坏现有代码,因此可以很容易地对其进行分配。作为意外的副作用,这隐式引入了某种数组分配,但是这发生在结构内部的某个位置,因此这实际上并不会引入数组使用方式的问题。


太糟糕了,C没有定义语法,例如int[10] c;不能使左值c表现为包含十个项目的数组,而不是指向十个项目数组的第一项的指针。在某些情况下,能够创建一个typedef很有用,它可以在用于变量时分配空间,但是在用作函数参数时可以传递指针,但是无法获得数组类型的值是一个重大的语义缺陷。语言。
超级猫

重要的一点不是说“必须指向某个内存的指针”,而是指针本身必须像常规指针一样存储在内存中。您稍后的解释中确实会遇到这种情况,但是我认为这更好地突出了关键区别。(在现代C语言中,数组变量的名称确实是指一个内存块,因此没有区别。这是指针本身在逻辑上没有存储在抽象机中的任何位置。)
Peter Cordes

有关历史记录的不错总结,请参见C对数组的厌恶
彼得·科德斯

31

关于赋值运算符,C ++标准规定如下(C ++ 03§5.17/ 1):

有几个赋值运算符...所有都需要可修改的左值作为其左操作数

数组不是可修改的左值。

但是,对类类型对象的分配是专门定义的(第5.17 / 4节):

类的对象的分配由复制分配运算符定义。

因此,我们看一下隐式声明的类的拷贝赋值运算符的作用(第12.8 / 13节):

类X的隐式定义的副本分配运算符执行其子对象的成员分配。...以适合其类型的方式分配每个子对象:
...-
如果子对象是数组,则以适合元素类型的方式分配每个元素
...

因此,对于类类型对象,可以正确复制数组。请注意,如果提供了用户声明的副本分配运算符,则无法利用此功能,并且必须逐元素复制数组。


C语言中的推理类似(C99§6.5.16/ 2):

赋值运算符的左值应为可修改的左值。

§6.3.2.1/ 1:

可修改的左值是没有数组类型的左值... [其他约束如下]

在C中,赋值比在C ++(第6.5.16.1.1 / 2节)中简单得多:

在简单赋值(=)中,将右操作数的值转换为赋值表达式的类型,并替换存储在由左操作数指定的对象中的值。

对于结构类型对象的分配,左操作数和右操作数必须具有相同的类型,因此右操作数的值仅复制到左操作数中。


1
为什么数组是不可变的?还是更确切地说,为什么不像在类类型中那样专门为数组定义赋值?
GManNickG 2010年

1
@GMan:那是更有趣的问题,不是吗。对于C ++,答案可能是“因为它就是C语言中的样子”,而对于C语言,我猜想那仅仅是由于语言的发展(即,原因是历史性的,而不是技术性的),但我还活着当大部分发生时,我将把它留给更有知识的人来回答:-P(FWIW,我在C90或C99基本原理文档中找不到任何内容)。
James McNellis 2010年

2
有人知道C ++ 03标准中“可修改的左值”的定义在哪里吗?它应该在第3.10节中。索引说它是在该页面上定义的,但是没有。§8.3.4/ 5上的(非规范性)注释说:“不能修改数组类型的对象,请参阅3.10。”但§3.10一次不使用“数组”一词。
詹姆斯·麦克奈利斯

@詹姆斯:我只是在做同样的事情。它似乎是指已删除的定义。是的,我一直想知道这背后的真正原因,但这似乎是个谜。我听说过诸如“通过意外分配数组防止人们效率低下”之类的事情,但这太荒谬了。
GManNickG 2010年

1
@GMan,詹姆斯:最近有一个关于comp.lang.c ++ groups.google.com/group/comp.lang.c++/browse_frm/thread/的讨论,如果您错过了并且仍然感兴趣。显然不是因为数组不是可修改的左值(数组当然是左值,并且所有非常量左值都是可修改的),而是因为在RHS=需要右值而数组不能是右值!数组禁止从左值到右值转换,而由左值到指针转换。因为它是用相同的术语定义的,所以在进行右值运算方面并没有什么更好。static_cast
Potatoswatter

2

在此链接中:http : //www2.research.att.com/~bs/bs_faq2.html中 有关于数组分配的部分:

数组的两个基本问题是

  • 数组不知道自己的大小
  • 稍有挑衅,数组的名称将转换为指向其第一个元素的指针

而且我认为这是数组和结构之间的根本区别。数组变量是具有有限自我知识的低级数据元素。从根本上讲,它是一块内存,并且是对其进行索引的一种方式。

因此,编译器无法判断int a [10]和int b [20]之间的区别。

但是,结构没有相同的歧义。


3
该页面讨论将数组传递给函数(这无法完成,因此只是一个指针,这就是他说失去大小时的意思)。这与将数组分配给数组无关。不,数组变量不仅仅是“真正”指向第一个元素的指针,它还是一个数组。数组不是指针。
GManNickG 2010年

感谢您的评论,但是当我阅读本文的那一部分时,他预先说数组不知道其自身大小,然后使用一个示例,其中将数组作为参数传递来说明这一事实。因此,当将数组作为参数传递时,它们会丢失有关其大小的信息,还是从没有这些信息开始。我以为后者。
Scott Turley 2010年

3
编译器可以分辨出两个不同大小的数组之间的区别-尝试打印sizeof(a)vs.sizeof(b)或传递avoid f(int (&)[20]);
Georg Fritzsche 2010年

重要的是要了解每个数组大小都构成自己的类型。参数传递的规则确保您可以编写可采用任何大小的数组参数的穷人的“泛型”函数,但需要分别传递大小。如果不是这种情况(在C ++中,您可以-并且必须!-将引用参数定义为特定大小的数组),则需要为每个不同的大小使用特定的函数,这显然是胡说八道。我在另一篇文章中写道
彼得-恢复莫妮卡

0

我知道,每个回答的人都是C / C ++专家。但是我认为,这是主要原因。

num2 = num1;

在这里,您尝试更改数组的基址,这是不允许的。

当然,struct2 = struct1;

在这里,对象struct1被分配给另一个对象。


分配结构最终将分配数组成员,这引起了完全相同的问题。在两种情况下都是数组时,为什么允许一个而不允许另一个?
GManNickG 2010年

1
同意 但是第一个被编译器阻止(num2 = num1)。编译器不会阻止第二个。那有很大的不同。
nsivakr 2010年

如果数组是可分配的,num2 = num1则将表现得很好。的元素与num2的对应元素具有相同的值num1
juanchopanza

0

没有进一步努力增强C中的数组的另一个原因可能是数组分配没有那么有用。即使可以通过将其包装在struct中轻松实现(并且可以将struct的地址简单地转换为数组的地址,甚至可以将数组的第一个元素的地址进行进一步处理),但很少使用此功能。原因之一是不同大小的数组不兼容,这限制了赋值或相关的按值传递给函数的好处。

对于数组是第一类类型的语言,大多数带有数组参数的函数都是针对任意大小的数组编写的。然后,该函数通常在给定数量的元素上进行迭代,即数组提供的信息。(在C语言中,习惯用法当然是传递一个指针和一个单独的元素计数。)不需要经常接受仅接收一个特定大小的数组的函数,因此不会遗漏太多。(当您可以将其留给编译器以针对任何出现的数组大小生成单独的函数时(与C ++模板一样,这会发生变化;这std::array是有用的原因。)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.