数组语法与指针语法和代码生成?

时间:2018-07-17 05:00:07

标签: c arrays pointers pointer-arithmetic errata

在书中,"Understanding and Using C Pointers" by Richard Reese在第85页上说,

int vector[5] = {1, 2, 3, 4, 5};
     

vector[i]生成的代码与*(vector+i)生成的代码不同。符号vector[i]生成从位置向量开始移动的机器代码,从该位置 moving i位置开始使用机器代码。符号*(vector+i)生成从位置vector开始的机器代码,向该地址添加 i,然后使用该地址的内容。结果相同时,生成的机器代码不同。对于大多数程序员而言,这种差异几乎没有意义。

您可以看到excerpt here。这是什么意思?在什么情况下,任何编译器会为这两个生成不同的代码?从基本位置“移动”与从基本位置“添加”之间有区别吗?我无法在GCC上使用它-生成不同的机器代码。

8 个答案:

答案 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)

  
      
  1. 后缀表达式后跟方括号[]中的表达式是数组对象元素的下标名称。 下标运算符[]的定义是E1[E2](*((E1)+(E2))) 相同。由于适用于二进制+运算符的转换规则,如果E1是数组对象(等效于指向数组对象初始元素的指针),而E2是整数,E1[E2]指定E2的第E1个元素(从零开始计数)。
  2.   

此外,假设规则在此处适用-如果程序的行为相同,即使语义不相同。

答案 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.2C 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确实做到了,您提到的是: 示例:

C-Code

Assembly code

诸如ARM或MIPS之类的其他语言有时也做同样的事情,但是我并没有全部测试。因此,看来它们是有区别的,但是gcc的更高版本“修复”了该错误。

答案 7 :(得分:-2)

这是C语言中使用的示例数组语法。

int a[10] = {1,2,3,4,5,6,7,8,9,10};