关于C#:对于数组,为什么a[5]==5[a]?

With arrays, why is it the case that a[5] == 5[a]?

正如Joel在Stack Overflow Podcast 34中指出的,在C编程语言(aka:k&r)中,提到了C:a[5] == 5[a]中数组的这个属性。

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


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

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

因此,a[5]将评估为:

1
*(a + 5)

5[a]将评估:

1
*(5 + a)

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


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


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

是的,根据定义,p[i]相当于*(p+i),这(因为加法是交换的)相当于*(i+p),这(同样,根据[]运算符的定义)相当于i

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

但是加法的交换性在这种情况下并不那么明显。

当两个操作数属于同一类型,或者甚至属于提升为通用类型的不同数字类型时,交换性完全有意义:x + y == y + x

但在本例中,我们专门讨论指针算法,其中一个操作数是指针,另一个是整数。(integer+integer是不同的操作,pointer+pointer是无意义的。)

C标准对+操作员(N1570 6.5.6)的描述如下:

For addition, either both operands shall have arithmetic type, or one
operand shall be a pointer to a complete object type and the other
shall have integer type.

它可以很容易地说:

For addition, either both operands shall have arithmetic type, or the left
operand shall be a pointer to a complete object type and the right operand
shall have integer type.

在这种情况下,i + pi

都是非法的。

在C++术语中,我们确实有两组重载的EDOCX1×7个运算符,它们可以被松散地描述为:

1
pointer operator+(pointer p, integer i);

1
pointer operator+(integer i, pointer p);

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

为什么是这样?

C++从C继承了这个定义,它从B中得到(数组索引的交换性在1972个用户对B的引用中明确提到),它从BCPL(手动日期1967)得到,它很可能是从更早的语言得到的(CPL)。Algol?).

所以数组索引是用加法来定义的,这个加法,即使是指针和整数,也是可互换的,可以追溯到几十年前的C语言。

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

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

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

所以现在我们有了arr[3]3[arr],意思完全一样,尽管后者不应该出现在ioccc之外。


当然

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

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

这也解释了"EDOCX1"(12)和"EDOCX1"(13)语法。"A = B + C"形式的所有内容都具有相同的编译形式。但是,如果B与A是同一个对象,那么就可以进行装配级优化。但是编译器不够聪明,无法识别它,所以开发人员不得不(A += C)。同样,如果C1,则可以使用不同的汇编级优化,开发人员必须再次明确说明,因为编译器无法识别它。(最近编译器会这样做,所以这些语法现在基本上是不必要的)


有一件事似乎没有人提到迪娜与以东的问题1(18):

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


从字面上回答这个问题。江户十一〔二十〕并不总是这样。

1
2
3
double zero = 0.0;
double a[] = { 0,0,0,0,0, zero/zero}; // NaN
cout << (a[5] == 5[a] ?"true" :"false") << endl;

印刷品

1
false


很好的问题/答案。

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

考虑以下声明:

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

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


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

1
2
3
4
5
6
7
8
9
10
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)

}

当然,我很确定在真正的代码中没有这种情况,但是我发现它很有趣:)


对于C中的指针,我们有

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

而且

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

因此,a[5] == 5[a].是真的


不是答案,只是一些思考的食物。如果类具有重载的index/subscript运算符,则表达式0[x]将不起作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Sub
{
public:
    int operator [](size_t nIndex)
    {
        return 0;
    }  
};

int main()
{
    Sub s;
    s[0];
    0[s]; // ERROR
}

因为我们没有访问int类的权限,所以不能这样做:

1
2
3
4
class int
{
   int operator[](const Sub&);
};


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

泰德·詹森解释说:

In fact, this is true, i.e wherever one writes a[i] it can be
replaced with *(a + i) without any problems. In fact, the compiler
will create the same code in either case. Thus we see that pointer
arithmetic is the same thing as array indexing. Either syntax produces
the same result.

This is NOT saying that pointers and arrays
are the same thing, they are not. We are only saying that to identify
a given element of an array we have the choice of two syntaxes, one
using array indexing and the other using pointer arithmetic, which
yield identical results.

Now, looking at this last
expression, part of it.. (a + i), is a simple addition using the +
operator and the rules of C state that such an expression is
commutative. That is (a + i) is identical to (i + a). Thus we could
write *(i + a) just as easily as *(a + i).
But *(i + a) could have come from i[a] ! From all of this comes the curious
truth that if:

1
char a[20];

writing

1
a[3] = 'x';

is the same as writing

1
3[a] = 'x';


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

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

a[5]将如何工作->

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

所以,

同样,当C代码分解为3个地址代码时,5[a]将变为->

1
2
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],它会给我

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

它将返回位置990处的Me对象。


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


在C编译器中

1
2
3
a[i]
i[a]
*(a+i)

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


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

1
let V = vec 10

这实际上分配了11个字的内存,而不是10个字。通常V是第一个,包含紧接着的单词的地址。因此,与C不同的是,命名V到达了那个位置,并获取了数组零元素的地址。因此,在bcpl中数组间接,表示为

1
let J = V!5

实际上必须执行EDOCX1(使用bcpl语法),因为需要获取v来获取数组的基地址。因此,V!55!V是同义词。作为一个轶事观察,wafl(华威函数语言)是用bcpl编写的,据我所知,倾向于使用后一种语法而不是前一种语法来访问用作数据存储的节点。当然这是35到40年前的事了,所以我的记忆有点生疏。:)

后来有了一项创新,即去掉多余的存储字,让编译器在数组命名时插入其基地址。根据C历史论文,这种情况发生在将结构添加到C时。

注意,bcpl中的!同时是一元前缀运算符和二进制中缀运算符,在这两种情况下都执行间接寻址。只是二进制形式在进行间接寻址之前包含了两个操作数的加法。考虑到bcpl(和b)面向单词的特性,这实际上很有意义。在C语言中,当获取数据类型时,需要对"指针和整数"进行限制,于是sizeof就成了一回事。


在C中

1
2
3
4
5
6
 int a[]={10,20,30,40,50};
 int *p=a;
 printf("%d
"
,*p++);//output will be 10
 printf("%d
"
,*a++);//will give an error

指针是"变量"

数组名是"助记键"或"同义词"

p++;有效,但a++无效

a[2]等于2〔A〕,因为这两者的内部操作都是

"指针算术"内部计算为

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


好吧,这是一个只有语言支持才可能实现的特性。

编译器将a[i]解释为*(a+i),表达式5[a]的计算结果为*(5+a)。因为加法是交换的,所以两个都是相等的。因此,该表达式的计算结果为true


指针类型

1)指向数据的指针

1
int *ptr;

2)常量指向数据的指针

1
int const *ptr;

3)常量指针指向常量数据

1
int const *const ptr;

数组是我们列表中的(2)种类型当一次定义数组时,该指针中的一个地址将被初始化。正如我们所知道的,我们不能更改或修改程序中的常量值,因为它在编译时抛出了一个错误。

我发现的主要区别是…我们可以用一个地址重新初始化指针,但数组的情况不同。

不等于回到你的问题…A[5]只是*(A+5)你很容易理解一个包含地址(人们称之为基地址)就像我们列表中的指针类型(2)[]-该运算符可以用指针*替换。

所以最后…

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