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

时间:2008-12-19 17:01:33

标签: c arrays pointers pointer-arithmetic

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

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

18 个答案:

答案 0 :(得分:1813)

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

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

因此a[5]将评估为:

*(a + 5)

5[a]将评估为:

*(5 + a)

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

答案 1 :(得分:278)

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

答案 2 :(得分:211)

我认为其他答案会遗漏某些内容。

是的,根据定义,p[i]等同于*(p+i),其中(因为加法是可交换的)相当于*(i+p),(同样,[]的定义} operator)相当于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继承了这个定义,从B获得它(数组索引的交换性在1972年明确提到Users' Reference to B),它来自BCPL(1967年手册),很可能从早期的语言中得到它(CPL?Algol?)。

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

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

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

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

现在我们arr[3]3[arr]的含义完全相同,但后一种形式永远不会出现在IOCCC之外。

答案 3 :(得分:191)

当然还有

 ("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,则可以使用不同的程序集级别优化,并且开发人员必须再次明确,因为编译器无法识别它。 (最近编译器会这样做,所以这些日子基本上不需要这些语法)

答案 4 :(得分:52)

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

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

答案 5 :(得分:48)

从字面上回答这个问题。 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

答案 6 :(得分:23)

不错的问题/答案。

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

考虑以下声明:

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

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

答案 7 :(得分:22)

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

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)

}

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

答案 8 :(得分:18)

对于C中的指针,我们有

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

以及

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

因此a[5] == 5[a].

确实如此

答案 9 :(得分:14)

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

答案 10 :(得分:9)

指南中的指南和阵列中有很好的解释 作者: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';

答案 11 :(得分:6)

我知道这个问题得到了回答,但我无法抗拒分享这个解释。

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

a[5]如何运作 - &gt;

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

所以,

类似地,当c代码被分解为3地址代码时, 5[a]将成为 - &gt;

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位置返回我的对象​​。

答案 12 :(得分:5)

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

答案 13 :(得分:4)

c编译器中的

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

是引用数组中元素的不同方法! (不是所有的WEIRD)

答案 14 :(得分:1)

现在有点历史了。在其他语言中,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)的面向单词的性质,这实际上很有意义。当C获得数据类型时,“指针和整数”的限制在C中变得必要,sizeof变成了问题。

答案 15 :(得分: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)

答案 16 :(得分:0)

嗯,这是一项功能,只有语言支持才能实现。

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

答案 17 :(得分:-3)

指针类型

1)指向数据的指针

int *ptr;

2)指向数据的const指针

int const *ptr;

3)指向const数据的const指针

int const *const ptr;

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

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

我们可以通过地址重新初始化指针,但数组不能使用相同的大小写。

======
然后回到您的问题...
a [5]只是*(a + 5)
您可以通过以下方式轻松理解
包含地址(人们称其为基址),就像列表中的(2)类型的指针一样
[]-该运算符可以替换为指针*。

所以终于...

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