正如 Joel 在Stack Overflow 播客 #34中指出的那样,在C 编程语言(又名:K & R)中,提到了 C 中数组的这个属性:a[5] == 5[a]
Joel 说这是因为指针运算,但我还是不明白。为什么a[5] == 5[a]
呢?
正如 Joel 在Stack Overflow 播客 #34中指出的那样,在C 编程语言(又名:K & R)中,提到了 C 中数组的这个属性:a[5] == 5[a]
Joel 说这是因为指针运算,但我还是不明白。为什么a[5] == 5[a]
呢?
C 标准对[]
运算符的定义如下:
a[b] == *(a + b)
因此a[5]
将评估为:
*(a + 5)
并将5[a]
评估为:
*(5 + a)
a
是指向数组第一个元素的指针。a[5]
是距离 5 个元素的值,与相同a
,*(a + 5)
从小学数学我们知道它们是相等的(加法是可交换的)。
因为数组访问是根据指针定义的。 a[i]
被定义为表示*(a + i)
,它是可交换的。
我认为其他答案遗漏了一些东西。
是的,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 强。特别是,指针和整数之间的区别经常被忽略。(早期的 C 程序员有时使用指针作为无符号整数,在unsigned
关键字被添加到语言之前。)因此,由于操作数属于不同类型而使加法不可交换的想法可能不会发生在那些语言的设计者身上。如果用户想要添加两个“东西”,无论这些“东西”是整数、指针还是其他东西,语言都无法阻止它。
多年来,对该规则的任何更改都会破坏现有代码(尽管 1989 年的 ANSI C 标准可能是一个很好的机会)。
将 C 和/或 C++ 更改为需要将指针放在左侧,将整数放在右侧可能会破坏一些现有代码,但不会损失真正的表达能力。
所以现在我们有了arr[3]
和3[arr]
意思完全相同的东西,尽管后一种形式永远不应该出现在IOCCC之外。
而且当然
("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
,则可以使用不同的汇编级别优化,并且开发人员必须再次使其显式,因为编译器无法识别它。(最近编译器这样做了,所以这些语法这些天在很大程度上是不必要的)
关于黛娜的问题,似乎没有人提到一件事sizeof
:
您只能将整数添加到指针,不能将两个指针相加。这样,在将指针添加到整数或将整数添加到指针时,编译器始终知道哪个位具有需要考虑的大小。
从字面上回答这个问题。并非总是如此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
我只是发现这种丑陋的语法可能是“有用的”,或者当您想要处理引用同一数组中位置的索引数组时,至少玩起来很有趣。它可以替换嵌套的方括号,使代码更具可读性!
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 指针和数组是不一样的,尽管在这种情况下区别并不重要。
考虑以下声明:
int a[10];
int* p = a;
在a.out
中,符号a
位于数组开头的地址,符号p
位于存储指针的地址,该内存位置的指针值是数组的开头。
对于 C 中的指针,我们有
a[5] == *(a + 5)
并且
5[a] == *(5 + a)
因此,这是真的a[5] == 5[a].
不是答案,只是一些思考的食物。如果类具有重载的索引/下标运算符,则表达式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&);
};
它在Ted Jensen的A TUTORIAL ON POINTERS AND ARRAYS in 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
是一个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 处返回我的对象。
在 C 数组中,arr[3]
和3[arr]
是相同的,它们的等效指针符号是*(arr + 3)
to *(3 + arr)
。但反之[arr]3
or[3]arr
不正确,会导致语法错误,as (arr + 3)*
and (3 + arr)*
are not valid表达式。原因是解引用运算符应该放在表达式产生的地址之前,而不是地址之后。
在 c 编译器中
a[i]
i[a]
*(a+i)
是引用数组中元素的不同方法!(一点也不奇怪)
现在有点历史了。在其他语言中,BCPL 对 C 的早期开发产生了相当大的影响。如果你在 BCPL 中声明了一个数组,如下所示:
let V = vec 10
实际上分配了 11 个字的内存,而不是 10 个。通常 V 是第一个字,并且包含紧随其后的字的地址。因此,与 C 不同的是,命名 V 会转到该位置并获取数组中第零个元素的地址。因此 BCPL 中的数组间接,表示为
let J = V!5
确实必须这样做J = !(V + 5)
(使用 BCPL 语法),因为必须获取 V 才能获取数组的基地址。因此V!5
和5!V
是同义词。作为轶事观察,WAFL(Warwick 函数式语言)是用 BCPL 编写的,据我记忆,在访问用作数据存储的节点时,倾向于使用后一种语法而不是前者。当然,这是 35 到 40 年前的某个地方,所以我的记忆有点生疏了。:)
省略了额外的存储字并让编译器在数组命名时插入基地址的创新是后来出现的。根据 C 历史论文,这大约发生在将结构添加到 C 的时候。
请注意,!
在 BCPL 中既是一元前缀运算符又是二元中缀运算符,在这两种情况下都执行间接操作。只是二进制形式在进行间接之前包括两个操作数的加法。鉴于 BCPL(和 B)的面向单词的性质,这实际上很有意义。当 C 获得数据类型时,“指针和整数”的限制是必要的,并sizeof
成为一个东西。
在 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
。
因为 C 编译器总是将数组表示法转换为指针表示法。
a[5] = *(a + 5)
也5[a] = *(5 + a) = *(a + 5)
因此,两者都是平等的。
因为避免混淆嵌套很有用。
你宁愿读这个:
array[array[head].next].prev
或这个:
head[array].next[array].prev
Incidentally, C++ has a similar commutative property for function calls. Rather than writing g(f(x))
as you must in C, you may use member functions to write x.f().g()
. Replace f and g with lookup tables and you can write g[f[x]]
(functional style) or (x[f])[g]
(oop style). The latter gets really nice with structs containing indices: x[xs].y[ys].z[zs]
. Using the more common notation that's zs[ys[xs[x].y].z]
.