正如Joel在Stack Overflow播客#34中用C编程语言(又名:K&R)所指出的那样,在C中提到了数组的这种属性:a[5] == 5[a]
乔尔说,这是因为指针运算,但我仍然不明白.为什么a[5] == 5[a]
?
1> Mehrdad Afsh..:
C标准定义[]
运算符如下:
a[b] == *(a + b)
因此a[5]
将评估为:
*(a + 5)
并且5[a]
将计算为:
*(5 + a)
a
是指向数组的第一个元素的指针.a[5]
就是这5组值的元素进一步的a
,这是一样的*(a + 5)
,而且从小学数学,我们知道那些是相等的(除了是可交换的).
我想知道它是不是更像*((5*sizeof(a))+ a).虽然很好的解释.
@Dinah:从C编译器的角度来看,你是对的.不需要sizeof,我提到的那些表达是相同的.但是,编译器在生成机器代码时会考虑sizeof.如果a是一个int数组,`a [5]`将编译为类似`mov eax,[ebx + 20]`而不是`[ebx + 5]`
"从小学数学我们知道那些是平等的" - 我明白你是在简化,但我和那些觉得这样的人在简化过程中有所帮助.它不是基本的`*(10 +(int*)13)!=*((int*)10 + 13)`.换句话说,这里比小学算术更多.可交换性主要依赖于编译器识别哪个操作数是指针(以及对象的大小).换句话说,`(1个苹果+ 2个橙子)=(2个橙子+ 1个苹果)`,但是`(1个苹果+ 2个橙子)!=(1个橙子+ 2个苹果)`.
@ sr105:这是+运算符的特例,其中一个操作数是指针而另一个是整数.标准说结果将是指针的类型.编译器/必须足够聪明.
@Dinah:A是一个地址,比如0x1230.如果a在32位int数组中,则a [0]为0x1230,a [1]为0x1234,a [2]为0x1238 ... a [5]为x1244等.如果我们只添加5 0x1230,我们得到0x1235,这是错误的.
所以在5 [a]的情况下,编译器足够智能使用"*((5*sizeof(a))+ a)"而不是"*(5 +(a*sizeof(5)))"?注意:我想是的.我在GCC尝试了这个并且它有效.
当你向指针添加一个整数时,编译器知道指针指向的是什么类型(所以如果a是一个int*,它是4个字节或者其他......)所以可以执行算术权限.基本上,如果你做"p ++",那么应调整p以指向内存中的下一个对象."p ++"基本上等同于"p = p + 1",因此指针添加的定义使一切都排成一行.另请注意,您无法使用`void*`类型的指针进行算术运算.
@LarsH:你是对的.我会说它更类似于`(10in + 10cm)`而不是苹果和橙子(你可以有意义地将它们转换成另一种).
@Mehrdad:够公平的.也许更好的类比是日期与时间间隔,如'(2010年5月1日+ 3周)`.
如果你有一个4字节整数的数组,则[1] - a [0] = 4(两个指针之间的4个字节).
为什么考虑sizeof().我认为指向'a'的指针是数组的开头(即:0元素).如果是这样,你只需要*(a + 5).我的理解一定是不正确的.什么是正确的理由?
@James:宾果游戏.这就是我需要看到的.我一直看到sizeof()和思考count()并且变得非常困惑.不是我最亮的时刻.谢谢!
评论永远不会浮现在我的记忆中
@Tomalak我明白了.有很多地方是相关的,我们已经讨论过了.然而,虽然问题特别询问*原因*为什么它的工作方式.我无法想象这是`5 [a]`的行为,如果在C的原始实现中,指针实际上并不是代表CPU可直接理解的内存地址的二进制文件.如果我们想要过于迂腐,那么答案(对于这个问题还有更多)是:"因为标准在一侧定义`int`类型的`[]`运算符的行为,而在另一侧定义数组或指针类型的行为".
@BenVoigt其实我觉得你的例子应该是`double x = a/2;`.如果它是'2.0`,结果将是`double`,无论`a`是`int`还是`double`.
一点历史可能有助于解释为什么会这样.如上所述:http://www.gotw.ca/conv/003.htm C和C++起源于BCPL.BCPL使用`!`(aka pling)作为间接运算符,它采用了两种形式,一元和二元.`!a`一元与`*a`在C/C++中的含义相同,即一元间接.`a!b`二进制用于数组查找,相当于C中的`a [b]`.因为二进制`!`在BCPL中是可交换的,并且与`!(a + b)具有相同的效果``我非常强烈怀疑这就是数组间接在C/C++中具有相同的交换行为的原因.
2> David Thornl..:
因为数组访问是根据指针定义的. a[i]
被定义为意味着*(a + i)
,这是可交换的.
数组不是根据指针定义的,而是_access_对它们的定义.
我会添加"所以它等于`*(i + a)`,它可以写成`i [a]`".
我建议你包括标准的引用,如下所示:6.5.2.1:2后缀表达式后跟方括号[]中的表达式是数组对象元素的下标.下标运算符[]的定义是E1 [E2]与(*((E1)+(E2)))相同.由于适用于binary +运算符的转换规则,如果E1是数组对象(等效地,指向数组对象的初始元素的指针)并且E2是整数,则E1 [E2]指定E2的第E2个元素. E1(从零开始计数).
3> Keith Thomps..:
我认为其他答案正在忽略某些事情.
是的,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语言.特别是,指针和整数之间的区别经常被忽略.(在将unsigned
关键字添加到语言之前,早期的C程序员有时使用指针作为无符号整数.)因此,对于那些语言的设计者来说,可能不会发生因为操作数是不同类型而使加法不可交换的想法.如果用户想要添加两个"东西",无论这些"东西"是整数,指针还是其他东西,都不能用语言来阻止它.
多年来,对该规则的任何更改都会破坏现有代码(尽管1989 ANSI C标准可能是一个很好的机会).
改变C和/或C++要求将指针放在左边,而整数放在右边可能会破坏一些现有代码,但不会损失真正的表达能力.
所以现在我们拥有arr[3]
并且3[arr]
意味着完全相同的东西,尽管后一种形式永远不会出现在IOCCC之外.
这个属性的奇妙描述.从高级别的角度来看,我认为`3 [arr]`是一个有趣的工件,但是很少使用它.我回答过这个问题(
)的公认答案改变了我对语法的思考方式.虽然技术上通常没有正确和错误的方式来做这些事情,但这些类型的功能开始以一种与实现细节分开的方式进行思考.这种不同的思维方式有好处,当你注意到实现细节时,这种思维方式部分失去了.
@iheanyi:加法通常是可交换的 - 它通常需要两个相同类型的操作数.指针添加允许您添加指针和整数,但不能添加两个指针.恕我直言,这已经是一个非常奇怪的特殊情况,要求指针是左操作数不会是一个重大的负担.(有些语言使用"+"进行字符串连接;这肯定不是可交换的.)
增加是可交换的.对于C标准来定义它否则会很奇怪.这就是为什么它不能很容易地说:"对于另外,无论是两个操作数应具有算术类型,或左操作数应该是指向一个完整的对象类型和正确的操作应具有整数类型." - 这对大多数添加东西的人来说没有意义.
@supercat,那更糟.这意味着有时x + 1!= 1 + x.这将完全违反添加的关联属性.
@iheanyi:我认为你的意思是交换财产; 加法已经不是关联的,因为在大多数实现中(1LL + 1U)-2!= 1LL +(1U-2).实际上,这种变化会使一些情况联想起来,而现在却没有,例如3U +(UINT_MAX-2L)将等于(3U + UINT_MAX)-2.然而,最好的是,语言为可升级整数和"包裹"代数环添加新的不同类型,因此在保存65535的`ring16_t`中加2会产生值为1的`ring16_t`,*独立于`int`*的大小.
4> James Curran..:
而且当然
("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
,则可以使用不同的程序集级别优化,并且开发人员必须再次明确,因为编译器无法识别它.(最近的编译器会这样做,所以这些天的语法基本上是不必要的)
实际上,评估为假; 第一个术语"ABCD"[2] == 2 ["ABCD"]的计算结果为真,或1和1!='C':D
这不是一个神话吗?我的意思是创建+ =和++运算符是为了简化编译器?有些代码会更清晰,无论编译器使用什么代码,它都是有用的语法.
@Jonathan:同样含糊不清导致编辑这篇文章的原始标题.我们是等数标记数学等价,代码语法还是伪代码.我认为数学等价,但由于我们谈论代码,我们无法逃避我们在代码语法方面查看所有内容.
否 - "ABCD"[2] ==*("ABCD"+ 2)=*("CD")='C'.取消引用字符串会为您提供char,而不是子字符串
+ =和++有另一个显着的好处.如果左侧在评估时更改了某些变量,则更改只会执行一次.a = a + ...; 会做两次.
@ ThomasPadron-McCarthy:来自[here](http://cm.bell-labs.com/cm/cs/who/dmr/chist.html):"在开发过程中,[Thompson]不断努力克服内存限制:每种语言添加了编译器以使其几乎不适合,但每次重写利用该功能都会减小其大小.例如,B引入了广义赋值运算符,使用x = + y将y添加到x ... Thompson更进了一步发明++和 - 运算符......创新的更强烈动机可能是他观察到++ x的翻译小于x = x + 1的翻译."
"以这种方式实现起来会更容易"然后"在数学上它更有意义",所以即使它没有任何实际意义,也可以将它作为一种理性添加到语言中.
听说+ =减少错误的几率,因为你写变量名两次而不是三次......
@dave:它是`x + = 5;`而不是`x = + 5;`因为后者将被解析为`x =(+ 5);`
@JamesCurran我很确定它最初是以"LHS = - RHS;"开头的,并最终交换使用` - =`.
那么,如果*++*在很大程度上是不必要的,那么*C++*基本上是不必要的吗?我正在坚持C###,我自己.
@Vatine是对的,在`+ =`之前是`= +`.B编程语言(我很惊讶地阅读它仍然使用),C的祖先,使用`= +`形式.IIRC,改变它的主要原因是`i = -1;`是模棱两可的.对编译器没有模糊性,但对于那些无法理解是否应该将`i`减少1(因此正确写入),或者是否应该将`-1`分配给`i`(因此代码中的错误).免责声明:我的回忆可能有误.
5> 小智..:
似乎没有人提到Dinah的问题sizeof
:
您只能向指针添加整数,不能将两个指针添加到一起.这样,当向整数添加指针或向指针添加整数时,编译器始终知道哪个位具有需要考虑的大小.
6> Peter Lawrey..:
从字面上回答这个问题.并非总是如此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
实际上"nan"并不等于它自己:`cout <<(a [5] == a [5]?"true":"false")<< endl;`是`false`.
@TrueY:他确实陈述了专门针对NaN案例(特别是`x == x`并不总是如此).我认为这是他的意图.所以他在技术上*是正确的(可能,正如他们所说,最好的正确!).
问题是关于C,你的代码不是C代码.在``中还有一个`NAN`,它比'0.0/0.0`更好,因为当没有定义`__STDC_IEC_559__`时,`0.0/0.0`是UB(大多数实现没有定义`__STDC_IEC_559__` ,但在大多数实现中,`0.0/0.0`仍然有效)
7> PolyThinker..:
不错的问题/答案.
只是想指出C指针和数组是不一样的,虽然在这种情况下差异并不重要.
请考虑以下声明:
int a[10];
int* p = a;
在a.out中,符号a位于数组开头的地址处,符号p位于存储指针的地址处,而该存储器位置处的指针值是数组的开头.
很好的一点.当我在一个模块中将全局符号定义为char s [100]时,我记得有一个非常讨厌的错误,将其声明为extern char*s; 在另一个模块中.将它们连接在一起之后,程序表现得非常奇怪.因为使用extern声明的模块使用数组的初始字节作为指向char的指针.
不,从技术上讲它们不一样.如果将某个b定义为int*const并使其指向一个数组,它仍然是一个指针,这意味着在符号表中,b指的是存储地址的内存位置,该地址又指向数组所在的位置.
8> Frédéric Ter..:
我只是发现这个丑陋的语法可能是"有用的",或者至少非常有趣,当你想要处理引用同一数组中的位置的索引数组时.它可以替换嵌套的方括号,使代码更具可读性!
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)
}
当然,我很确定实际代码中没有用例,但无论如何我发现它很有趣:)
9> 小智..:
对于C中的指针,我们有
a[5] == *(a + 5)
并且
5[a] == *(5 + a)
因此,确实如此 a[5] == 5[a].
10> Ajay..:
不是答案,而只是一些值得思考的东西.如果类具有重载索引/下标运算符,则表达式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&);
};
这......是不是......℃.
哎呀,你是对的."`operator []`应该是一个非静态成员函数,只有一个参数." 我熟悉`operator =`的限制,不认为它适用于`[]`.
`class Sub {public:int operator [](size_t nIndex)const {return 0; } friend int operator [](size_t nIndex,const Sub&This){return 0; }
11> A.s. Bhullar..:
它在
Ted Jensen的C指南和C的阵列中有很好的解释.
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';
a + i不是简单的加法,因为它是指针算术.如果a的元素的大小是1(char),那么是的,它就像整数+.但如果它是(例如)一个整数,那么它可能等于+ 4*i.
12> Ajinkya Pati..:
我知道问题已得到解答,但我无法抗拒分享这个解释.
我记得编译器设计的原理,我们假设a
是一个int
数组,大小int
是2字节,基地址a
是1000.
怎么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位置返回我的对象.
13> 小智..:
在C数组,arr[3]
并且3[arr]
是相同的,和它们的等效指针符号是*(arr + 3)
对*(3 + arr)
.但相反[arr]3
或[3]arr
不正确会导致语法错误,因为(arr + 3)*
并且(3 + arr)*
不是有效的表达式.原因是解除引用操作符应放在表达式产生的地址之前,而不是在地址之后.
14> AVIK DUTTA..:
在C编译器中
a[i]
i[a]
*(a+i)
是引用数组中元素的不同方法!(一点也不奇怪)