在书中,"Understanding and Using C Pointers" by Richard Reese在第85页上说,
int vector[5] = {1, 2, 3, 4, 5};
vector[i]
生成的代码与*(vector+i)
生成的代码不同。符号vector[i]
生成从位置向量开始移动的机器代码,从该位置 movingi
位置开始使用机器代码。符号*(vector+i)
生成从位置vector
开始的机器代码,向该地址添加i
,然后使用该地址的内容。结果相同时,生成的机器代码不同。对于大多数程序员而言,这种差异几乎没有意义。
您可以看到excerpt here。这是什么意思?在什么情况下,任何编译器会为这两个生成不同的代码?从基本位置“移动”与从基本位置“添加”之间有区别吗?我无法在GCC上使用它-生成不同的机器代码。
答案 0 :(得分:94)
引用是错误的。悲剧性的是,这种垃圾在这十年中仍然被发表。实际上,C标准将x[y]
定义为*(x+y)
。
页面后面有关左值的部分也是完全错误的。
恕我直言,使用这本书的最好方法是将其放入回收箱或燃烧。
答案 1 :(得分:33)
我有2个C文件:ex1.c
% cat ex1.c
#include <stdio.h>
int main (void) {
int vector[5] = { 1, 2, 3, 4, 5 };
printf("%d\n", vector[3]);
}
和ex2.c
% cat ex2.c
#include <stdio.h>
int main (void) {
int vector[5] = { 1, 2, 3, 4, 5 };
printf("%d\n", *(vector + 3));
}
然后将它们都编译为汇编,并显示生成的汇编代码中的差异
% gcc -S ex1.c; gcc -S ex2.c; diff -u ex1.s ex2.s
--- ex1.s 2018-07-17 08:19:25.425826813 +0300
+++ ex2.s 2018-07-17 08:19:25.441826756 +0300
@@ -1,4 +1,4 @@
- .file "ex1.c"
+ .file "ex2.c"
.text
.section .rodata
.LC0:
Q.E.D。
C标准非常明确地声明了(C11 n1570 6.5.2.1p2):
- 后缀表达式后跟方括号
[]
中的表达式是数组对象元素的下标名称。 下标运算符[]
的定义是E1[E2]
与(*((E1)+(E2)))
相同。由于适用于二进制+
运算符的转换规则,如果E1
是数组对象(等效于指向数组对象初始元素的指针),而E2
是整数,E1[E2]
指定E2
的第E1
个元素(从零开始计数)。
此外,假设规则在此处适用-如果程序的行为相同,即使语义不相同。
答案 2 :(得分:19)
所引用的段落是完全错误的。表达式vector[i]
和*(vector+i)
完全相同,可以在所有情况下产生相同的代码。
根据定义,表达式vector[i]
和*(vector+i)
相同。这是C编程语言的核心和基本属性。任何有能力的C程序员都可以理解这一点。题为理解和使用C指针的书的任何作者都必须理解这一点。 C编译器的任何作者都会理解这一点。这两个片段将并非偶然地生成相同的代码,但是因为实际上任何C编译器实际上都会几乎立即将一种形式转换为另一种形式,因此在到达其代码生成阶段时,它甚至都不知道最初使用的是哪种形式。 (如果C编译器为vector[i]
生成与*(vector+i)
不同的代码,我会感到非常惊讶。)
实际上,引用的文本矛盾。正如您所指出的,这两个段落
符号
vector[i]
生成从位置vector
开始的机器代码,从该位置移动i
个位置,并使用其内容。
和
符号
*(vector+i)
生成从位置vector
开始的机器代码,将i
添加到该地址,然后使用该地址的内容。
说基本相同的事情。
他的语言与旧question 6.2的C FAQ list中的语言极为相似:
...当编译器看到表达式
a[3]
时,它将发出代码,以从位置“a
”开始,经过三个位置,然后在此处获取字符。当它看到表达式p[3]
时,它发出代码,从位置“p
”开始,在此处获取指针值,将指针加3,最后获取指向的字符。
但是这里的主要区别当然是 a
是一个数组,而p
是一个指针。常见问题列表不是在谈论a[3]
与*(a+3)
,而是谈论a[3]
(或*(a+3)
),其中a
是一个数组,而{{1} }(或p[3]
),其中*(p+3)
是指针。 (当然,这两种情况会生成不同的代码,因为数组和指针是不同的。正如FAQ列表所述,从指针变量获取地址与使用数组的地址根本不同。)
答案 3 :(得分:6)
我认为原始文本可能是一些编译器可能会或可能不会执行的优化。
示例:
for ( int i = 0; i < 5; i++ ) {
vector[i] = something;
}
vs。
for ( int i = 0; i < 5; i++ ) {
*(vector+i) = something;
}
在第一种情况下,优化编译器可能会检测到数组vector
逐个元素迭代,从而生成类似
void* tempPtr = vector;
for ( int i = 0; i < 5; i++ ) {
*((int*)tempPtr) = something;
tempPtr += sizeof(int); // _move_ the pointer; simple addition of a constant.
}
它甚至可以在可用的情况下使用目标CPU的指针后递增指令。
对于第二种情况,让编译器看到通过某些“任意”指针算术表达式计算出的地址表现出相同的属性,即在每个变量中单调地前进固定的数量是“困难的”迭代。因此,它可能找不到优化并在每次使用附加乘法的迭代中计算((void*)vector+i*sizeof(int))
。在这种情况下,没有(临时)指针被“移动”,而仅重新计算了一个临时地址。
但是,该语句可能并不适用于所有版本的所有C编译器。
更新:
我检查了上面的示例。 看来,至少在没有gcc-8.1 x86-64的情况下启用优化,第二种(指针算术)形式比第一种(数组索引)产生更多的代码(2条额外指令)。
请参阅:https://godbolt.org/g/7DaPHG
但是,在启用了 (-O
... -O3
)的任何优化下,两者的生成代码都是相同的(长度)。
答案 4 :(得分:6)
当arr[i]
是数组对象时,标准将arr
的行为指定为等效于将arr
分解为指针,添加i
并取消引用结果。尽管这些行为在所有标准定义的情况下都是等效的,但在某些情况下,即使标准确实要求动作,编译器也可以有效地处理动作,因此arrayLvalue[i]
和*(arrayLvalue+i)
的处理可能会有所不同
例如,给定
char arr[5][5];
union { unsigned short h[4]; unsigned int w[2]; } u;
int atest1(int i, int j)
{
if (arr[1][i])
arr[0][j]++;
return arr[1][i];
}
int atest2(int i, int j)
{
if (*(arr[1]+i))
*((arr[0])+j)+=1;
return *(arr[1]+i);
}
int utest1(int i, int j)
{
if (u.h[i])
u.w[j]=1;
return u.h[i];
}
int utest2(int i, int j)
{
if (*(u.h+i))
*(u.w+j)=1;
return *(u.h+i);
}
GCC为test1生成的代码将假定arr [1] [i]和arr [0] [j]不能为别名,但是为test2生成的代码将允许指针算术访问整个数组。 ,gcc会认识到在utest1中,左值表达式uh [i]和uw [j]都访问同一个并集,但是它不够复杂,不足以注意到*(u.h + i)和*(u.w + j)在utest2中。
答案 5 :(得分:3)
让我尝试“狭义地”回答这个问题(其他人已经描述了为什么“按原样”描述有点缺乏/不完整/误导):
在什么情况下,任何编译器会为这两个生成不同的代码?
“并非非常优化”的编译器可能会在几乎任何上下文中生成不同的代码,因为在解析时存在差异:x[y]
是一个表达式(数组的索引),而{{1 }}是两个表达式(将整数添加到指针,然后取消引用)。当然,识别这一点(即使在解析时)并加以相同也不是很难,但是,如果要编写一个简单/快速的编译器,则应避免在其中添加“太多的技巧”。例如:
*(x+y)
编译器在解析char vector[] = ...;
char f(int i) {
return vector[i];
}
char g(int i) {
return *(vector + i);
}
时会看到“索引”,并且可能会生成类似内容(对于某些68000类CPU):
f()
OTOH,对于MOVE D0, [A0 + D1] ; A0/vector, D1/i, D0/result of function
,编译器看到两件事:首先是解除引用(“尚未发生的事情”),然后将整数添加到指针/数组,因此不是很优化,它可以结束于:
g()
显然,这非常依赖于实现,某些编译器也可能不喜欢使用用于MOVE A1, A0 ; A1/t = A0/vector
ADD A1, D1 ; t += i/D1
MOVE D0, [A1] ; D0/result = *t
的复杂指令(使用复杂指令会使调试编译器更加困难),CPU可能没有这样的复杂指令,等等。
从基本位置“移动”和从基本位置“添加”之间有区别吗?
书中的描述可以说是措辞不佳。但是,我认为作者想描述一下上面所示的区别-索引(从基数“移出”)是一个表达式,而“加然后解除引用”是两个表达式。
这是关于编译器实现,而不是不是语言定义的,该区别也应该在书中明确指出。
答案 6 :(得分:2)
我对代码的某些编译器版本进行了测试,其中大多数都为我提供了两条指令的相同汇编代码(未经优化就在x86上进行了测试)。 有趣的是,gcc 4.4.7确实做到了,您提到的是: 示例:
诸如ARM或MIPS之类的其他语言有时也做同样的事情,但是我并没有全部测试。因此,看来它们是有区别的,但是gcc的更高版本“修复”了该错误。
答案 7 :(得分:-2)
这是C语言中使用的示例数组语法。
int a[10] = {1,2,3,4,5,6,7,8,9,10};