这里指出_
术语可修改左值用于强调左值允许更改和检查指定对象。以下对象类型是左值,但不是可修改的左值:
- 数组类型
- 不完整的类型
- 一个 const 限定的类型
- 结构或联合类型,其成员之一限定为 const 类型
因为这些左值是不可修改的,所以它们不能出现在赋值语句的左侧。
为什么数组类型对象不可修改?写的不对吗
int i = 5, a[10] = {0};
a[i] = 1;
?
还有,什么是不完全类型?
假设声明
int a[10];
那么以下所有情况都是正确的:
a
是“10 元素数组int
”;除非a
是sizeof
或一元运算符的操作数,否则&
表达式将被转换为“指向int
”类型的表达式,其值将是数组中第一个元素的地址;a[i]
是int
; 它指的是作为i
数组的第 ' 个元素存储的整数对象;a
目标,因为 C 不会将数组视为其他变量,因此您不能编写类似a = b
或a = malloc(n * sizeof *a)
类似的东西。你会注意到我一直在强调“表达”这个词。我们留出用于保存 10 个整数的内存块与我们用来指代该内存块的符号(表达式)之间存在差异。我们可以用表达式来引用它a
。我们还可以创建指向该数组的指针:
int (*ptr)[10] = &a;
该表达式*ptr
还具有“10 元素数组int
”类型,它指的是所指的同一块内存a
。
C 不会将数组表达式 ( a
, *ptr
) 视为其他类型的表达式,其中一个区别是数组类型的表达式可能不是赋值的目标。您不能重新分配a
以引用不同的数组对象(表达式相同*ptr
)。您可以a[i]
为or分配一个新值(*ptr)[i]
(更改每个数组元素的值),并且您可以分配ptr
以指向不同的数组:
int b[10], c[10];
.....
ptr = &b;
.....
ptr = &c;
至于第二个问题……
不完整的类型缺少大小信息;像这样的声明
struct foo;
int bar[];
union bletch;
all 创建不完整的类型,因为没有足够的信息让编译器确定为该类型的对象留出多少存储空间。您不能创建不完整类型的对象;例如,您不能声明
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;
};
left
因为当我们声明and成员时类型不完整right
,而且每个left
andright
成员都将包含它们自己的left
and成员,每个成员都将包含它们自己的and成员,等等。right
left
right
这对结构体和联合体来说很好,但是呢?
int bar[];
???
我们已经声明了符号bar
并指出它将是一个数组类型,但此时大小是未知的。 最终我们必须用一个大小来定义它,但是这样这个符号可以在数组大小没有意义或没有必要的情况下使用。不过,没有一个好的、非人为的例子来说明这一点。
编辑
回复这里的评论,因为评论部分没有空间来写我想写的东西(我今晚心情很冗长)。您询问:
这是否意味着每个变量都是表达式?
这意味着任何变量都可以是表达式,也可以是表达式的一部分。以下是语言标准如何定义术语表达式:
6.5 表达式
1表达式是一系列运算符和操作数,用于指定值的计算,或指定对象或函数,或产生副作用,或执行它们的组合。
例如,变量a
all 本身算作一个表达式;它指定我们定义的包含 10 个整数值的数组对象。它还计算数组第一个元素的地址。该变量a
也可以是较大表达式的一部分,例如a[i]
; 运算符是下标运算符[]
,操作数是变量a
和i
。此表达式指定数组的单个成员,并计算出当前存储在该成员中的值。该表达式又可以是更大表达式的一部分,例如a[i] = 0
.
还要让我清楚一点,在声明 int a[10] 中,a[] 是否代表数组类型
对,就是这样。
在 C 中,声明基于表达式的类型,而不是对象的类型。如果您有一个名为y
存储int
值的简单变量,并且您想要访问该值,您只需y
在表达式中使用,例如
x = y;
表达式 的类型y
是int
,所以y
写成的声明
int y;
另一方面,如果您有一个值数组,int
并且想要访问特定元素,则可以使用数组名称和索引以及下标运算符来访问该值,例如
x = a[i];
表达式 的类型a[i]
是int
,所以数组的声明写成
int arr[N]; // for some value N.
的“ int
-ness”arr
由类型说明符给出int
;的“数组性”arr
由 declarator 给出arr[N]
。声明器为我们提供了被声明对象的名称 ( arr
) 以及类型说明符未提供的一些附加类型信息(“是一个 N 元素数组”)。声明“读”为
a -- a
a[N] -- is an N-element array
int a[N]; -- of int
编辑2
毕竟,我还没有告诉你数组表达式是不可修改的左值的真正原因。因此,这本书的答案又是一章。
C 并不是从丹尼斯·里奇的头脑中完全形成的;它源自一种称为 B 的早期语言(源自 BCPL)。1 B 是一种“无类型”的语言;对于整数、浮点数、文本、记录等,它没有不同的类型。相反,一切都只是一个固定长度的单词或“单元格”(本质上是一个无符号整数)。记忆被视为细胞的线性阵列。当您在 B 中分配一个数组时,例如
auto V[10];
编译器分配了 11 个单元格;数组本身的 10 个连续单元格,加上一个绑定到 V 的单元格,其中包含第一个单元格的位置:
+----+
V: | | -----+
+----+ |
... |
+----+ |
| | <----+
+----+
| |
+----+
| |
+----+
| |
+----+
...
当 Ritchie 在struct
C 中添加类型时,他意识到这种安排给他带来了一些问题。例如,他想创建一个结构类型来表示文件或目录表中的条目:
struct {
int inumber;
char name[14];
};
他希望该结构不仅以抽象的方式描述条目,而且还表示实际文件表条目中的位,它没有额外的单元格或单词来存储数组中第一个元素的位置。所以他摆脱了它——他没有留出一个单独的位置来存储第一个元素的地址,而是编写了 C 语言,以便在计算数组表达式时计算第一个元素的地址。
这就是为什么你不能做类似的事情
int a[N], b[N];
a = b;
因为在该上下文中,两者a
都b
评估为指针值;相当于写作3 = 4
。内存中没有任何东西实际存储数组中第一个元素的地址;编译器只是在翻译阶段计算它。
术语“数组类型的左值”字面意思是将数组对象称为数组类型的左值,即作为一个整体的数组对象。这个左值作为一个整体是不可修改的,因为没有合法的操作可以整体修改它。事实上,您可以对数组类型的左值执行的唯一操作是:一元&
(地址)sizeof
和隐式转换为指针类型。这些操作都不会修改数组,这就是数组对象不可修改的原因。
a[i]
不适用于数组类型的左值。a[i]
指定一个int
对象:数组的第 i 个元素a
。此表达式的语义(如果明确拼写)是:*((int *) a + i)
. 第一步 -(int *) a
已经将数组类型的左值转换为类型的右值int *
。在这一点上,数组类型的左值已经不存在了。
不完整类型是 [尚未] 知道大小的类型。例如:已声明但未定义的struct类型,未指定大小的数组类型,void
类型。
不完整类型是已声明但未定义的类型,例如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};
假设a
是一个整数数组,a[10]
不是一个数组。它是一个int
.
a = {0}
将是非法的。
请记住,数组的值实际上是其第一个元素的地址(指针)。此地址无法修改。所以
int a[10], b[10];
a = b
是非法的。
它当然与修改数组的内容无关,如a[1] = 3