数组到指针的衰减是否变为指针对象?

15
int a[] = {1, 2 ,3};

我理解数组名会被转换为指针。通常用的术语是它们“衰变”成指针。

然而,对我来说,一个指针是一个保存另一个内存区域地址的内存区域,所以:

int *p = a;

可以像这样绘制:

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

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


2
int *p = a 之后,p 指向数组 a 的第一个元素。如果您仅编写 a,则 a 会衰减为指向 a 的第一个元素的指针。 - Jabberwocky
2
a 没有被存储。这只是编译器的“技巧”。a 只是 &a[0] 的简写。 - Jabberwocky
1
这回答解决了你的问题吗? 在 C 语言中,数组是指针还是用作指针? - Adam
1
基本上,“数组名用作指针”的概念在编译期间被替换为硬编码地址,因为编译器知道所有变量的地址。同样地,在机器代码中不存在变量名,因为它们都被替换为寄存器、堆栈分配或地址访问。 - Lundin
1
我喜欢用类比的方式来思考它。1是一个int值,但它没有地址。&i是一个指针值,但它没有地址。数组名的衰减会产生一个指针值,但这个指针值没有地址。 - Ian Abbott
显示剩余3条评论
4个回答

12

C有对象和值。

值是一个抽象的概念,通常表示某种含义,比如数学上的值。数字有像4、19.5或-3这样的值。地址的值是内存中的位置。结构体的值是其成员作为一个整体考虑的值。

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

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

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

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

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

从技术上讲,a 不是对象本身,它是指定对象的标识符。 - Ian Abbott
2
@IanAbbott:从技术上讲,a是对象,“a”是标识符,就像狗是动物,“狗”是一个单词一样。我们写“狗有四条腿”,而不是“被称为狗的东西有四条腿”。也就是说,当我们在句子中使用名称时,名称通常指代所命名的事物;它不是对名称作为字符序列的引用。 - Eric Postpischil
我承认这一点。在某些地方,标准谈论的是“被声明为”对象的标识符,而在其他地方则谈论的是“指定”对象的标识符。 - Ian Abbott
1
@ryyker:我使用分号有两种方式(除了数学/技术用途之外):将两个独立的从句连接起来,这两个从句在不同的方式下表达相同的内容;或者作为列表中已经有逗号的项目的逗号。这是第一种用法:对象是“执行环境中的数据存储区域,其内容可以表示值。”因此,int a[10]定义了一个对象,而该对象是十个int的内存。对象和十个int的内存是同一件事情。 - Eric Postpischil
1
@ryyker: 有些来源对从句之间的关系要求不那么严格,允许仅仅是“密切相关”的从句。我会尽量保留它用于当它们有相当程度的重叠时。 - Eric Postpischil
显示剩余2条评论

9
“但是,a 本身并没有指向内存的另一个区域,而是内存区域本身。所以,当编译器将其转换为指针时,它会在内存中保存它(例如 p)还是隐式转换?”
这是一种隐式转换。编译器不会在内存中实现创建一个单独的指针对象(您可以用任何方式分配其他内存地址),以保存第一个元素的地址。
标准规定(强调我的部分):
“除了作为 sizeof 运算符、一元 & 运算符的操作数或用于初始化数组的字符串文字之外,具有类型“type 的数组”的表达式被转换为具有类型“指向 type 的指针”的表达式,该表达式指向数组对象的初始元素,并且不是 lvalue。如果数组对象具有寄存器存储类,则行为未定义。” 来源: ISO/IEC 9899:2018 (C18), 6.3.2.1/4
数组被转换为指针类型的表达式,它不是 lvalue。
编译器只是将 a 评估为 &a[0](指向 a[0] 的指针)。
“我了解数组名称会被转换为指针。”
数组并不总是转换为指向其第一个元素的指针。请看上面引用的第一部分。例如,当作为 &a 使用时,a 不会衰减为指向其第一个元素的指针。相反,它会获得一个指向整个数组 int (*)[3] 的指针。

2
但是a本身并没有指向另一个内存区域,它就是内存区域本身。因此,当编译器将其转换为指针时,它会将其保存在内存中的某个位置,还是隐式转换呢?
从逻辑上讲,这是一种隐式转换 - 实现没有要求为指针实现永久储存空间。
在实现方面,这取决于编译器。例如,下面是一个简单的代码片段,创建一个数组并打印其地址:
#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变量 - 它全部用当前帧指针的偏移量表示。

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

同样,这就是Red Hat上运行的x86-64上的gcc的方式。在不同体系结构上的不同编译器将以不同的方式执行。


1
以下是2011年ISO C标准的说法(6.3.2.1p3):
除了作为sizeof运算符或一元&运算符的操作数,或者是用于初始化数组的字符串字面量之外,具有“类型为类型的数组”类型的表达式将被转换为具有“指向数组对象的初始元素”的“类型为类型的指针”的表达式,并且不是左值。如果数组对象具有寄存器存储类,则行为未定义。
此处标准使用了“转换”一词,但它并不是通常意义上的转换。
通常,转换(无论是隐式转换还是由强制类型转换操作符指定的显式转换)将某个类型的表达式作为其操作数,并产生目标类型的结果。结果取决于操作数的值。在大多数或所有情况下,您都可以编写执行相同操作的函数。(请注意,隐式和显式转换执行相同的操作;数组到指针的转换是隐式的事实并不特别相关。)
在上述的数组转指针转换中,并非如此。数组对象的值由其元素的值组成--该值不包含有关存储数组的地址的任何信息。
也许更清楚地将其称为"调整"而不是"转换"。标准使用"调整"一词来指代将数组类型的参数在编译时转换为指针类型的过程。例如,这个:
void func(int notReallyAnArray[42]);

really means this:

void func(int *notReallyAnArray);

“转换”数组表达式为指针表达式是一种类似的事情。
另一方面,“转换”一词并不仅仅意味着类型转换。例如,标准在讨论printf格式字符串(“%d”和“%s”是“转换说明符”)时使用“转换”这个词。
一旦你理解所描述的“转换”实际上是一个编译时调整,将一种表达式转换为另一种表达式(而不是值),那么它就不会那么令人困惑了。
离题:
关于数组到指针转换的标准描述有一个有趣的地方,它谈到了一个数组类型的表达式,但行为取决于“数组对象”的存在。非数组类型的表达式不一定有与之相关联的对象(即不一定是左值)。但每个数组表达式都是左值。在一种情况下(非值联合或结构表达式的数组成员名称,特别是当函数返回结构值时),语言必须更新以保证这种情况始终存在,并且需要在2011年标准中引入“临时生存期”的概念。在1990年和1999年的标准中,提到引用由函数调用返回的结构的数组成员名称的语义并不明确。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接