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

Eva*_*oll 54 c arrays pointers pointer-arithmetic errata

Richard Reese所着的"理解和使用C指针"一书中,它在第85页说

int vector[5] = {1, 2, 3, 4, 5};
Run Code Online (Sandbox Code Playgroud)

生成的代码与生成的代码vector[i]不同*(vector+i).该表示法vector[i]生成从位置向量开始的机器代码,从该位置移动 i位置,并使用其内容.符号*(vector+i)生成的机器代码从位置开始vector,添加 i到地址,然后使用该地址的内容.结果相同,生成的机器代码不同.这种差异对大多数程序员来说很少有意义.

你可以在这里看到摘录.这段经文是什么意思?在什么情况下,任何编译器都会为这两个编译器生成不同的 "移动"与基础之间是否存在差异,而"添加"基数是否存在差异?我无法让它在GCC上工作 - 生成不同的机器代码.

M.M*_*M.M 95

引用是错的.很遗憾这种垃圾在这十年中仍然存在.事实上,C标准定义x[y]*(x+y).

关于页面后面左值的部分也是完全错误的.

恕我直言,使用本书的最佳方法是将其放入回收箱或刻录.

  • 如果一个联合包含一个数组,gcc将为`theUnion.anArray [i]`和`*(theUnion.anArray + i)`生成不同的机器代码.只有在前一种情况下,gcc才能足够聪明地认识到访问`anArray [i]`可能会影响联盟及其他成员. (5认同)
  • 我不会说这是_wrong_,但不完整.问题是,_some_编译器可能真的为`x [y]`而不是`*(x + y)`生成不同的机器代码(实际上,同样适用于`*(y + x)`和`y [x]`) .IOW,如果我们在整个引用前加上"On some compilers ...",它实际上是正确的. (3认同)
  • 日常的方法是使用这本书作为一杯热咖啡的立场. (3认同)
  • @NicHartley:GCC 使用代码生成/执行模型来处理“aggregate.member”或“aggregate.member[index]”或“aggregate.member[index1][index2]”等形式的左值表达式。作为对聚合的操作,但无法将指向聚合成员的指针识别为与聚合有任何关系,即使在“立即”使用该指针的情况下也是如此。该标准将对此类构造的支持视为实现质量问题,而 gcc 是围绕该标准允许低质量实现这一事实而设计的。 (2认同)

Ant*_*ala 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]);
}
Run Code Online (Sandbox Code Playgroud)

而且ex2.c,

% cat ex2.c
#include <stdio.h>

int main (void) {
    int vector[5] = { 1, 2, 3, 4, 5 };
    printf("%d\n", *(vector + 3));
}
Run Code Online (Sandbox Code Playgroud)

我将两者编译成汇编,并显示生成的汇编代码的差异

% 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:
Run Code Online (Sandbox Code Playgroud)

QED


C标准非常明确地陈述(C11 n1570 6.5.2.1p2):

  1. 后缀表达式后跟方括号中的表达式[]是数组对象元素的下标名称.下标操作符的定义[]E1[E2]相同(*((E1)+(E2))).由于适用于二元+运算符的转换规则,if E1是一个数组对象(等效地,指向数组对象的初始元素的指针)并且E2是一个整数,因此E1[E2]指定E2-th元素E1(从零开始计数).

此外,as-if规则适用于此 - 如果程序的行为相同,即使语义相同,编译器也可以生成相同的代码.

  • 这假设了特定的编译器和优化,但它通常与我做的一样.我并不满意,因为这样的测试会根据架构的字节代码对基于语言的语言做出假设. (4认同)
  • "我感到不满意,因为这样的测试会根据架构的字节代码对基于语言代码的语言做出假设",重点是我们正在讨论生成的程序集,这是一个特定于实现的程序集.关于这种主张,你唯一能做的就是 - 除了注意标准规定的可观察行为的等效性外 - 还要看各种编译器的输出. (3认同)
  • 有趣的说明:根据引用和实践,"我[矢量]"也有效,尽管在大多数情况下这样做会很糟糕.你不能从位置`i`开始并从这个位置移动`vector`位置. (2认同)

Ste*_*mit 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到地址,然后使用该地址的内容.

说基本相同的事情.

他的语言与旧C FAQ列表的问题6.2中的语言非常类似:

...当编译器看到表达式时a[3],它会发出代码从位置" a"开始,向后移动三个,然后在那里获取字符.当它看到表达式时p[3],它会发出代码从位置" p" 开始,在那里获取指针值,向指针添加三个,最后获取指向的字符.

但当然这里的关键区别在于它a是一个数组而且p是一个指针.常见问题列表不是谈论a[3]*(a+3),而是关于a[3](或*(a+3))在哪里a是一个数组,而不是p[3](或*(p+3))where p是一个指针.(当然这两种情况会产生不同的代码,因为数组和指针是不同的.正如FAQ列表所解释的那样,从指针变量中获取地址与使用数组的地址根本不同.)


Jim*_*myB 6

认为原始文本可能指的是一些编译器可能会或可能不会执行的一些优化.

例:

for ( int i = 0; i < 5; i++ ) {
  vector[i] = something;
}
Run Code Online (Sandbox Code Playgroud)

for ( int i = 0; i < 5; i++ ) {
  *(vector+i) = something;
}
Run Code Online (Sandbox Code Playgroud)

在第一种情况下,优化编译器可以检测到数组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.
}
Run Code Online (Sandbox Code Playgroud)

它甚至可以在可用的情况下使用目标CPU的指针后增量指令.

对于第二种情况,编译器"更难"看到通过某些"任意"指针算术表达式计算的地址显示了在每次迭代中单调推进固定量的相同属性.因此,可能无法((void*)vector+i*sizeof(int))在每次迭代中找到使用额外乘法的优化和计算.在这种情况下,没有(临时)指针被"移动"但只重新计算了一个临时地址.

但是,该声明可能并不普遍适用于所有版本的所有C编译器.

更新:

我检查了上面的例子.似乎没有启用优化,至少gcc-8.1 x86-64为第二个(指针 - 算法)形式生成比第一个(数组索引)更多的代码(2个额外指令).

请参阅:https://godbolt.org/g/7DaPHG

然而,任何优化接通(-O... -O3)生成的代码是用于两个相同的(长度).


sup*_*cat 6

标准指定arr[i]when 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);
}
Run Code Online (Sandbox Code Playgroud)

GCC为test1生成的代码将假设arr [1] [i]和arr [0] [j]不能别名,但生成的test2代码将允许指针算法访问整个数组.另一方面,gcc将认识到在utest1中,左值表达式uh [i]和uw [j]都访问同一个联合,但它不够复杂,不能注意到*(u.h + i)和*(u.w + j)中的相同utest2.