【问题标题】:Is the array to pointer decay changed to a pointer object?指向指针衰减的数组是否更改为指针对象?
【发布时间】:2020-10-02 08:08:16
【问题描述】:
int a[] = {1, 2 ,3};

我了解数组名称会转换为指针。经常使用的一个术语是它们衰减为指针。

但对我来说,pointer 是一个内存区域,用于保存另一个内存区域的地址,所以:

int *p = a;

可以这样画:

-----              -----
  p    --------->  a[0].  .....
-----              -----
 0x1                0x9

但是a 本身并没有指向另一个内存区域,它是内存区域本身。 因此,当编译器将其转换为指针时,它是否将它(如p)保存在内存中的某处或 是隐式转换吗?

【问题讨论】:

  • 在int *p = a之后,p指向数组a的第一个元素。如果你只写a,a 会衰减为指向a 的第一个元素的指针
  • a 未存储。这只是编译器的“技巧”。 a 只是 &a[0] 的简写。
  • 这能回答你的问题吗? In C, are arrays pointers or used as pointers?
  • 基本上“用作指针的数组名”在编译期间被替换为硬编码地址,因为编译器知道所有变量的地址。同样,机器代码中不存在变量名,因为它们都被寄存器、堆栈分配或地址访问所取代。
  • 我喜欢类推。 1 是一个 int 值,但它没有地址。 &i 是一个指针值,但它没有地址。数组名的衰减产生一个指针值,但指针值没有地址。

标签: c arrays pointers memory language-lawyer


【解决方案1】:

C 有对象和值。

值是一个抽象概念——它是某种含义,通常是数学含义。数字具有 4、19.5 或 -3 等值。地址的值是内存中的位置。结构的值是被视为聚合的成员的值。

值可以用在表达式中,例如3 + 4*5。在表达式中使用值时,它们在 C 使用的计算模型中没有任何内存位置。这包括地址值,例如&x + 3 中的&x。

对象是内存区域,其内容可以表示值。声明 int *p = &x 将 p 定义为一个对象。为它保留了内存,并为其分配了值&x。

对于以int a[10] 声明的数组,a 是一个对象;这是为 10 个int 元素保留的所有内存。

当在表达式中使用a 时,除了作为sizeof 或一元& 的操作数之外,表达式中使用的a 会自动转换为其第一个元素的地址&a[0] .这是一个值。没有为它保留内存;它不是一个对象。它可以在表达式中用作值,而无需为其保留任何内存。请注意,实际的a 不会以任何方式转换;当我们说a 被转换为指针时,我们的意思只是生成了一个地址以供在表达式中使用。

以上所有内容都描述了 C 使用的计算模型中的语义,这是一些抽象计算机的语义。实际上,当编译器使用表达式时,它通常使用处理器寄存器来操作这些表达式中的值。处理器寄存器是内存的一种形式(它们是设备中保留值的东西),但它们不是我们通常所说的“主内存”,当我们没有限定地谈论“内存”时。但是,编译器也可能根本没有任何内存中的值,因为它在编译期间部分或全部计算表达式,因此在程序执行时实际计算的表达式可能不包括名义上存在的所有值用 C 语言编写的表达式。编译器也可能在主存中有值,因为计算复杂的表达式可能会溢出处理器寄存器中可行的内容,因此表达式的部分必须临时存储在主存中(通常在硬件堆栈上)。

【讨论】:

  • 从技术上讲,a 不是对象本身,它是指定对象的标识符。
  • @IanAbbott:从技术上讲,a 是对象,“a”是标识符,就像狗是动物,“狗”是一个词一样。我们写的是“狗有四条腿”,而不是“所谓的狗有四条腿”。也就是说,当我们在句子中使用名称时,名称一般是指被命名的事物;它不是对名称作为字符序列的引用。
  • 我承认这一点。在某些地方,该标准谈论的是“声明为”对象的标识符,尽管在其他地方它谈论的是“指定”对象的标识符。
  • @ryyker:我以两种方式使用分号(在数学/技术用途之外):连接两个独立的子句,它们以不同的方式表达大致相同的事情,或者在项目列表中作为逗号已经有了自己的逗号。这是第一个用途:对象是“执行环境中的数据存储区域,其内容可以表示值”。所以int a[10] 定义了一个对象,该对象是十个int 的内存。十个int的对象和内存是一回事。
  • @ryyker:[有些消息来源不那么严格](Some sources are less strict about the relationship between the clauses.) 关于前一种用法,允许仅“密切相关”的子句。我尝试保留它,以备它们大量重叠时使用。
【解决方案2】:

“但是a本身并没有指向另一个内存区域,它是内存区域本身。

“所以当编译器将它转换为指针时,它是把它(如p)保存在内存中的某个地方还是隐式转换?”

这是一种隐式转换。编译器没有实现在内存中创建单独的指针对象(您可以以任何方式分配不同的内存地址)来保存第一个元素的地址。

标准状态(强调我的):

"除非是 sizeof 运算符的操作数,或一元 & 运算符,或者是用于初始化数组的字符串字面量,否则类型为 "array of type" 的表达式将转换为类型为 " 的表达式指向类型的指针”,指向数组对象的初始元素,不是左值。如果数组对象有寄存器存储类,则行为未定义。”

来源:ISO/IEC 9899:2018 (C18),6.3.2.1/4

数组被转换为指针类型的表达式,它不是lvalue。

编译器只是将a 评估为&a[0](指向a[0] 的指针)。


“我知道数组名被转换为指针。”

数组并不总是转换为指向其第一个元素的指针。看看上面引用的第一部分。 F.e.当用作&a 时,a 不会衰减为指向其第一个元素的指针。相反,它获得了指向整个数组 int (*)[3] 的指针。

【讨论】:

    【解决方案3】:

    但是 a 本身并不指向另一个内存区域,它是内存区域本身。那么当编译器将其转换为指针时,它是把它(如 p)保存在内存中的某处还是隐式转换?

    从逻辑上讲,这是一种隐式转换 - 不要求实现实现指针的永久存储。

    在实现方面,这取决于编译器。例如,下面是一段简单的代码,它创建了一个数组并打印了它的地址:

    #include <stdio.h>
    
    int main( void )
    {
      int arr[] = { 1, 2, 3 };
      printf( "%p", (void *) arr );
      return 0;
    }
    

    当我在 Red Hat 系统上使用 gcc 为 x86-64 编译它时,我得到以下机器代码:

    GAS LISTING /tmp/ccKF3mdz.s             page 1
    
    
       1                    .file   "arr.c"
       2                    .text
       3                    .section    .rodata
       4                .LC0:
       5 0000 257000        .string "%p"
       6                    .text
       7                    .globl  main
       9                main:
      10                .LFB0:
      11                    .cfi_startproc
      12 0000 55            pushq   %rbp
      13                    .cfi_def_cfa_offset 16
      14                    .cfi_offset 6, -16
      15 0001 4889E5        movq    %rsp, %rbp
      16                    .cfi_def_cfa_register 6
      17 0004 4883EC10      subq    $16, %rsp
      18 0008 C745F401      movl    $1, -12(%rbp)
      18      000000
      19 000f C745F802      movl    $2, -8(%rbp)
      19      000000
      20 0016 C745FC03      movl    $3, -4(%rbp)
      20      000000
      21 001d 488D45F4      leaq    -12(%rbp), %rax
      22 0021 4889C6        movq    %rax, %rsi
      23 0024 BF000000      movl    $.LC0, %edi
      23      00
      24 0029 B8000000      movl    $0, %eax
      24      00
      25 002e E8000000      call    printf
      25      00
      26 0033 B8000000      movl    $0, %eax
      26      00
      27 0038 C9            leave
      28                    .cfi_def_cfa 7, 8
      29 0039 C3            ret
      30                    .cfi_endproc
      31                .LFE0:
      33                    .ident  "GCC: (GNU) 7.3.1 20180712 (Red Hat 7.3.1-6)"
      34                    .section    .note.GNU-stack,"",@progbits
    

    第 17 行通过从堆栈指针中减去 16 为数组分配空间(是的,数组中只有 3 个元素,应该只需要 12 个字节 - 我会让更熟悉 x86_64 架构的人解释原因,因为我会弄错)。

    第 18、19 和 20 行初始化数组的内容。请注意,机器代码中没有 arr 变量 - 这一切都是根据当前帧指针的 offset 来完成的。

    第 21 行是发生转换的地方 - 我们将数组第一个元素的有效地址(即存储在 %rbp 寄存器中的地址减去 12)加载到 %rax 寄存器中。然后将该值(连同格式字符串的地址)传递给printf。请注意,此转换的结果不会存储在寄存器以外的任何位置,因此下次写入 %rax 时它将丢失 - IOW,没有为其预留永久存储空间,就像设置存储空间一样除了数组内容。

    再一次,这就是在 x86-64 上运行的 Red Hat 中的 gcc 的做法。不同架构上的不同编译器会做不同的事情。

    【讨论】:

      【解决方案4】:

      这是 2011 ISO C 标准 (6.3.2.1p3) 所说的:

      除非它是 sizeof 运算符的操作数,或者 一元 &amp; 运算符,或者是用于初始化 数组,类型为“type 的数组”的表达式被转换为 一个类型为“pointer to type”的表达式,它指向初始 数组对象的元素并且不是左值。如果数组对象 有注册存储类,行为未定义。

      标准在这里使用“转换”一词,但这不是通常的转换。

      通常,转换(隐式转换或由强制转换运算符指定的显式转换)将某种类型的表达式作为其操作数,并产生目标类型的结果。结果由操作数的值决定。在大多数或所有情况下,您都可以编写一个执行相同操作的函数。 (请注意,隐式和显式转换执行相同的操作;数组到指针的转换是隐式的这一事实并不特别相关。)

      在上述数组到指针转换的情况下,情况并非如此。数组对象的值由其元素的值组成——该值不包含有关存储数组的地址的信息。

      将其称为调整而不是转换可能更清楚。该标准使用“调整”一词来指代数组类型参数到指针类型参数的编译时转换。例如,这个:

      void func(int notReallyAnArray[42]);
      

      真的是这个意思:

      void func(int *notReallyAnArray);
      

      数组表达式到指针表达式的“转换”是类似的事情。

      另一方面,“转换”一词仅表示类型转换。例如,标准在讨论printf 格式字符串时使用“转换”一词("%d" 和"%s" 是转换规范)。

      一旦你理解所描述的“转换”实际上是编译时调整,将一种表达式转换为另一种表达式(不是值),就不会那么混乱了。

      题外话:

      标准对数组到指针转换的描述有一个有趣的地方是它讨论了一个数组类型的表达式,但其行为取决于“数组对象”的存在。非数组类型的表达式不一定有与之关联的对象(即,它不一定是左值)。但是每个数组表达式都是一个左值。在一种情况下(非值联合或结构表达式的数组成员的名称,特别是当函数返回结构值时),必须更新语言以确保始终如此,以及 临时寿命必须在 2011 年标准中引入。引用函数调用返回的结构的数组成员名称的语义在 1990 年和 1999 年的标准中完全不明确。

      【讨论】:

        猜你喜欢
        • 2011-11-14
        • 2016-10-25
        • 2021-12-01
        相关资源
        最近更新 更多