提问人:Dinah 提问时间:12/20/2008 最后编辑:CœurDinah 更新时间:12/29/2022 访问量:128698
对于数组,为什么会是 a[5] == 5[a]?
With arrays, why is it the case that a[5] == 5[a]?
问:
正如 Joel 在 Stack Overflow 播客 #34 中指出的那样,在 C 编程语言(又名:K & R)中,提到了 C 语言中数组的这个属性:a[5] == 5[a]
乔尔说这是因为指针算术,但我还是不明白。为什么 a[5] == 5[a]
?
答:
C 标准对运算符的定义如下:[]
a[b] == *(a + b)
因此,将评估为:a[5]
*(a + 5)
并将评估为:5[a]
*(5 + a)
a
是指向数组的第一个元素的指针。 是离 相距 5 个元素的值,这与 相同,从小学数学中我们知道它们是相等的(加法是可交换的)。a[5]
a
*(a + 5)
评论
a[5]
mov eax, [ebx+20]
[ebx+5]
*(10 + (int *)13) != *((int *)10 + 13)
(1 apple + 2 oranges) = (2 oranges + 1 apple)
(1 apple + 2 oranges) != (1 orange + 2 apples)
因为数组访问是根据指针定义的。 定义为 ,它是可交换的。a[i]
*(a + i)
评论
*(i + a)
i[a]
*(a + i)
*(a + i) = *(i + a) = i[a]
+
当然,还有
("ABCD"[2] == 2["ABCD"]) && (2["ABCD"] == 'C') && ("ABCD"[2] == 'C')
造成这种情况的主要原因是,早在 70 年代设计 C 语言时,计算机就没有太多内存(64KB 是很多),因此 C 编译器没有做太多的语法检查。因此,“”被相当盲目地翻译成”X[Y]
*(X+Y)
"
这也解释了 “” 和 “” 语法。形式 “” 中的所有内容都具有相同的编译形式。但是,如果 B 与 A 是同一对象,则可以进行程序集级优化。但是编译器不够亮,无法识别它,因此开发人员必须 ()。同样,如果 是 ,则可以进行不同的汇编级优化,并且开发人员必须再次明确它,因为编译器无法识别它。(最近编译器这样做了,所以这些语法现在基本上是不必要的)+=
++
A = B + C
A += C
C
1
评论
LHS =- RHS;
-=
不错的问题/答案。
只是想指出 C 指针和数组是不一样的,尽管在这种情况下区别不是本质的。
请考虑以下声明:
int a[10];
int* p = a;
在 中,符号位于数组开头的地址,符号位于存储指针的地址,该内存位置的指针值是数组的开头。a.out
a
p
评论
int a[10]
int*p = a;
int b = 5;
关于黛娜的问题,似乎没有人提到过一件事:sizeof
您只能向指针添加一个整数,不能将两个指针相加。这样,在向整数添加指针或向指针添加整数时,编译器始终知道哪个位的大小需要考虑。
评论
不是答案,而只是一些值得深思的东西。
如果类具有重载的索引/下标运算符,则表达式将不起作用:0[x]
class Sub
{
public:
int operator [](size_t nIndex)
{
return 0;
}
};
int main()
{
Sub s;
s[0];
0[s]; // ERROR
}
由于我们无法访问 int 类,因此无法执行此操作:
class int
{
int operator[](const Sub&);
};
评论
class Sub { public: int operator[](size_t nIndex) const { return 0; } friend int operator[](size_t nIndex, const Sub& This) { return 0; } };
operator[]
operator=
[]
[]
a[b]
*(a + b)
int::operator[](const Sub&);
int
从字面上回答问题。并非总是如此x == x
double zero = 0.0;
double a[] = { 0,0,0,0,0, zero/zero}; // NaN
cout << (a[5] == 5[a] ? "true" : "false") << endl;
指纹
false
评论
cout << (a[5] == a[5] ? "true" : "false") << endl;
false
x == x
NAN
<math.h>
0.0/0.0
0.0/0.0
__STDC_IEC_559__
__STDC_IEC_559__
0.0/0.0
对于 C 语言中的指针,我们有
a[5] == *(a + 5)
以及
5[a] == *(5 + a)
因此,确实如此a[5] == 5[a].
我只是发现这种丑陋的语法可能是“有用的”,或者至少当你想处理一个将位置引用到同一数组中的索引数组时,玩起来非常有趣。它可以替换嵌套的方括号,使代码更具可读性!
int a[] = { 2 , 3 , 3 , 2 , 4 };
int s = sizeof a / sizeof *a; // s == 5
for(int i = 0 ; i < s ; ++i) {
cout << a[a[a[i]]] << endl;
// ... is equivalent to ...
cout << i[a][a][a] << endl; // but I prefer this one, it's easier to increase the level of indirection (without loop)
}
当然,我很确定在实际代码中没有用例,但无论如何我都觉得它很有趣:)
评论
i[a][a][a]
a
a[a[a[i]]]
i
我认为其他答案遗漏了一些东西。
是的,根据定义,它等价于 ,它(因为加法是可交换的)等价于 ,它(同样,根据运算符的定义)等价于 。p[i]
*(p+i)
*(i+p)
[]
i[p]
(在 中,数组名称被隐式转换为指向数组第一个元素的指针。array[i]
但在这种情况下,加法的可交换性并不那么明显。
当两个操作数属于同一类型,甚至属于不同的数值类型,这些类型被提升为公共类型时,可交换性就非常有意义了: .x + y == y + x
但在本例中,我们专门讨论指针算术,其中一个操作数是指针,另一个是整数。(整数+整数是不同的操作,指针+指针是无稽之谈。
C 标准对运算符的描述 (N1570 6.5.6) 说:+
此外,两个操作数都应具有算术类型,或者一个 操作数应为指向完整对象类型的指针,另一个 应具有整数类型。
它可以很容易地说:
此外,两个操作数都应具有算术类型,或者左操作数应为指向完整对象类型的指针,而右操作数应为整数类型。
在这种情况下,两者都是非法的。i + p
i[p]
用 C++ 术语来说,我们实际上有两组重载运算符,可以粗略地描述为:+
pointer operator+(pointer p, integer i);
和
pointer operator+(integer i, pointer p);
其中只有第一个是真正必要的。
那么为什么会这样呢?
C++从C继承了这个定义,C从B得到它(数组索引的交换性在1972年的用户参考B中明确提到),它从BCPL(1967年的手册)中得到它,它很可能从更早的语言(CPL?Algol?
因此,数组索引是根据加法来定义的,而加法,即使是指针和整数,也是可交换的,这种想法可以追溯到几十年前,可以追溯到 C 的祖先语言。
这些语言的类型远不如现代 C 语言那么强。特别是,指针和整数之间的区别经常被忽略。(早期的 C 程序员有时会将指针用作无符号整数,然后再将关键字添加到语言中。因此,由于操作数是不同类型的,因此使加法成为非交换性的想法可能不会出现在这些语言的设计者身上。如果用户想要添加两个“事物”,无论这些“事物”是整数、指针还是其他东西,都不能由语言来阻止它。unsigned
多年来,对该规则的任何更改都会破坏现有代码(尽管 1989 年的 ANSI C 标准可能是一个很好的机会)。
将 C 和/或 C++ 更改为需要将指针放在左侧,将整数放在右侧可能会破坏一些现有代码,但不会损失真正的表达能力。
因此,现在我们拥有和含义完全相同的东西,尽管后一种形式不应该出现在 IOCCC 之外。arr[3]
3[arr]
评论
3[arr]
int
的大小无关。ring16_t
ring16_t
在 Ted Jensen 的 A TUTORIAL ON POINTERS AND ARRAYS IN C 中对此有很好的解释。
泰德·詹森(Ted Jensen)将其解释为:
事实上,这是真的,即无论一个人在哪里写它都可以 替换为没有任何问题。事实上,编译器 无论哪种情况,都将创建相同的代码。因此,我们看到该指针 算术与数组索引是一回事。任一语法都会产生 相同的结果。
a[i]
*(a + i)
这并不是说指针和数组 是一回事,他们不是。我们只是说要识别 数组的给定元素,我们可以选择两种语法,一种 使用数组索引,另一个使用指针算术,其中 产生相同的结果。
现在,看看最后这个 表达,其中的一部分。,是使用 + 的简单加法 运算符和 C 的规则声明这样的表达式是 交换。也就是说,(a + i) 等同于 。因此,我们可以 写起来就像 . 但本来可以来的!从这一切中产生了好奇心 如果:
(a + i)
(i + a)
*(i + a)
*(a + i)
*(i + a)
i[a]
char a[20];
写作
a[3] = 'x';
和写作是一样的
3[a] = 'x';
评论
在 C 数组中,和是相同的,并且它们的等效指针表示法是 。但相反,或者是不正确的,并且会导致语法错误,因为和都是无效的表达式。原因是取消引用运算符应放在表达式生成的地址之前,而不是地址之后。arr[3]
3[arr]
*(arr + 3)
*(3 + arr)
[arr]3
[3]arr
(arr + 3)*
(3 + arr)*
在 C 编译器中
a[i]
i[a]
*(a+i)
是引用数组中元素的不同方式!(一点也不奇怪)
我知道这个问题已经得到回答,但我忍不住分享这个解释。
我记得编译器设计的原则,
假设是一个数组,大小为 2 个字节,
的基址是 1000。a
int
int
a
将如何工作 ->a[5]
Base Address of your Array a + (5*size of(data type for array a))
i.e. 1000 + (5*2) = 1010
所以
同样,当 c 代码分解为 3 地址代码时,将变为 ->5[a]
Base Address of your Array a + (size of(data type for array a)*5)
i.e. 1000 + (2*5) = 1010
因此,基本上这两个语句都指向内存中的同一位置,因此,.a[5] = 5[a]
这种解释也是数组中的负索引在 C 中起作用的原因。
即如果我访问它会给我a[-5]
Base Address of your Array a + (-5 * size of(data type for array a))
i.e. 1000 + (-5*2) = 990
它将在位置 990 返回我的对象。
在 C 语言中
int a[]={10,20,30,40,50};
int *p=a;
printf("%d\n",*p++);//output will be 10
printf("%d\n",*a++);//will give an error
指针是“变量”,数组名是“助记词”或“同义词”,
所以是有效的,但无效。p
a
p++
a++
a[2]
等于,因为对这两者的内部运算都是“指针算术”,内部计算为等于2[a]
*(a+2)
*(2+a)
嗯,这是一个只有语言支持才能实现的功能。
编译器解释为 ,表达式的计算结果为 。由于加法是可交换的,因此事实证明两者都是相等的。因此,表达式的计算结果为 。a[i]
*(a+i)
5[a]
*(5+a)
true
评论
现在有一点历史。在其他语言中,BCPL 对 C 的早期发展产生了相当大的影响。如果您在 BCPL 中声明了一个数组,如下所示:
let V = vec 10
这实际上分配了 11 个单词的记忆,而不是 10 个。通常,V 是第一个,包含紧随其后的单词的地址。因此,与 C 不同的是,命名 V 会转到该位置并获取数组中 zeroeth 元素的地址。因此,BCPL 中的数组间接,表示为
let J = V!5
确实必须这样做(使用 BCPL 语法),因为必须获取 V 才能获取数组的基址。因此,和是同义词。作为一个轶事观察,WAFL(Warwick Functional Language)是用 BCPL 编写的,据我所知,它倾向于使用后一种语法而不是前一种语法来访问用作数据存储的节点。当然,这是 35 到 40 年前的某个地方,所以我的记忆有点生疏。:)J = !(V + 5)
V!5
5!V
省去额外的存储字,让编译器在命名数组时插入数组的基址的创新是后来才出现的。根据 C 历史论文,这发生在向 C 添加结构时。
请注意,在 BCPL 中,既是一元前缀运算符,也是二进制中缀运算符,在这两种情况下都是间接的。只是在进行间接操作之前,二进制形式包括了两个操作数的相加。鉴于 BCPL(和 B)的面向文字性质,这实际上很有意义。“指针和整数”的限制在C语言中获得了数据类型,并成为一种东西。!
sizeof
因为 C 编译器总是以指针表示法转换数组表示法。 也所以,两者都是平等的。a[5] = *(a + 5)
5[a] = *(5 + a) = *(a + 5)
因为它有助于避免混淆嵌套。
你愿意阅读这个吗:
array[array[head].next].prev
或者这个:
head[array].next[array].prev
顺便说一下,C++ 对函数调用具有类似的交换属性。与其在 C 中编写,不如使用成员函数编写 .将 f 和 g 替换为查找表,您可以编写 (函数式) 或 (oop 式)。后者在包含索引的结构中变得非常好: .使用更常见的表示法,即 .g(f(x))
x.f().g()
g[f[x]]
(x[f])[g]
x[xs].y[ys].z[zs]
zs[ys[xs[x].y].z]
评论
C 基于 BCPL。BCPL 直接将内存公开为一系列可寻址字。一元运算符(也称为 LV)为您提供了地址位置 X 的内容。为方便起见,还有一个二进制运算符等价物,它为您提供了位置 X 处数组的第 Y 个单词的内容,或者等效地提供了位置 Y 处数组的第 X 个单词的内容。!X
X!Y
!(X+Y)
在 C 语言中,变成了 ,但原始的 BCPL 语义是 show through,这解释了为什么算子是可交换的。X!Y
X[Y]
!(X+Y)
上一个:C语言中的“:-!!”是什么?
下一个:< 比 <= 快吗?
评论
a[1]
char bar[]; int foo[];
foo[i][bar]
a[b]
*(a + b)
a
b
+
i + p
p + i
+