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

问题描述 投票:54回答:8

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

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

通过vector[i]生成的代码是从由*(vector+i)生成的代码不同。符号vector[i]生成开始于位置矢量,从这个位置移动i位置,并使用其内容的机器代码。符号*(vector+i)生成开始于位置vector机器代码,添加i到地址,然后在该地址使用的内容。虽然结果是一样的,所生成的机器代码是不同的。这种差异是很少的意义大多数程序员。

你可以看到excerpt here。这是什么意思通道?在什么情况下会任何编译器为这两个不同的代码?有没有之间的“移动”,从基地,并“添加”立足区别吗?我无法得到这个在GCC的工作 - 产生不同的机器代码。

c arrays pointers pointer-arithmetic errata
8个回答
94
投票

该报价是错误的。漂亮的悲剧,这样的垃圾仍然是发表在这十年。事实上,C标准x[y]定义为*(x+y)

关于左值的部分后,页面上是完全彻底错了。

恕我直言,使用这本书的最好的办法是把它放到回收箱或刻录。


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:

证明完毕


C标准非常明确地指出(C11 n1570 6.5.2.1p2)

  1. 后缀表达式,随后在方括号[]一个表达式是一个数组对象的元素的下标指定。下标操作[]的定义是,E1[E2]相同(*((E1)+(E2)))。因为应用到二进制+操作者,如果E1是一个数组对象(等同于一个指针数组对象的初始元件)和E2是整数的转换规则的,E1[E2]指定E2的计数的E1个元素(从零)。

此外,作为-如果规则适用于这里 - 如果程序的行为是相同的,编译器可以生成即使语义是不一样的相同的代码。


19
投票

所引用的通道是十分错误的。表达vector[i]*(vector+i)是完全相同的,可以预期的情况下都生成相同的代码。

表达式vector[i]*(vector+i)根据定义相同。这是C编程语言的一个中心环节和根本属性。任何一个合格的C程序员理解这一点。任何作者题为理解和使用C指针,指向必须了解这本书的。 C编译器的任何笔者也明白这一点。这两个片段会产生相同的代码不是偶然的,而是因为几乎所有的C编译器会在效果上一种形式几乎立即转化为其他的,使得由它得到它的代码生成阶段的时候,它甚至不会知道其形式已经初步使用。 (如果C编译器不断生成,而不是vector[i] *(vector+i)显著不同的代码,我会很惊讶。)

而事实上,引用文本自相矛盾。正如你提到的,这两个通道

符号qazxsw POI生成开始于位置qazxsw POI,从该位置移动vector[i]位置,并使用其内容的机器代码。

符号vector生成开始于位置i机器代码,添加*(vector+i)到地址,然后在该地址使用的内容。

说基本上是一回事。

他的语言是极其相似的是,在老vectori

...当编译器看到表达式question 6.2,它生成代码的位置“C FAQ list”开始,动议三项过去吧,这里面取的字符。当它看到表达a[3],它发射的代码在位置“a”开始,取该指针值在那里,添加三个指针,最后取的字符指向。

但是,当然,这里的关键区别在于p[3]是一个数组,p是一个指针。常见问题列表被说不是ap,而是约a[3](或*(a+3))其中a[3]是一个数组,与*(a+3)(或a)其中p[3]是一个指针。 (当然那些2案件产生不同的代码,因为数组和指针是不同的。由于FAQ列表介绍,从指针变量获取的地址是从使用阵列的地址根本不同的。)


6
投票

我认为原文可能是指一些优化一些编译器可能会或可能不会进行。

例:

*(p+3)

p

在第一种情况下,一个优化编译器可以检测到该阵列for ( int i = 0; i < 5; i++ ) { vector[i] = something; } 由元件遍历元件并因此产生类似

for ( int i = 0; i < 5; i++ ) {
  *(vector+i) = something;
}

它甚至可以在那里获得使用目标CPU的指针递增后的指令。

对于第二种情况下,它是“较硬”的编译器可以看出这的Via一些“任意”指针算术表达式计算的地址表示的在每次迭代单调前进固定的量相同的属性。它可能因此找不到优化,在使用一个额外的乘法每次迭代计算vector。在这种情况下,有没有(临时)指针被“感动”,但只是暂时的地址重新计算。

然而,该声明可能不普遍保持在所有版本中所有的C编译器。

更新:

我检查上述示例。似乎没有在启用优化至少GCC-8.1的x86-64生成用于第二(指针arithmethics)形式比所述第一(数组索引)更多的代码(2个额外的指令)。

请参阅:void* tempPtr = vector; for ( int i = 0; i < 5; i++ ) { *((int*)tempPtr) = something; tempPtr += sizeof(int); // _move_ the pointer; simple addition of a constant. }

然而,任何优化接通(((void*)vector+i*sizeof(int)) ... https://godbolt.org/g/7DaPHG)生成的代码是用于两个相同的(长度)。


6
投票

该标准规定-O的行为时-O3是一个数组对象为等同于分解arr[i]的指针,增加arr,和解除引用的结果。虽然行为将在所有标准定义的情况下等效,也有一些情况下,编译器处理措施有效,即使标准确实需要它,arr的处理和i可以作为一个后果是不同的。

例如,给定

arrayLvalue[i]

GCC对TEST1生成的代码将假定ARR [1] [i]和ARR [0] [j]的不能别名,但对于TEST2生成的代码将允许指针运算访问整个阵列,在另一面,GCC将认识到,在utest1,左值表达式呃[i]和UW [J]都访问相同的工会,但它不是足够发达,通知一下*相同(u.h + i)和*(u.w + J)在utest2。


3
投票

让我试试“在狭隘”来回答这个问题(别人已经描述了为什么说明“原样”有点欠缺/不全/误导):

在什么情况下会任何编译器为这两个不同的代码?

A“不是非常优化”编译器可能在几乎任何上下文生成不同的代码,因为,在解析,有一个区别:*(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); } 是两个表达式(添加到一个整数指针,则解除引用它)。当然,这不是很难认识到这一点(甚至在解析),并把它一样,但是,如果你正在写一个简单/编译器速度快,那么你避免把“太聪明了进去。”举个例子:

x[y]

编译器,在解析*(x+y),看到“索引”,并可能产生类似(对于某些68000-如CPU):

char vector[] = ...;
char f(int i) {
    return vector[i];
}
char g(int i) {
    return *(vector + i);
}

OTOH,对于f(),编译器看到两件事情:首先是(的“事情还在后头”)提领,然后整数指针/阵列的加入,所以是不是非常优化,它可以结束:

MOVE D0, [A0 + D1] ; A0/vector, D1/i, D0/result of function

显然,这是非常依赖于实现,一些编译器使用复杂的指令作为用于g()也可能不喜欢(使用复杂的指令使得它更难调试编译器)时,CPU可能没有这样复杂的指令,等等。

有没有之间的“移动”,从基地,并“添加”立足区别吗?

书中的描述可以说是没有得到很好的措辞。但是,我觉得作者想描述上面显示的区别 - 索引(从基地“移动”)是一个表达,而“增加,然后解引用”两种表达方式。

这是有关编译器实现,而不是语言定义,这应该也已经明确在书中指出的区别。


2
投票

我测试的代码编译器的一些变化,其中大多数是给我两个指令相同的汇编代码(用于x86测试没有优化)。有意思的是,海湾合作委员会4.4.7不正是,你所提到的:例如:

MOVE A1, A0 ; A1/t = A0/vector ADD A1, D1 ; t += i/D1 MOVE D0, [A1] ; D0/result = *t

f()

像ARM或MIPS其他汉语语言正在做有时是相同的,但我didn`t测试这一切。如此看来他们是有差别,但GCC的更高版本的“固定”这个bug。


-2
投票

这是一个样本阵列的语法中使用C.

C-Code
© www.soinside.com 2019 - 2024. All rights reserved.