Din*_*nah 1567 c arrays pointers pointer-arithmetic
正如Joel在Stack Overflow播客#34中用C编程语言(又名:K&R)所指出的那样,在C中提到了数组的这种属性:a[5] == 5[a]
乔尔说,这是因为指针运算,但我仍然不明白.为什么a[5] == 5[a]
?
Meh*_*ari 1863
C标准定义[]
运算符如下:
a[b] == *(a + b)
因此a[5]
将评估为:
*(a + 5)
Run Code Online (Sandbox Code Playgroud)
并且5[a]
将计算为:
*(5 + a)
Run Code Online (Sandbox Code Playgroud)
a
是指向数组的第一个元素的指针.a[5]
就是这5组值的元素进一步的a
,这是一样的*(a + 5)
,而且从小学数学,我们知道那些是相等的(除了是可交换的).
Dav*_*ley 282
因为数组访问是根据指针定义的. a[i]
被定义为意味着*(a + i)
,这是可交换的.
Kei*_*son 216
我认为其他答案正在忽略某些事情.
是的,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);
Run Code Online (Sandbox Code Playgroud)
和
pointer operator+(integer i, pointer p);
Run Code Online (Sandbox Code Playgroud)
其中只有第一个是真正必要的.
那么为什么会这样呢?
C++从C继承了这个定义,它从B得到它(数组索引的交换性在1972年用户参考B中明确提到),它是从BCPL(1967年的手册)得到的,它很可能从它获得它早期的语言(CPL?Algol?).
因此,数组索引是根据加法定义的,并且即使是指针和整数,这种加法也是可交换的,可以追溯到C的祖先语言.
这些语言的类型远不如现代C语言.特别是,指针和整数之间的区别经常被忽略.(在将unsigned
关键字添加到语言之前,早期的C程序员有时使用指针作为无符号整数.)因此,对于那些语言的设计者来说,可能不会发生因为操作数是不同类型而使加法不可交换的想法.如果用户想要添加两个"东西",无论这些"东西"是整数,指针还是其他东西,都不能用语言来阻止它.
多年来,对该规则的任何更改都会破坏现有代码(尽管1989 ANSI C标准可能是一个很好的机会).
改变C和/或C++要求将指针放在左边,而整数放在右边可能会破坏一些现有代码,但不会损失真正的表达能力.
所以现在我们拥有arr[3]
并且3[arr]
意味着完全相同的东西,尽管后一种形式永远不会出现在IOCCC之外.
Jam*_*ran 192
而且当然
("ABCD"[2] == 2["ABCD"]) && (2["ABCD"] == 'C') && ("ABCD"[2] == 'C')
Run Code Online (Sandbox Code Playgroud)
其主要原因是在70年代设计C时,计算机没有太多内存(64KB很多),所以C编译器没有做太多的语法检查.因此" X[Y]
"被盲目地翻译成" *(X+Y)
"
这也解释了" +=
"和" ++
"语法.形式" A = B + C
"中的所有内容都具有相同的编译形式.但是,如果B与A是同一个对象,则可以使用汇编级优化.但编译器不够明亮,无法识别它,因此开发人员必须(A += C
).类似地,如果C
是1
,则可以使用不同的程序集级别优化,并且开发人员必须再次明确,因为编译器无法识别它.(最近的编译器会这样做,所以这些天的语法基本上是不必要的)
小智 54
似乎没有人提到Dinah的问题sizeof
:
您只能向指针添加整数,不能将两个指针添加到一起.这样,当向整数添加指针或向指针添加整数时,编译器始终知道哪个位具有需要考虑的大小.
Pet*_*rey 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;
Run Code Online (Sandbox Code Playgroud)
版画
false
Run Code Online (Sandbox Code Playgroud)
Pol*_*ker 25
不错的问题/答案.
只是想指出C指针和数组是不一样的,虽然在这种情况下差异并不重要.
请考虑以下声明:
int a[10];
int* p = a;
Run Code Online (Sandbox Code Playgroud)
在a.out中,符号a位于数组开头的地址处,符号p位于存储指针的地址处,而该存储器位置处的指针值是数组的开头.
Fré*_*oni 23
我只是发现这个丑陋的语法可能是"有用的",或者至少非常有趣,当你想要处理引用同一数组中的位置的索引数组时.它可以替换嵌套的方括号,使代码更具可读性!
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)
}
Run Code Online (Sandbox Code Playgroud)
当然,我很确定实际代码中没有用例,但无论如何我发现它很有趣:)
小智 19
对于C中的指针,我们有
a[5] == *(a + 5)
Run Code Online (Sandbox Code Playgroud)
并且
5[a] == *(5 + a)
Run Code Online (Sandbox Code Playgroud)
因此,确实如此 a[5] == 5[a].
Aja*_*jay 15
不是答案,而只是一些值得思考的东西.如果类具有重载索引/下标运算符,则表达式0[x]
将不起作用:
class Sub
{
public:
int operator [](size_t nIndex)
{
return 0;
}
};
int main()
{
Sub s;
s[0];
0[s]; // ERROR
}
Run Code Online (Sandbox Code Playgroud)
由于我们无法访问int类,因此无法完成:
class int
{
int operator[](const Sub&);
};
Run Code Online (Sandbox Code Playgroud)
A.s*_*lar 10
它在 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]
!所有这一切都来自于以下奇怪的事实:Run Code Online (Sandbox Code Playgroud)char a[20];
写作
Run Code Online (Sandbox Code Playgroud)a[3] = 'x';
和写作一样
Run Code Online (Sandbox Code Playgroud)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
Run Code Online (Sandbox Code Playgroud)
所以,
类似地,当c代码被分解为3地址代码时,
5[a]
将变为 - >
Base Address of your Array a + (size of(data type for array a)*5)
i.e. 1000 + (2*5) = 1010
Run Code Online (Sandbox Code Playgroud)
所以基本上两个语句都指向内存中的相同位置,因此,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
Run Code Online (Sandbox Code Playgroud)
它将在990位置返回我的对象.
现在有点历史了。在其他语言中,BCPL 对 C 的早期发展产生了相当大的影响。如果您在 BCPL 中声明了一个数组,如下所示:
let V = vec 10
Run Code Online (Sandbox Code Playgroud)
这实际上分配了 11 个字的内存,而不是 10 个。通常 V 是第一个,并包含紧随其后的字的地址。因此,与 C 不同的是,命名 V 会转到该位置并获取数组第零个元素的地址。因此BCPL中的数组间接,表示为
let J = V!5
Run Code Online (Sandbox Code Playgroud)
确实必须这样做J = !(V + 5)
(使用 BCPL 语法),因为有必要获取 V 以获取数组的基地址。因此V!5
和5!V
是同义词。作为一个轶事观察,WAFL(Warwick Functional Language)是用 BCPL 编写的,据我所知,在访问用作数据存储的节点时,我倾向于使用后者而不是前者。当然,这是 35 到 40 年前的某个地方,所以我的记忆有点生疏。:)
免除存储的额外字和让编译器在命名时插入数组的基地址的创新是后来出现的。根据 C 历史论文,这发生在结构被添加到 C 的时间。
请注意,!
在 BCPL 中既是一元前缀运算符又是二元中缀运算符,在这两种情况下都进行了间接操作。只是二进制形式在执行间接操作之前包括两个操作数的相加。鉴于 BCPL(和 B)的面向词的性质,这实际上很有意义。“指针和整数”的限制在C获得数据类型时变得必要,并sizeof
成为一件事。
C是基于BCPL的。BCPL 直接将内存暴露为可寻址字序列。一元运算符!X
(也称为 LV)为您提供地址位置 X 的内容。为了方便起见,还有一个X!Y
相当于它的二元运算符为!(X+Y)
您提供位置 X 处数组的第 Y 个字的内容,或者等效地,数组中位置 Y 处的第 X 个字。
在 C 中,X!Y
成为X[Y]
,但原始 BCPL 语义为!(X+Y)
show through,这解释了为什么运算符是可交换的。