指向指针衰减的数组是否已更改为指针对象?

问题描述 投票:5回答:4
int a[] = {1, 2 ,3};

我知道数组名称会转换为指针。经常使用的术语是它们会衰减到指针。

但是对我来说,pointer是一个内存区域,将地址保存到另一个内存区域,因此:

int *p = a;

可以这样绘制:

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

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

c pointers
4个回答
7
投票

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

“因此,当编译器将其转换为指针时,会将它(例如p)保存在内存中的某个位置,还是隐式转换?”

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

标准状态(强调我的::)>

“除非它是sizeof运算符的操作数或一元&运算符,或者是用于初始化数组的字符串文字,否则将类型为” array of type“的表达式转换为类型为” pointer“的表达式“键入”,指向数组对象的初始元素,并且不是左值

。如果数组对象具有寄存器存储类,则行为未定义。”

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

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

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


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

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

C具有对象和值。

值是一个抽象概念,它是某种含义,通常是数学上的。数字的值如4、19.5或-3。地址的值就是内存中的位置。结构的值是其成员的值,这些值被视为集合。

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

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

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

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

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

但是一个自身并不指向内存的另一个区域,它是内存本身的区域。因此,当编译器将其转换为指针时,会将它(如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做到这一点的方式。不同体系结构上的不同编译器将以不同的方式进行操作。

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

除了它是sizeof

运算符的操作数,还是 一元&运算符,或者是用于初始化 数组,将类型为“ type的数组”的表达式转换为 类型为“指向type的指针”的表达式,它指向初始 数组对象的元素,不是左值。如果数组对象 具有寄存器存储类,行为未定义。

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

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

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

将其称为adjustment

而不是conversion可能更清楚。该标准使用单词“ adjusted”来表示将数组类型的参数编译为指针类型的参数的编译时转换。例如,此:
void func(int notReallyAnArray[42]);

真的是这样:

void func(int *notReallyAnArray);

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

另一方面,单词“ conversion”不是only

表示类型转换。例如,当讨论printf格式字符串("%d""%s"转换规范)时,该标准使用了“转换”一词。

[一旦您了解所描述的“转换”实际上是编译时的调整,即将一​​种表达式转换为另一种表达式(而不是值),则不会造成混乱。

DIGRESSION:

关于标准的数组到指针转换的描述的一件有趣的事情是,它谈论的是数组类型的expression

,但是其行为取决于“数组对象”的存在。非数组类型的表达式不一定具有与之关联的对象(即,它不一定是左值)。但是每个数组表达式is是一个左值。在一种情况下(非值并集或结构表达式的数组成员的名称,特别是当函数返回结构值时),必须更新语言以保证始终如此,并且 [临时寿命必须在2011年标准中引入。在1990年和1999年的标准中,完全不清楚引用函数调用返回的结构的数组成员名称的语义。

8
投票

C具有对象和值。


2
投票

但是一个自身并不指向内存的另一个区域,它是内存本身的区域。因此,当编译器将其转换为指针时,会将它(如p)保存在内存中的某个位置还是隐式转换?


0
投票

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

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