【问题标题】:what does compiler do with a[i] which a is array? And what if a is a pointer?编译器如何处理 a[i] 其中 a 是数组?如果 a 是一个指针呢?
【发布时间】:2011-01-05 14:18:31
【问题描述】:

c-faq 告诉我,当 a 是数组或指针时,编译器会做不同的事情来处理 a[i]。以下是来自 c-faq 的示例:

char a[] = "hello";
char *p = "world";

鉴于上面的声明,当编译器看到表达式 a[3] 时,它会发出代码以从位置“a”开始,移动三个过去,然后从那里获取字符。当它看到表达式 p[3] 时,它会发出代码从位置“p”开始,获取那里的指针值,将指针加 3,最后获取指向的字符。

但有人告诉我,在处理 a[i] 时,编译器倾向于将 a(它是一个数组)转换为指向数组的指针。所以我想看看汇编代码,看看哪个是对的。

编辑:

这是该声明的来源。 c-faq 并注意这句话:

a[i] 形式的表达式导致数组按照上述规则衰减为指针,然后像表达式 p[i] 中的指针变量一样被下标(尽管最终的内存访问会有所不同,“

我对此感到很困惑:既然 a 已经衰减为指针,那么他为什么要说“内存访问会有所不同?”

这是我的代码:

// array.cpp
#include <cstdio>
using namespace std;

int main()
{
    char a[6] = "hello";
    char *p = "world";
    printf("%c\n", a[3]);
    printf("%c\n", p[3]);
}

这是我使用 g++ -S array.cpp 得到的部分汇编代码

    .file   "array.cpp" 
    .section    .rodata
.LC0:
    .string "world"
.LC1:
    .string "%c\n"
    .text
.globl main
    .type   main, @function
main:
.LFB2:
    leal    4(%esp), %ecx
.LCFI0:
    andl    $-16, %esp
    pushl   -4(%ecx)
.LCFI1:
    pushl   %ebp
.LCFI2:
    movl    %esp, %ebp
.LCFI3:
    pushl   %ecx
.LCFI4:
    subl    $36, %esp
.LCFI5:
    movl    $1819043176, -14(%ebp)
    movw    $111, -10(%ebp)
    movl    $.LC0, -8(%ebp)
    movzbl  -11(%ebp), %eax
    movsbl  %al,%eax
    movl    %eax, 4(%esp)
    movl    $.LC1, (%esp)
    call    printf
    movl    -8(%ebp), %eax
    addl    $3, %eax
    movzbl  (%eax), %eax
    movsbl  %al,%eax
    movl    %eax, 4(%esp)
    movl    $.LC1, (%esp)
    call    printf
    movl    $0, %eax
    addl    $36, %esp
    popl    %ecx
    popl    %ebp
    leal    -4(%ecx), %esp
    ret 

我无法从上面的代码中弄清楚 a[3] 和 p[3] 的机制。如:

  • “hello”在哪里初始化?
  • 1819043176 美元是什么意思?可能是“hello”的内存地址(a的地址)?
  • 我确定“-11(%ebp)”表示 a[3],但为什么呢?
  • 在“movl -8(%ebp), %eax”中,指针 p 的内容存储在 EAX 中,对吗?那么 $.LC0 是指指针 p 的内容吗?
  • “movsbl %al,%eax”是什么意思?
  • 并且,请注意以下 3 行代码:
    movl $1819043176, -14(%ebp)
    movw $111, -10(%ebp)
    movl $.LC0, -8(%ebp)

    最后一个使用“movl”,但为什么没有覆盖-10(%ebp)的内容? (我现在知道分析器了 :),地址是递增的,“movl $.LC0 -8(%ebp) 只会覆盖 {-8, -7, -6, -5}(%ebp))

对不起,我对机制以及汇编代码完全感到困惑......

非常感谢您的帮助。

【问题讨论】:

  • 我认为你的这句话“编译器倾向于将一个(这是一个数组)转换为一个指向数组的指针”是不正确的。请告诉我这是谁对你说的?
  • +1,在询问之前自己尝试一下。
  • 不是指向数组的指针,指向字符的指针。
  • +1 用于检查 ASM。 你已经开始了,蚱蜢...
  • @Prasoon Saurrav 我找到了我的声明的来源,发现我的和他的之间存在细微差别。它在这里:c-faq.com/aryptr/aryptrequiv.html 注意这句话:“ a[i] 形式的表达式导致数组按照上面的规则衰减为指针,然后像表达式中的指针变量一样被下标p[i] (尽管最终的内存访问会有所不同,“我对此感到很困惑:既然 a 已经衰减为指针,那么他为什么要说“内存访问会有所不同?”

标签: c++ c arrays pointers assembly


【解决方案1】:

a 是一个指向字符数组的指针。 p 是一个指向 char 的指针,在这种情况下,它恰好指向一个字符串字面量。

movl    $1819043176, -14(%ebp)
movw    $111, -10(%ebp)

初始化堆栈上的本地“hello”(这就是它通过ebp 引用的原因)。由于“hello”有4个多字节,所以需要两条指令。

movzbl  -11(%ebp), %eax
movsbl  %al,%eax

引用a[3]:这两个步骤的过程是因为在访问ebp 引用的内存方面存在限制(我的 x86-fu 有点生疏)。

movl -8(%ebp), %eax 确实引用了p 指针。

LC0 引用一个“相对内存”位置:一旦程序加载到内存中,就会分配一个固定的内存位置。

movsbl %al,%eax 的意思是:“移动单个字节,降低”(给予或接受......我必须查一下......我在这方面有点生疏)。 al 表示来自寄存器eax 的一个字节。

【讨论】:

  • 所以你的意思是 a 也是一个指针?但是有人告诉我 a 的类型是数组。
  • @ibread:当它下降到汇编级别时,实际上并没有数组的概念,只是通过指针等访问内存。
  • ...以及为什么“不加评论地直接投票”??
  • @jldupont 但我认为最好将“a”称为包含“hello”的内存块的名称?毕竟,在汇编级别没有取消对“a”的引用。
  • @ibread:我不认为“开车投票”评论是针对您的。它是针对反对这个答案的人。
【解决方案2】:

进入语言方面,因为已经处理了汇编器方面:

注意这句话:“ a[i] 形式的表达式导致数组衰减为指针,遵循上述规则,然后像表达式 p[i] 中的指针变量一样被下标(尽管最终的内存访问会有所不同,“我对此感到很困惑:既然 a 已经衰减为指针,那么他为什么要说“内存访问会有所不同?

这是因为 衰减后,(现在是指针值)和指针的访问是相等的。但区别在于如何首先获得指针值。我们来看一个例子:

char c[1];

char cc;
char *pc = &cc;

现在,你有一个数组。这个数组除了一个字符外不占用任何存储空间! 没有为它存储指针。你有一个指向一个字符的指针。指针占用一个地址的大小,并且指针指向一个字符。现在让我们看看数组 case 获取指针值的过程:

c[0] = 'A';
// #1: equivalent: *(c + 0) = 'A';
// #2: => 'c' appears not in address-of or sizeof 
// #3: => get address of "c": This is the pointer value P1

指针大小写不同:

pc[0] = 'A';
// #1: equivalent: *(pc + 0) = 'A';
// #2: => pointer value is stored in 'pc'
// #3: => thus: read address stored in 'pc': This is the pointer value P1

如您所见,对于获取我们添加索引值所需的指针值的数组情况(在这种情况下是无聊的0),我们不需要从内存中读取,因为数组已经是需要的指针值。但是对于指针的情况,我们需要的指针值存储在指针中:我们需要从内存中读取来获取该地址。

在此之后,两者的路径相同:

// #4: add "0 * sizeof(char)" to P1. This is the address P2
// #5: store 'A' to address P2

这是为数组和指针大小写生成的汇编代码:

        add     $2, $0, 65  ; write 65 into r2
        stb     $2, $0, c   ; store r2 into address of c
# pointer case follows
        ldw     $3, $0, pc  ; load value stored in pc into r3
        add     $2, $0, 65  ; write 65 into r2
        stb     $2, $3, 0   ; store r2 into address loaded to r3

我们可以将65'A' 的 ASCII 码)存储在c 的地址中(当它是全局的时,在编译或链接时就已经知道了)。对于指针的情况,我们首先必须将它存储的地址加载到寄存器3,然后将65写入该地址。

【讨论】:

    【解决方案3】:

    虽然数组确实不是指针,但它们的行为非常相似。在这两种情况下,编译器都会在内部存储类型化元素的地址,并且在这两种情况下,都可以有一个或多个元素。

    在数组和指针中,当被 [] 运算符取消引用时,编译器通过将索引乘以数据类型的大小并将其添加到指针的地址或数组。

    指针和数组的根本区别在于数组本质上是一个引用。在合法的情况下,将指针初始化为null,或者改变指针存储的值,数组不能为null,也不能设置为其他数组;它们本质上是不能设置为 null 的常量指针。

    另外,数组可以分配在堆栈上,而指针则不可能(尽管指针可以设置为堆栈上的地址,但这会变得很丑)。

    【讨论】:

    • “我希望该结构不仅可以表征抽象对象,还可以描述可能从目录中读取的位集合。编译器可以在哪里隐藏语义要求的指向名称的指针?即使结构被认为更抽象,并且指针的空间可以以某种方式隐藏,我如何处理在分配复杂对象时正确初始化这些指针的技术问题,也许是一个指定结构包含包含任意深度结构的数组的结构? ...
    • ... 该解决方案构成了无类型 BCPL 和有类型 C 进化链中的关键跳跃。它消除了指针在存储中的物化,而是在数组名称时创建指针在表达式中被提及。在今天的 C 语言中仍然存在的规则是,当数组类型的值出现在表达式中时,它们会被转换为指向组成数组的第一个对象的指针。”(Dennis M. Ritchie 关于 C 的历史:@987654321 @)
    • (您的回答听起来像char p[1]; 需要超过1 字节的存储空间,因为您说编译器会将p 的地址存储到“常量指针”中,这确实是错误的) .
    • 你“数组本质上是一个引用”“不能设置为空的常量指针”维基百科“一旦创建了引用,以后就不能再引用另一个对象;它不能被重新定位。这个通常是用指针来完成的。引用不能为空,而指针可以" ...幽默比比皆是。
    【解决方案4】:

    这些定义看起来很相似,但实际上却大不相同。

    假设您的数组在函数内声明:
    void f()
    {
        char a[] = "hello";
        char *p = "world";
    }
    

    在第一种情况下,'a' 衰减为指向 STACK 上 6 个字符的 const 指针。 在第二种情况下,'p' 是一个非常量指针,指向 CONST 区域(数据段)中的 6 个字符。

    这样写是合法的:

    a[3] = 'L';
    

    但是

    p[3] = 'L';
    

    看起来正确,但会导致内存冲突,因为字符数组不在堆栈上,而是在只读部分中。

    此外,

    a++
    

    是非法的('a' 衰减为一个 const 指针,它是一个 r 值),但是

    p++
    

    是合法的(p 是一个左值)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2016-12-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多