题 对于数组,为什么a [5] == 5 [a]?


正如乔尔指出的那样 Stack Overflow播客#34,在 C编程语言 (又名:K&R),在C中提到了数组的这个属性: a[5] == 5[a]

乔尔说,这是因为指针运算,但我仍然不明白。 为什么 a[5] == 5[a]


1429
2017-12-19 17:01


起源


像[+]这样的东西也像*(a ++)OR *(++ a)一样工作吗? - Egon
@Egon:这非常有创意,但不幸的是编译器的工作方式并非如此。编译器解释 a[1] 作为一系列标记,而不是字符串:*({运算符}的整数位置+ {整数} 1)与*({integer} 1 {运算符} + {整数位置} a)相同但不是与*({整数位置}} {运算符} + {运算符} +)相同 - Dinah
关于此的一个有趣的复合变体如图所示 不合逻辑的阵列访问,你在哪里 char bar[]; int foo[]; 和 foo[i][bar] 用作表达式。 - Jonathan Leffler
@EldritchConundrum,为什么你认为'编译器无法检查左边的部分是否为指针'?是的,它可以。这是真的 a[b] = *(a + b) 任何给定的 a 和 b,但这是语言设计师的自由选择 + 为所有类型定义可交换的。没有什么能阻止他们禁止 i + p 同时允许 p + i。 - ach
@Andrey One通常期待 + 是可交换的,所以也许真正的问题是选择使指针操作类似于算术,而不是设计一个单独的偏移运算符。 - Eldritch Conundrum


答案:


C标准定义了 [] 运营商如下:

a[b] == *(a + b)

因此 a[5] 将评估为:

*(a + 5)

5[a] 将评估为:

*(5 + a)

a 是指向数组的第一个元素的指针。 a[5] 是5的值 分子 进一步 a,这是一样的 *(a + 5)从小学数学我们知道那些是平等的(另外是 可交换)。


1708
2017-12-19 17:04



我想知道它是不是更像*((5 * sizeof(a))+ a)。但是很好的解释。 - John MacIntyre
@Dinah:从C编译器的角度来看,你是对的。不需要sizeof,我提到的那些表达是相同的。但是,编译器在生成机器代码时会考虑sizeof。如果a是一个int数组, a[5] 会编译成类似的东西 mov eax, [ebx+20] 代替 [ebx+5] - Mehrdad Afshari
@Dinah:A是一个地址,比如0x1230。如果a在32位int数组中,则a [0]为0x1230,a [1]为0x1234,a [2]为0x1238 ... a [5]为x1244等。如果我们只添加5 0x1230,我们得到0x1235,这是错误的。 - James Curran
@ sr105:这是+运算符的特例,其中一个操作数是指针而另一个是整数。标准说结果将是指针的类型。编译器/必须足够聪明。 - aib
“从小学数学我们知道那些是平等的” - 我明白你是在简化,但我和那些觉得这样的人在一起 过度简化。这不是基本的 *(10 + (int *)13) != *((int *)10 + 13)。换句话说,这里比小学算术还要多。可交换性主要依赖于编译器识别哪个操作数是指针(以及对象的大小)。换一种方式, (1 apple + 2 oranges) = (2 oranges + 1 apple)但是 (1 apple + 2 oranges) != (1 orange + 2 apples)。 - LarsH


因为数组访问是根据指针定义的。 a[i] 定义为 *(a + i),这是可交换的。


273
2017-12-19 17:05



数组不是用指针定义的,而是 访问 他们是。 - Lightness Races in Orbit
我会补充“所以它等于 *(i + a),可以写成 i[a]”。 - Jim Balter
我建议你包括标准的引用,如下所示:6.5.2.1:2后缀表达式后跟方括号[]中的表达式是数组对象元素的下标。下标运算符[]的定义是E1 [E2]与(*((E1)+(E2)))相同。由于适用于binary +运算符的转换规则,如果E1是数组对象(等效地,指向数组对象的初始元素的指针)并且E2是整数,则E1 [E2]指定E2的第E2个元素。 E1(从零开始计数)。 - Vality
更正确:当您访问它们时,数组会衰减为指针。 - 12431234123412341234123


我认为其他答案正在忽略某些事情。

是, 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继承了这个定义,它从B得到它(数组索引的可交换性在1972年明确提到) 用户对B的参考从它得到它 BCPL (1967年的手册),很可能是从早期的语言中获得的(CPL?Algol?)。

因此,数组索引是根据加法来定义的,并且即使是指针和整数,这种加法也是可交换的,可以追溯到C的祖先语言几十年。

这些语言的类型远不如现代C语言。特别是,指针和整数之间的区别经常被忽略。 (早期的C程序员有时会使用指针作为无符号整数 unsigned 关键词被添加到语言中。)因此,由于操作数是不同类型的,因此添加非交换的想法可能不会发生在那些语言的设计者身上。如果用户想要添加两个“东西”,无论这些“东西”是整数,指针还是其他东西,都不能用语言来阻止它。

多年来,对该规则的任何更改都会破坏现有代码(尽管1989 ANSI C标准可能是一个很好的机会)。

改变C和/或C ++要求将指针放在左边,而整数放在右边可能会破坏一些现有的代码,但不会损失真正的表达能力。

所以现在我们有了 arr[3] 和 3[arr] 意思完全相同的东西,虽然后一种形式永远不会出现在外面 IOCCC


186
2017-08-23 01:37



这个属性的奇妙描述。从高层面来看,我认为 3[arr] 是一个有趣的神器,但应该很少使用。这个问题的接受答案(<stackoverflow.com/q/1390365/356>我问过一段时间后,我改变了我对语法的看法。虽然技术上通常没有正确和错误的方法来做这些事情,但这些类型的功能开始以一种与实现细节分开的方式进行思考。这种不同的思维方式有好处,当你注意到实现细节时,这种思维方式部分失去了。 - Dinah
增加是可交换的。对于C标准来定义它否则会很奇怪。这就是为什么它不能轻易说“另外,要么两个操作数都有算术类型,要么左操作数应该是指向完整对象类型的指针,右操作数应该是整数类型。” - 这对大多数添加东西​​的人来说没有意义。 - iheanyi
@iheanyi:加法通常是可交换的 - 它通常需要两个相同类型的操作数。指针添加允许您添加指针和整数,但不能添加两个指针。恕我直言,已经是一个非常奇怪的特殊情况,要求指针是左操作数不会是一个重大的负担。 (有些语言使用“+”进行字符串连接;这肯定不是可交换的。) - Keith Thompson
@supercat,情况更糟。这意味着有时x + 1!= 1 + x。这将完全违反添加的关联属性。 - iheanyi
@iheanyi:我认为你的意思是交换财产;加法已经不是关联的,因为在大多数实现中(1LL + 1U)-2!= 1LL +(1U-2)。实际上,这种改变会使一些情况联想起来,而这种情况目前并非如此。 3U +(UINT_MAX-2L)等于(3U + UINT_MAX)-2。然而,最好的是,语言为可升级整数添加新的不同类型并“包裹”代数环,以便将2添加到a ring16_t 持有65535将产生一个 ring16_t 值为1, 独立于大小 int。 - supercat


而且当然

 ("ABCD"[2] == 2["ABCD"]) && (2["ABCD"] == 'C') && ("ABCD"[2] == 'C')

主要原因是在70年代设计C时,计算机没有太多内存(64KB很多),所以C编译器没有做太多的语法检查。因此“X[Y]“被盲目翻译成”*(X+Y)

这也解释了“+=“和”++“语法。形式中的一切”A = B + C“有相同的编译形式。但是,如果B是与A相同的对象,那么可以使用汇编级优化。但是编译器不够亮,无法识别它,因此开发人员必须(A += C)。同样,如果 C 是 1,可以使用不同的程序集级别优化,并且开发人员必须再次明确,因为编译器无法识别它。 (最近的编译器会这样做,所以这些天的语法很大程度上是不必要的)


184
2017-12-19 17:07



实际上,评估为假;第一个术语“ABCD”[2] == 2 [“ABCD”]的计算结果为真,或1和1!='C':D - Jonathan Leffler
@Jonathan:同样含糊不清导致编辑这篇文章的原始标题。我们是等数标记数学等价,代码语法还是伪代码。我认为数学等价,但由于我们谈论代码,我们无法逃避我们在代码语法方面查看所有内容。 - Dinah
这不是一个神话吗?我的意思是创建+ =和++运算符以简化编译器?有些代码随着它们变得更加清晰,无论编译器使用什么代码,它都是有用的语法。 - Thomas Padron-McCarthy
+ =和++有另一个显着的好处。如果左侧在评估时更改了某些变量,则更改只会执行一次。 a = a + ...;会做两次。 - Johannes Schaub - litb
否 - “ABCD”[2] == *(“ABCD”+ 2)= *(“CD”)='C'。取消引用字符串会为您提供char,而不是子字符串 - MSalters


似乎没有人提到有关黛娜问题的一件事 sizeof

您只能向指针添加整数,不能将两个指针一起添加。这样,当向整数添加指针或向指针添加整数时,编译器总是知道哪个位具有需要考虑的大小。


51
2018-02-11 15:56



在接受的答案的评论中,有一个相当详尽的对话。我在原始问题的编辑中引用了所说的对话,但没有直接解决你对sizeof的非常有效的关注。不知道如何在SO中做到最好。我应该对orig进行另一次编辑吗?题? - Dinah


从字面上回答这个问题。并非总是如此 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

46
2017-08-11 13:50



实际上,“nan”并不等于它自己: cout << (a[5] == a[5] ? "true" : "false") << endl; 是 false。 - TrueY
@TrueY:他确实陈述了专门针对NaN案件(特别是那个案例) x == x 并非总是如此)。我认为那是他的意图。所以他是 技术上 正确(并且可能,正如他们所说,最好的正确!)。 - Tim Čas
问题是关于C,你的代码不是C代码。还有一个 NAN 在 <math.h>,这比 0.0/0.0因为 0.0/0.0 是UB的时候 __STDC_IEC_559__ 没有定义(大多数实现没有定义 __STDC_IEC_559__,但在大多数实现 0.0/0.0 仍然会工作) - 12431234123412341234123


不错的问题/答案。

只是想指出C指针和数组不是 相同虽然在这种情况下,差异并不重要。

请考虑以下声明:

int a[10];
int* p = a;

的a.out,符号 一个 在一个地址,这是数组的开头和符号 p 在存储指针的地址处,该存储器位置处的指针值是数组的开头。


23
2017-12-20 08:16



不,从技术上讲它们不一样。如果将某个b定义为int * const并使其指向数组,则它仍然是指针,这意味着在符号表中,b指的是存储地址的内存位置,而该地址又指向数组所在的位置。 - PolyThinker
很好的一点。当我在一个模块中将全局符号定义为char s [100]时,我记得有一个非常讨厌的错误,将其声明为extern char * s;在另一个模块中。将它们连接在一起之后,程序表现得非常奇怪。因为使用extern声明的模块使用数组的初始字节作为char的指针。 - Giorgio
最初,在C的祖父母BCPL中,数组是一个指针。那就是你写的时候得到的(我已经音译到C) int a[10] 是一个名为'a'的指针,指向其他地方足够存储10个整数。因此,+ i和j + i具有相同的形式:添加几个存储位置的内容。事实上,我认为BCPL是无类型的,所以它们是相同的。并且sizeof-type缩放不适用,因为BCPL纯粹是面向字的(在字寻址机器上也是如此)。 - dave
我认为理解差异的最好方法是比较 int*p = a; 至 int b = 5;  在后者中,“b”和“5”都是整数,但“b”是变量,而“5”是固定值。类似地,“p”和“a”都是字符的地址,但“a”是固定值。 - James Curran


我只是发现这个丑陋的语法可能是“有用的”,或者至少非常有趣,当你想要处理引用同一数组中的位置的索引数组时。它可以替换嵌套的方括号,使代码更具可读性!

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)

}

当然,我很确定实际代码中没有用例,但无论如何我发现它很有趣:)


21
2018-06-10 19:50



天啊!!!有人怎么说喜欢这种符号!!!它伤害了我的眼睛! - Luis Colorado
当你看到 i[a][a][a] 你认为我要么是指向数组的指针,要么是指向数组或数组的指针数组...和 a 是一个索引。当你看到 a[a[a[i]]],你认为a是指向数组或数组的指针 i 是一个索引。 - 12431234123412341234123
哇!这个“愚蠢”的功能非常酷。在一些问题的算法竞赛中可能有用))) - Serge Breusov


对于C中的指针,我们有

a[5] == *(a + 5)

并且

5[a] == *(5 + a)

因此,确实如此 a[5] == 5[a].


17
2018-03-23 07:05