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

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


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

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

因此,[5]将评估:

*(a + 5)

并且5[a]将评估:

*(5 + a)

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


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


当然,还有

 ("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,则可以使用不同的汇编级优化,开发人员再次必须使其显式,因为编译器无法识别它


很好的问题/答案。

我只想指出,C指针和数组并不相同,尽管在这种情况下,差异并不是本质的。

考虑以下声明:

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

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


关于Dinah的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&);
};

按照字面意思回答问题。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

对于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)
    
}

当然,我很确定在实际代码中没有这样的用例,但我还是觉得很有趣:)


我认为其他答案遗漏了一些东西。

是的,p[i]根据定义等价于*(p+i),*(i+p)(因为加法是可交换的)等效于*(i+p),而*(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);

and

pointer operator+(integer i, pointer p);

其中只有第一个是真正必要的。

那么为什么会这样呢?

C++继承了C的这一定义,后者从B获得了它(数组索引的交换性在1972年的《用户参考B》中明确提到),后者从BCPL(1967年的手册)获得了它,BCPL很可能是从更早的语言(CPL?Algol?)获得的。

因此,数组索引是用加法来定义的,即使是指针和整数的加法,也是可交换的,这一想法可以追溯到几十年前,直到C的祖先语言。

这些语言比现代C语言的强类型少得多。特别是,指针和整数之间的区别经常被忽略。(在无符号关键字被添加到语言中之前,早期的C程序员有时将指针用作无符号整数。)因此,由于操作数的类型不同,所以使加法不可交换的想法可能不会出现在这些语言的设计者身上。如果用户想添加两个“东西”,无论这些“东西”是整数、指针还是其他东西,都不能由语言来阻止。

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

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

所以现在我们有了arr[3]和3[arr],意思完全相同,尽管后一种形式不应该出现在IOCCC之外。


它在C语言中的指针和阵列教程中有很好的解释由Ted Jensen撰写。

Ted Jensen解释为:

事实上,这是真的,也就是说,无论你在哪里写一个替换为*(a+i)而没有任何问题。事实上,编译器将在任何情况下创建相同的代码。因此,我们看到了指针算术与数组索引相同。任一语法都会产生同样的结果。这并不是说指针和数组是一样的东西,它们不是。我们这么说只是为了确定对于数组的给定元素,我们可以选择两种语法,一种使用数组索引,另一种使用指针算法产生相同的结果。现在,看看最后一个表达式的一部分..(a+i)是使用+运算符和C的规则声明这样的表达式是交换的。即(a+i)与(i+a)相同。因此,我们可以写*(i+a)和*(a+i)一样容易。但*(i+a)可能来自i[a]!所有这些都带来了好奇如果:字符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)

引用数组中元素的方式不同!(一点都不奇怪)


我知道问题得到了答案,但我忍不住分享了这个解释。

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

[5]将如何工作->

Base Address of your Array a + (5*size of(data type for array a))
i.e. 1000 + (5*2) = 1010

So,

类似地,当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中工作的原因。

即,如果我访问[-5],它会给我

Base Address of your Array a + (-5 * size of(data type for array a))
i.e. 1000 + (-5*2) = 990

它将在990位置返回我的对象。


In 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到那个位置,并获取数组第0个元素的地址。因此,BCPL中的数组间接寻址表示为

let J = V!5

真的不得不做J=!(V+5)(使用BCPL语法),因为需要获取V以获得阵列的基地址。因此,V!5和5!V是同义词。据坊间观察,WAFL(Warwick Functional Language,沃里克函数语言)是用BCPL编写的,据我所知,在访问用作数据存储的节点时,倾向于使用后一种语法而不是前一种语法。当然这是35到40年前的某个地方,所以我的记忆有点生疏

省去了额外的存储字,并在命名数组时让编译器插入数组的基地址,这一创新是后来才出现的。根据C历史论文,这大约发生在C中添加结构的时候。

注意!BCPL中既有一元前缀运算符,也有二元中缀运算符,在这两种情况下都是间接操作。只是二进制形式在执行间接操作之前包括两个操作数的相加。鉴于BCPL(和B)面向单词的性质,这实际上很有意义。当C获得数据类型时,“指针和整数”的限制就变得必要了,sizeof也成了一件事。


因为C编译器总是将数组表示法转换为指针表示法。a[5]=*(a+5)也是5[a]=*所以,两者都是相等的。


因为避免混淆嵌套非常有用。

你愿意读这个吗

array[array[head].next].prev

或者:

head[array].next[array].prev

顺便说一下,C++对于函数调用具有类似的交换性质。您可以使用成员函数来编写x.f().g(),而不是像在C中那样编写g(f(x))。用查找表替换f和g,您可以编写g[f[x]](函数样式)或(x[f])[g](oop样式)。后者非常适合包含索引的结构:x[xs].y[ys].z[zs]。使用更常见的符号zs[ys[xs[x].y].z]。


C基于BCPL。BCPL将存储器直接暴露为一系列可寻址字。一元运算符!X(也称为LV)为您提供了地址位置X的内容。为了方便起见,还有一个二进制运算符X!Y等于!(X+Y),它为您提供了位置X处数组的第Y个字的内容,或者等效地,位置Y处数组的X个字。

在C,X!Y变成了X[Y],但是!的原始BCPL语义!(X+Y)表示通过,这解释了为什么算子是可交换的。