C具有对象和值。
int a[] = {1, 2 ,3};
我知道数组名称会转换为指针。经常使用的术语是它们会衰减到指针。
但是对我来说,pointer
是一个内存区域,将地址保存到另一个内存区域,因此:
int *p = a;
可以这样绘制:
----- -----
p ---------> a[0]. .....
----- -----
0x1 0x9
但是a
本身并不指向内存的另一个区域,而是内存本身的区域。因此,当编译器将其转换为指针时,会将它(例如p
)保存在内存中的某个位置或这是隐式转换吗?
“但是
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 = &x
将p
定义为对象。为其保留了内存,并为其分配了值&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年的标准中,完全不清楚引用函数调用返回的结构的数组成员名称的语义。C具有对象和值。
但是一个自身并不指向内存的另一个区域,它是内存本身的区域。因此,当编译器将其转换为指针时,会将它(如p)保存在内存中的某个位置还是隐式转换?
这是2011 ISO C标准所说的(6.3.2.1p3):