对于数组,为什么会出现a [5] == 5 [a]?


1621

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

乔尔(Joel)说,这是因为指针运算,但是我还是不明白。为什么a[5] == 5[a]呢?


48
a [+]之类的东西也可以像*(a ++)或*(++ a)一样工作吗?
伊贡(Egon)2010年

45
@Egon:这很有创造力,但是不幸的是,这不是编译器的工作方式。编译器将解释a[1]为一系列标记,而不是字符串:*({a {operator} + {integer} 1的整数位置}与*({integer} 1 {operator} + {a的整数位置)相同但与*({a {operator} + {operator} +}的整数位置)不同
Dinah 2010年

11
不合逻辑的数组访问中对此有一个有趣的复合变化,在其中有char bar[]; int foo[];foo[i][bar]用作表达式。
乔纳森·勒夫勒

5
@EldritchConundrum,您为什么认为“编译器无法检查左部分是否为指针”?是的,它可以。这是真的,a[b]= *(a + b)对于任何给定ab,但它是语言的设计者自由选择的+,为所有类型的定义交换。没有什么可以阻止他们i + p在允许的情况下禁止使用p + i
2014年

13
@Andrey一个人通常期望+是可交换的,所以也许真正的问题是选择使指针运算类似于算术运算,而不是设计单独的偏移运算符。
Eldritch难题,2014年

Answers:


1924

C标准将[]运算符定义如下:

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

因此a[5]将评估为:

*(a + 5)

并且5[a]将计算为:

*(5 + a)

a是指向数组第一个元素的指针。a[5]是距,与相同的5个元素的值,从小学数学我们知道它们是相等的(加法是可交换的)。a*(a + 5)


325
我想知道它是否更像*((5 * sizeof(a))+ a)。很好的解释。
约翰·麦金太尔

92
@Dinah:从C编译器的角度来看,您是对的。不需要sizeof,我提到的那些表达式是相同的。但是,在生成机器代码时,编译器将考虑sizeof。如果a是一个int数组,a[5]它将编译为类似mov eax, [ebx+20][ebx+5]
格式,

12
@Dinah:A是一个地址,例如0x1230。如果a在32位int数组中,则a [0]处于0x1230,a [1]处于0x1234,a [2]处于0x1238 ... a [5]处于x1244等。如果我们将5加到0x1230,我们得到0x1235,这是错误的。
James Curran

36
@ sr105:这是+运算符的特例,其中一个操作数是一个指针,另一个是整数。该标准说,结果将是指针的类型。编译器必须足够聪明。
aib

48
“从小学数学开始,我们知道那些是平等的”-我知道您正在简化,但是我与那些认为简化过度的人在一起。那不是基本的*(10 + (int *)13) != *((int *)10 + 13)。换句话说,这里的事情比小学算术还要多。可交换性主要取决于编译器,该编译器识别哪个操作数是指针(以及对象的大小)。换句话说(1 apple + 2 oranges) = (2 oranges + 1 apple),但是(1 apple + 2 oranges) != (1 orange + 2 apples)
LarsH 2010年

288

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


42
数组不是根据指针定义的,但可以访问它们。
Lightness Races in Orbit

5
我要添加“因此等于*(i + a),可以写成i[a]”。
Jim Balter 2013年

4
我建议您在标准中加上以下引号:6.5.2.1:2后缀表达式和后跟方括号[]的表达式是数组对象元素的下标名称。下标运算符[]的定义是E1 [E2]与(*((E1)+(E2)))相同。由于适用于二进制+运算符的转换规则,如果E1是一个数组对象(相当于一个指向数组对象初始元素的指针),而E2是一个整数,则E1 [E2]表示E1的第E2个元素E1(从零开始计数)。
价2015年

更正确地说:访问数​​组时,数组会衰减为指针。
12431234123412341234123 '18年

Nitpick:说“ *(a + i)是可交换的”是没有意义的。但是,*(a + i) = *(i + a) = i[a]因为加法是可交换的。
安德烈亚斯·雷布兰德

231

我认为其他答案缺少一些东西。

是的,p[i]根据定义*(p+i),它等于,(由于加法是可交换的)等于*(i+p),它(同样,由[]运算符的定义)等于i[p]

(在 array[i],数组名隐式转换为指向数组第一个元素的指针。)

但是在这种情况下,加法的可交换性不是很明显。

当两个操作数是同一类型,或者甚至是提升为通用类型的不同数值类型时,可交换性就很有意义: x + y == y + x

但是在这种情况下,我们专门讨论的是指针算法,其中一个操作数是一个指针,另一个是整数。(整数+整数是一个不同的运算,而指针+指针是无意义的。)

C标准的+操作员说明(N1570 6.5.6)说:

另外,两个操作数都应具有算术类型,或者一个操作数应是指向完整对象类型的指针,另一个操作数应具有整数类型。

可以很容易地说:

另外,两个操作数都应具有算术类型,或者 操作数应是指向完整对象类型的指针,而右操作数 应具有整数类型。

在这种情况下,两个i + pi[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语言相比,这些语言的强类型要少得多。特别是,指针和整数之间的区别通常被忽略。(在将unsigned关键字添加到语言之前,早期的C程序员有时将指针用作无符号整数。)因此,由于操作数的类型不同,使这些语言的设计人员可能不会想到使加法运算不可交换。如果用户想添加两个“事物”,无论这些“事物”是整数,指针还是其他事物,都无法阻止该语言。

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

更改C和/或C ++要求将指针放在左侧,将整数放在右侧可能会破坏某些现有代码,但不会损失实际的表达能力。

因此,现在我们拥有arr[3]3[arr]意义完全相同的东西,尽管后者永远不应出现在IOCCC之外。


12
此属性的绝佳描述。从较高的角度来看,我认为这3[arr]是一个有趣的工件,但很少使用。不久前我问过这个问题的可接受答案(< stackoverflow.com/q/1390365/356>)改变了我对语法的思考方式。尽管在技术上通常没有对与错的方式来做这些事情,但是这些功能使您开始以与实现细节分开的方式进行思考。这种不同的思维方式有好处,当您固定实现细节时,这种思维方式会部分丢失。
迪纳

3
加法是可交换的。对于C标准进行定义,否则会很奇怪。这就是为什么它不能这么简单地说“另外,两个操作数都应为算术类型,或者左操作数应为指向完整对象类型的指针,而右操作数应为整数类型。” -对于大多数添加东西​​的人来说,这是没有意义的。
iheanyi 2014年

9
@iheanyi:加法通常是可交换的-它通常需要两个相同类型的操作数。指针加法允许您添加一个指针和一个整数,但不能添加两个指针。恕我直言,这已经是一个非常奇怪的特殊情况,要求指针成为左操作数将不会是一个沉重的负担。(某些语言使用“ +”进行字符串连接;这当然不是可交换的。)
Keith Thompson

3
@supercat,那更糟。那将意味着有时x + 1!= 1 + x。这将完全违反加法的关联属性。
iheanyi 2014年

3
@iheanyi:我想你的意思是交换性财产;由于大多数实现(1LL + 1U)-2!= 1LL +(1U-2),加法已经不具有关联性。确实,此更改将使某些情况与当前不相关,例如3U +(UINT_MAX-2L)将等于(3U + UINT_MAX)-2。不过,最好的做法是,为该语言添加新的可推广整数和“包装”代数环的不同类型,以便将2加到ring16_t包含65535的a上将产生一个ring16_t值为1的值,而与大小无关int
2014年

196

而且当然

 ("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)。类似地,如果C1,则可以使用不同的程序集级别优化,并且开发人员必须再次使其明确,因为编译器无法识别它。(最近,编译器会这样做,因此,如今这些语法在很大程度上是不必要的)


127
实际上,这等于假。第一项“ ABCD” [2] == 2 [“ ABCD”]的计算结果为true,或者为1,并且1!='C':D
Jonathan Leffler

8
@乔纳森:同样的歧义导致了这篇文章的原始标题的编辑。我们是等号是数学上的对等,代码语法还是伪代码。我认为数学上的对等,但是由于我们在谈论代码,因此无法逃避我们正在按照代码语法查看所有内容。
黛娜

19
这不是神话吗?我的意思是创建+ =和++运算符是为了简化编译器?一些代码使它们变得更清晰,并且无论编译器如何处理,这都是有用的语法。
Thomas Padron-McCarthy

6
+ =和++还有另一个明显的好处。如果左侧在求值时更改了某个变量,则更改将只执行一次。a = a + ...; 会做两次。
Johannes Schaub-litb

8
否-“ ABCD” [2] == *(“ ABCD” + 2)= *(“ CD”)='C'。取消引用字符串会给您一个字符,而不是一个子字符串
MSalters 2009年

55

关于戴娜(Dinah)的问题,似乎没有人提到过一件事 sizeof

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


1
在已接受答案的注释中,对此进行了相当详尽的讨论。我在编辑中提到的谈话内容是原始问题,但没有直接解决您对sizeof的非常关注的问题。不知道如何在SO中做到最好。我应该对原件进行其他编辑吗?题?
Dinah

50

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

27
实际上,“ nan”并不等于其自身:cout << (a[5] == a[5] ? "true" : "false") << endl;is false
TrueY

8
@TrueY:他确实针对NaN案指出了这x == x一点(特别是并非总是如此)。我认为那是他的意图。因此,他在技术上是正确的(而且,正如他们所说,可能是最好的正确方法!)。
TimČas2015年

3
问题是关于C的,您的代码不是C代码。还有一个NANin <math.h>,它比in 更好0.0/0.0,因为0.0/0.0UB when __STDC_IEC_559__未定义(大多数实现未定义__STDC_IEC_559__,但是在大多数实现上0.0/0.0仍然可以使用)
12431234123412341234123

26

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

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]]],您会认为a是指向数组或数组的指针,并且i是索引。
12431234123412341234123 '18年

1
哇!这个“愚蠢”功能的用法非常酷。在某些问题的算法竞赛中可能很有用))
Serge Breusov

26

好的问题/答案。

只是想指出C指针和数组是不一样的,尽管在这种情况下,区别并不重要。

请考虑以下声明:

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

在中a.out,符号a位于数组开头的地址,符号p位于存储指针的地址,指针在该内存位置的值是数组的开头。


2
不,从技术上讲,它们是不同的。如果将一些b定义为int * const并使其指向数组,则它仍然是指针,这意味着在符号表中b指向存储地址的内存位置,该地址随后指向该数组所在的位置。
PolyThinker

4
很好的一点。我记得在一个模块中将全局符号定义为char s [100]并将其声明为extern char * s时遇到了一个非常讨厌的错误。在另一个模块中。将所有内容链接在一起后,程序的行为非常奇怪。因为使用extern声明的模块将数组的初始字节用作指向char的指针。
乔治

1
最初,在C的祖父母BCPL中,数组是一个指针。也就是说,当您写(我已经转译为C)时int a[10]得到的是一个名为“ a”的指针,该指针指向其他地方足以存储10个整数的位置。因此a + i和j + i具有相同的形式:添加几个存储单元的内容。实际上,我认为BCPL是无类型的,因此它们是相同的。并且sizeof类型的缩放不适用,因为BCPL纯粹是面向单词的(在基于字寻址的机器上也是如此)。
戴夫

我认为了解差异的最好方法是与进行比较int*p = a;int b = 5; 在后者中,“ b”和“ 5”都是整数,但是“ b”是变量,而“ 5”是固定值。同样,“ p”和“ a”都是字符的地址,但是“ a”是固定值。
詹姆斯·柯伦

20

对于C中的指针,我们有

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

并且

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

因此,的确 a[5] == 5[a].


15

不是答案,而是一些值得深思的地方。如果类具有重载的索引/下标运算符,则该表达式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&);
};

2
class Sub { public: int operator[](size_t nIndex) const { return 0; } friend int operator[](size_t nIndex, const Sub& This) { return 0; } };
Ben Voigt

1
您实际上尝试过编译吗?有一些不能在类外实现的运算符(即作为非静态函数)!
2013年

3
糟糕,您是对的。“ operator[]应为具有一个参数的非静态成员函数。” 我熟悉那个限制operator=,认为它不适用[]
Ben Voigt

1
当然,如果您更改[]运算符的定义,它将永远不再相等……如果a[b]等于*(a + b)并且更改了它,那么您也将不得不重载int::operator[](const Sub&);并且int不是一个类……
路易斯·科罗拉多

7
这不是... C
MD XF

11

在《 C语言中的指针和数组教程》中有很好的解释 Ted Jensen的。

泰德·詹森(Ted Jensen)解释为:

实际上,这是对的,也就是说,无论在哪里写a[i],都可以用*(a + i) 毫无问题。实际上,无论哪种情况,编译器都会创建相同的代码。因此,我们看到指针算术与数组索引是同一回事。两种语法都会产生相同的结果。

这并不是说指针和数组是同一回事,它们不是。我们只是说要确定数组的给定元素,我们可以选择两种语法,一种使用数组索引,另一种使用指针算法,它们产生相同的结果。

现在,看最后一个表达式,它的一部分.. (a + i)是使用+运算符和C规则的简单加法,该表达式是可交换的。即(a + i)等于(i + a)。因此,我们可以*(i + a)像一样轻松地编写*(a + i)。但是*(i + a)可能来自i[a]!所有这些都产生了一个奇怪的事实,即:

char a[20];

写作

a[3] = 'x';

和写作一样

3[a] = 'x';

4
a + i不是简单的加法,因为它是指针算法。如果a的元素大小为1(字符),则是的,就像整数+。但是,如果它是(例如)整数,则可能等于+ 4 * i。
亚历克斯·布朗

@AlexBrown是的,这是指针算术,这就是为什么您的最后一句话是错误的,除非您首先将'a'转换为(char *)(假设int为4个字符)。我真的不明白为什么这么多人迷上指针算术的实际值结果。指针算术的全部目的是抽象出底层的指针值,并使程序员考虑要操作的对象而不是地址值。
jschultz410 '18

8

我知道问题已经解决,但我无法抗拒分享这个解释。

我记得编译器设计原理,假设a是一个int数组,大小int为2个字节,&的基址为a1000。

怎么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处的对象。


6

C数组中arr[3]3[arr]相同,它们的等效指针符号*(arr + 3)*(3 + arr)。但是相反[arr]3还是[3]arr不正确,将导致语法错误,因为(arr + 3)*(3 + arr)*不是有效的表达式。原因是取消引用运算符应放置在表达式产生的地址之前,而不是地址之后。


6

在C编译器中

a[i]
i[a]
*(a+i)

是引用数组中元素的不同方法!(一点也不奇怪)


5

现在的历史。在其他语言中,BCPL对C的早期发展有相当大的影响。如果您在BCPL中声明了类似以下内容的数组:

let V = vec 10

实际上分配了11个内存字,而不是10个字。通常V是第一个,并且包含紧随其后的字的地址。因此,与C不同,命名V到该位置并获取数组的zeroeth元素的地址。因此,BCPL中的数组间接表示为

let J = V!5

确实确实需要做J = !(V + 5)(使用BCPL语法),因为有必要获取V以获取数组的基地址。因此V!55!V是同义词。有趣的是,WAFL(Warwick函数语言)是用BCPL编写的,据我所知,它倾向于使用后一种语法而不是前一种语法来访问用作数据存储的节点。当然,这是在35到40年前之间的某个地方,所以我的记忆有点生锈。:)

后来又有了创新,省去了多余的存储字并让编译器在命名数组时插入了数组的基地址。根据C历史记录,这大约是在将结构添加到C时发生的。

请注意,!在BCPL中,既是一元前缀运算符,又是二进制中缀运算符,在两种情况下都是间接的。只是二进制形式在执行间接操作之前将两个操作数相加。考虑到BCPL(和B)的面向单词的性质,这实际上很有意义。当获取数据类型并sizeof成为事实时,在C中就需要“指针和整数”的限制。


1

嗯,这是仅由于语言支持才可能实现的功能。

编译器解释a[i]*(a+i),表达式的5[a]计算结果为*(5+a)。由于加法是可交换的,因此证明两者相等。因此,表达式的计算结果为true


尽管多余,但这是清楚,简洁和简短的。
Bill K

0

在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++无效

a[2] 等于2 [a],因为这两者的内部运算都是

内部计算的“指针算术”为

*(a+3) 等于 *(3+a)


-4

指针类型

1)指向数据的指针

int *ptr;

2)const指向数据的指针

int const *ptr;

3)指向const数据的const指针

int const *const ptr;

并且数组是列表中(2)的类型
当您一次在该指针中初始化一个地址定义数组时 我们知道我们无法在程序中更改或修改const值,因为它在编译时会引发ERROR时间

我发现的主要区别是...

我们可以通过地址来重新初始化指针,但数组不能相同。

======
回到你的问题...
a[5]不过是*(a + 5)
您可以通过
a -包含地址(人们称其为基地址)很容易理解 -就像我们列表中的(2)类型的指针一样
[]-该运算符可以是可用指针替换*

所以最后...

a[5] == *(a +5) == *(5 + a) == 5[a] 
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.