为什么数组类型对象不可修改?

15

这里所述:

术语可修改的左值(modifiable lvalue)用于强调左值既可以被更改也可以被检查。下列对象类型是左值但不是可修改的左值:

  • 数组类型
  • 不完整类型
  • 带有一个成员限定为const类型的结构体或联合体类型

由于这些左值不可修改,它们不能出现在赋值语句的左侧。

为什么数组类型对象不可修改?难道写下面这样的语句不正确吗?

int i = 5, a[10] = {0};    
a[i] = 1;

什么是不完整类型?


我认为更改类型意味着从 int a[10]double a[10] - Ran Eldan
3
a = valuea[i] = value是有区别的。 - user142162
5个回答

25
假设声明如下:
int a[10];

如果以下所有条件都成立:

  • 表达式 a 的类型为 "10个元素的int数组";除非asizeof或一元&运算符的操作数,否则该表达式将被转换为类型为 "指向int的指针" 的表达式,并且其值将是数组中第一个元素的地址;
  • 表达式 a[i] 的类型为int;它指的是存储在数组中第i个元素中的整数对象;
  • 表达式 a 不能作为赋值的目标,因为C语言不像其他变量那样处理数组,所以您不能编写诸如a = ba = malloc(n * sizeof *a)之类的代码。

你会注意到我一直强调“表达式”这个词。我们设置了一块内存来保存10个整数,与我们用于引用该内存块的符号(表达式)之间存在差异。我们可以使用表达式a来引用它。我们也可以创建指向该数组的指针:

int (*ptr)[10] = &a;

表达式*ptr的类型也是“10个元素的int数组”,它指向与a相同的内存块。
C语言不像其他类型的表达式那样处理数组表达式(a, *ptr),其中一个区别是,数组类型的表达式不能成为赋值的目标。您不能重新将a分配给另一个数组对象(对于表达式*ptr也是如此)。您可以将新值赋给a[i](*ptr)[i](更改每个数组元素的值),并且可以将ptr分配给指向不同数组的指针。
int b[10], c[10];
.....
ptr = &b;
.....
ptr = &c;

关于第二个问题...

一个不完整的类型缺乏大小信息;例如下面的声明

struct foo;
int bar[];
union bletch;

所有创建的不完整类型,因为编译器无法确定为该类型的对象分配多少存储空间,所以不能创建不完整类型的对象。例如,您不能声明:
struct foo myFoo;

除非你完成了struct foo的定义,否则无法使用它。但是,你可以创建指向不完整类型的指针。例如,你可以声明

struct foo *myFooPtr;

因为指针只存储对象的地址,不需要知道类型的大小,所以可以在没有完成struct foo的定义的情况下定义自引用类型。

struct node {
  T key;  // for any type T
  Q val;  // for any type Q
  struct node *left; 
  struct node *right;
};

只有在遇到结尾的 } 之前,struct node 的类型定义才算是完整的。由于我们可以声明指向不完整类型的指针,所以没问题。但是,我们不能定义以下结构体:

struct node {
  ... // same as above
  struct node left;
  struct node right;
};

因为在声明leftright成员时,类型并不完整,并且每个leftright成员都会包含自己的leftright成员,而这些成员又会包含它们自己的leftright成员,如此循环。

对于结构体和联合体来说这是很好的,但是对于...

int bar[];

我们已经声明了符号bar,并指示它将是一个数组类型,但此时大小未知。最终,我们将不得不定义它的大小,但以这种方式,该符号可以用于数组大小无意义或不必要的情况下。我头脑中没有一个好的、非人为的例子来说明这一点。

编辑

回应这里的评论,因为评论区没有足够的空间让我写出想要写的内容(今晚我很啰嗦)。你问:

这是否意味着每个变量都是表达式?

这意味着任何变量都可以是表达式或表达式的一部分。以下是语言标准如何定义术语表达式

6.5 表达式
1 一个表达式是一系列操作符和操作数,它指定计算值、指定对象或函数、生成副作用或执行这些组合的序列。

例如,单独的变量a被视为一个表达式;它指定了我们定义的用于保存10个整数值的数组对象。它还评估为数组的第一个元素的地址。变量a也可以是一个更大表达式的一部分,如a[i];运算符是下标运算符[],操作数是变量ai。该表达式指定数组的一个成员,并评估为当前存储在该成员中的值。该表达式又可以是一个更大表达式的一部分,如a[i] = 0

并且让我澄清一下,在声明int a[10]中,a[]是否表示数组类型

是的,确切地说。

在C语言中,声明基于表达式的类型,而不是对象的类型。如果你有一个名为y的简单变量,它存储一个int值,并且你想要访问该值,你只需在表达式中使用y,如:

x = y;

y表达式的类型是int,因此声明y的写法如下:

int y;

如果您拥有一个整数值的 数组 ,并且想要访问特定元素,您可以使用数组名称和索引以及下标运算符来访问该值,例如:

x = a[i];

a[i]表达式的类型是int,因此数组的声明如下所示:

int arr[N]; // for some value N.  

"arr"的"int-ness"由类型说明符"int"给出;"arr"的"array-ness"则由声明符"arr[N]"给出。声明符给出被声明对象的名称("arr")以及类型说明符未提供的一些额外类型信息("是一个N元素数组")。这个声明可读作:
    a       -- a
    a[N]    -- is an N-element array
int a[N];   -- of int

EDIT2

经过这么多讲解,我还没有告诉你数组表达式为什么是不可修改的左值的真正原因。所以,这里又是一个关于这个问题的章节。

C语言并非Dennis Ritchie一蹴而就的产物;它是由一种早期语言B演变而来(而B又是由BCPL演变而来)。1 B是一种“无类型”语言;它没有针对整数、浮点数、文本、记录等不同类型的定义。相反,所有东西都只是固定长度的单元或“单元格”(基本上是无符号整数)。内存被视为单元格的线性数组。当你在B中分配一个数组时,例如:

auto V[10];

编译器分配了11个单元;其中10个单元是连续的数组本身,另一个单元绑定到V包含第一个单元的位置。
    +----+
V:  |    | -----+
    +----+      |
     ...        |
    +----+      |
    |    | <----+
    +----+
    |    |
    +----+
    |    |      
    +----+
    |    |
    +----+
     ...

当Ritchie在C语言中添加struct类型时,他意识到这种安排会给他带来一些问题。例如,他想创建一个结构体类型来表示文件或目录表中的条目:
struct {
  int inumber;
  char name[14];
};

他希望结构体不仅以抽象方式描述条目,还要表示实际文件表项中的位,该位没有额外的单元格或字存储第一个元素在数组中的位置。 因此,他放弃了这个做法 - 他没有设置一个单独的位置来存储第一个元素的地址,而是编写了C代码,使得当数组表达式被评估时,第一个元素的地址将会被计算。

这就是为什么你不能像这样做的原因。

int a[N], b[N];
a = b;

因为在这种情况下,`a`和`b`都被解释为指针;这等同于写`3 = 4`。实际上,并没有在内存中存储数组第一个元素的地址;编译器只是在翻译阶段计算它。
1. 这句话摘自C语言的发展论文。

你说过,arr 的“数组性”是由声明符 arr[N] 给出的。由于声明符是正在被声明的对象的名称,可能被操作符如 *[]() 包围,所以 arr 的数组性难道不是由 [] 操作符给出的吗? - haccks
1
@haccks:我之前已经说过了:声明符提供了被声明对象的名称(arr)以及类型说明符未给出的一些附加类型信息(“是一个N元素数组”)。 arr标识符arr[N]声明符 - John Bode
1
@haccks:至于声明是基于表达式类型的,我已经解释过了;arr是一个int数组;当代码中出现表达式arr[i](例如像x = arr[i]这样的语句)时,该表达式具有int类型的值。C语言的设计使得声明的形式与代码中表达式的形式非常相似。因此,当我们声明一个数组时,我们写成T a[N];,因为声明a[N]看起来像代码中的表达式a[i],如x = a[i] - John Bode
我不是很明白你的这一句话:- “但是也要表示实际文件表条目中的位,这些条目没有额外的单元格或字来存储数组中第一个元素的位置。” - haccks
地址运算符不应该用于指向数组的指针吗? int a [10]; int (* ptr) [10] =&a; C 在这里宽容;C++则不是。带有正确类型的版本(&a)在两种语言中都可以正常工作。 - Ben Voigt
显示剩余4条评论

6
“数组类型的lvalue”一词字面上指的是作为数组类型的lvalue的数组对象,即作为一个整体的数组对象。由于没有合法的操作可以将其作为整体修改,因此这个lvalue不能作为整体被修改。实际上,您可以对数组类型的lvalue执行的唯一操作是:一元运算符“&”(地址)、“sizeof”和隐式转换为指针类型。这些操作都不会修改数组,这就是为什么数组对象不可修改的原因。
“a[i]”不能与数组类型的lvalue一起使用。“a[i]”指定了一个“int”对象:数组“a”的第i个元素。如果明确地拼写出这个表达式的语义,则为:“*((int*)a+i)”。“(int*)a”这一步已经将数组类型的lvalue转换为了类型为“int*”的rvalue。此时,数组类型的lvalue已经从图中消失了。
“不完全类型”是一种大小尚未[确定]的类型。例如:已声明但未定义的结构类型、未指定大小的数组类型、void类型。

我对这行代码感到困惑:*a[i] does not work with lvalue of array type. a[i] modifies an int object.* - haccks
我想在这里澄清一件事........声明 int a[10] 是什么意思? - haccks

4

不完全类型是指已声明但未定义的类型,例如struct Foo;

您始终可以分配给单个数组元素(假设它们不是const)。 但是您无法将某个东西分配给整个数组。

C和C ++非常令人困惑,因为像int a [10] = {0、1、2、3};这样的内容不是赋值而是初始化,即使它看起来非常像赋值。

这是可以的(初始化):

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

这在C/C++中不起作用:

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

int i = 1; 也不是赋值语句吗? - haccks
1
不,这是初始化。例如,const int i = 1;也是有效的,尽管您无法更改(分配)i的值。 - Johannes Overmann
在声明 int a[10] 中,哪一个是对象,是 a 还是 a[10] - haccks
a是正在初始化的对象。C语法似乎很直观,但仍然令人困惑。a[10]作为声明或定义与a[10]作为表达式(数组指示器和大小与数组查找)非常不同。 - Johannes Overmann

2
假设a是一个整数数组,a[10]不是数组,它是一个int类型的变量。 a = {0} 是非法的。

那么 a = 1 呢? - haccks

1

请记住,数组的值实际上是其第一个元素的地址(指针)。这个地址是无法修改的。因此,

int a[10], b[10];
a = b

是非法的。

当然,这与修改数组的内容无关,如 a[1] = 3


1
这与指针的隐式转换无关(如果你所说的“数组的值是一个地址”指的是这个) - Cubbi

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