将指向数组的指针转换为指针

考虑以下C代码:

int arr[2] = {0, 0}; int *ptr = (int*)&arr; ptr[0] = 5; printf("%d\n", arr[0]); 

现在,很明显代码在常见编译器上打印5 。 但是,有人可以找到C标准中的相关部分,指出代码确实有效吗? 或者是代码未定义的行为?

我基本上要问的是,为什么&arr在转换为void *&arr一样转换为void * ? 因为我相信代码相当于:

 int arr[2] = {0, 0}; int *ptr = (int*)(void*)&arr; ptr[0] = 5; printf("%d\n", arr[0]); 

我在这里思考这个问题时发明了这个例子: 数组的指针到数组的重叠 ……但这显然是一个截然不同的问题。

对于工会和结构,参见 ISO 9899:2011§6.7.2.1/ 16f:

16联盟的大小足以容纳其中最大的成员。 最多一个成员的值可以随时存储在union对象中。 指向适当转换的union对象的指针指向其每个成员(或者如果成员是位字段,则指向它所在的单元),反之亦然。

17在结构对象中,非位字段成员和位字所在的单元具有按声明顺序增加的地址。 指向适当转换的结构对象的指针指向其初始成员(或者如果该成员是位字段,则指向它所在的单元),反之亦然。 结构对象中可能存在未命名的填充,但不是在其开头。

对于数组类型,情况稍微复杂一些。 首先,观察数组是什么,来自ISO 9899:2011§6.2.5/ 20:

数组类型描述了具有特定成员对象类型的连续分配的非空对象集,称为元素类型 。 只要指定了数组类型,元素类型就应该是完整的。 数组类型的特征在于它们的元素类型和数组中元素的数量。 数组类型据说是从其元素类型派生的,如果它的元素类型是T ,则数组类型有时称为“ T的数组”。 从元素类型构造数组类型称为“数组类型派生”。

“连续分配”的措辞意味着arrays成员之间没有填充。 脚注109肯定了这一概念:

两个对象在内存中可能是相邻的,因为它们是较大数组的相邻元素或结构的相邻成员,它们之间没有填充,或者因为实现选择放置它们,即使它们是不相关的。 如果先前的无效指针操作(例如数组边界外的访问)产生了未定义的行为,则后续比较也会产生未定义的行为。

在示例2的第6.5.3.5节中使用sizeof运算符表示在数组之前或之后也没有填充的意图:

例2

sizeof运算符的另一个用途是计算数组中元素的数量:

 sizeof array / sizeof array[0] 

因此,我得出结论,指向数组的指针,转换为指向该数组元素拼写错误的指针,指向数组中的第一个元素。 此外,观察关于指针的相等定义(§6.5.9/ 6f。):

6两个指针比较相等,当且仅当两个都是空指针时,两者都是指向同一对象的指针(包括指向对象的指针和在其开头的子对象)或函数,两者都是指向同一对象的最后一个元素的指针数组对象,或者一个是指向一个数组对象末尾的指针,另一个是指向不同数组对象的开头的指针,该数组对象恰好跟随地址空间中的第一个数组对象。 109)

7出于这些运算符的目的,指向不是数组元素的对象的指针与指向长度为1的数组的第一个元素的指针的行为相同,其中对象的类型为其元素类型。

由于数组的第一个元素是“开头的子对象”,因此指向数组第一个元素的指针和指向数组的指针相等。

这是一个稍微重构的代码版本,以便于参考:

 int arr[2] = { 0, 0 }; int *p1 = &arr[0]; int *p2 = (int *)&arr; 

问题是: p1 == p2真还是未指定,还是UB?


首先:我认为C的抽象记忆模型的作者打算p1 == p2为真; 如果标准实际上没有拼写出来那么它将成为标准中的缺陷。

继续; 唯一相关的文本似乎是C11 6.3.2.3/7(不相关的文本被删除):

指向对象类型的指针可以转换为指向不同对象类型的指针。 […]再次转换回来时,结果将与原始指针进行比较。

当指向对象的指针转换为指向字符类型的指针时,结果指向对象的最低寻址字节。 结果的连续递增(直到对象的大小)产生指向对象的剩余字节的指针。

它没有具体说明第一次转换的结果是什么。 理想情况下应该说……并且指针指向同一个地址 ,但事实并非如此。

但是,我认为暗示指针必须在转换后指向同一地址。 这是一个说明性的例子:

 void *v1 = malloc( sizeof(int) ); int *i1 = (int *)v1; 

如果我们不接受“ 并且指针指向同一个地址 ”那么i1可能实际上并没有指向malloc的空间,这将是荒谬的。

我的结论是我们应该阅读6.3.2.3/7,因为指针转换不会改变指向的地址。 关于使用指向字符类型的指针的部分似乎支持这一点。

因此,由于p1p2具有相同的类型并指向相同的地址,因此它们相等。

直接回答:

有人可以找到C标准中的相关部分,指出代码实际上有效吗?

  • 6.3.2.1左值,数组和函数指示符,第1段
  • 6.3.2.3指针,第1,5和6段
  • 6.5.3.2地址和间接运营商,第3段

或者是代码未定义的行为?

发布的代码未定义,但它“可能”是编译器/实现特定的(根据第6.3.2.3节第5/6页)

我基本上要问的是,为什么&arr在转换为void *&arr一样转换为void *

这意味着要问为什么int *ptr = (int*)(void*)&arr给出的结果与int *ptr = (int*)(void*)arr; ,但根据您发布的代码,您实际上是在问为什么int *ptr = (int*)(void*)&arrint *ptr = (int*)&arr

无论哪种方式,我都会扩展您的代码实际执行的操作以帮助澄清:

按6.3.2.1p3:

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

和6.5.3.2p3:

一元&运算符产生其操作数的地址。 如果操作数具有类型”type”,则结果具有类型”指向类型”的指针。

所以在你的第一次宣言中

int arr[2] = {0, 0};

arr被初始化为一个数组类型,其中包含2个int类型的元素,它们都等于0.然后,根据6.3.2.1p3它被“衰减”为指向第一个元素的指针类型,它在范围内被调用( 除非它被使用时) sizeof(arr)&arr++arr--arr )。

所以在下一行中,您可以简单地执行以下操作:

int *ptr = arr;int *ptr = &*arr; 或者int *ptr = &arr[0];

ptr现在是一个指向int类型的指针,指向数组arr的第一个元素(即&arr[0] )。

相反,你声明它是这样的:

int *ptr = (int*)&arr;

让我们把它分解成它的部分:

  1. &arr – >触发6.3.2.1p3的exception所以不是获取&arr[0] ,而是获取&arr[0]的地址,这是一个int(*)[2]类型(不是int*类型),所以你没有得到一个pointer to an intpointer to an int ,你得到一个pointer to an int arraypointer to an int array

  2. (int*)&arr ,(即转换为int* ) – >每6.5.3.2p3, &arr获取变量arr的地址返回指向它的类型的指针,所以简单地说int* ptr = &arr将给出一个“不兼容的指针类型”的警告(因为ptr的类型为int*&arr的类型为int(*)[2] ),这就是你需要转换为int*

进一步根据6.3.2.3p1: “指向void的指针可以转换为指向任何对象类型的指针。指向任何对象类型的指针可以转换为指向void的指针,然后再返回;结果应该等于原始指针“

所以,你要声明int* ptr = (int*)(void*)&arr; 会产生与int* ptr = (int*)&arr;相同的结果int* ptr = (int*)&arr; 因为您正在使用的类型和转换为/来自。 另外作为注释: ptr[0] = 5;*ptr = 5相同,其中ptr[1] = 5; 也与*++ptr = 5;

一些参考文献:

6.3.2.1左值,数组和函数指示符

1.左值是一个表达式(具有除void之外的对象类型),可能指定一个对象(*参见注释); 如果左值在评估时未指定对象,则行为未定义。 当一个对象被称为具有特定类型时,该类型由用于指定该对象的左值指定。 可修改的左值是一个左值,它没有数组类型,没有不完整的类型,没有constqualified类型,如果是结构或联合,则没有任何成员(包括,递归地,任何成员或元素)所有包含的聚合或联合)具有constqualified类型。

*名称”左值’最初来自赋值表达式E1 = E2,其中左操作数E1需要是(可修改的)左值。 它可能更好地被视为表示对象的“定位器值”。 有时被称为”rvalue’的东西在本国际标准中被描述为”表达的价值”。 左值的一个明显示例是对象的标识符。 作为另一示例,如果E是作为指向对象的指针的一元表达式,则* E是指定E指向的对象的左值。

2.除非它是sizeof运算符的操作数,_Alignof运算符,一元&运算符,++运算符, – 运算符或者左运算符。 运算符或赋值运算符,没有数组类型的左值被转换为存储在指定对象中的值(并且不再是左值); 这称为左值转换。 如果左值具有限定类型,则该值具有左值类型的非限定版本; 另外,如果左值具有primefaces类型,则该值具有左值类型的非primefaces版本; 否则,该值具有左值的类型。 如果左值具有不完整的类型且没有数组类型,则行为未定义。 如果左值指定了一个自动存储持续时间的对象,该对象可以使用寄存器存储类声明(从未使用其地址),并且该对象未初始化(未使用初始化程序声明,并且在使用之前未对其进行任何赋值) ),行为未定义。

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

6.3.2.3指针

1.指向void的指针可以转换为指向任何对象类型的指针。 指向任何对象类型的指针可以转换为指向void的指针,然后再返回; 结果应该等于原始指针。

5.整数可以转换为任何指针类型。 除了先前指定的,结果是实现定义的,可能未正确对齐,可能不指向引用类型的实体,并且可能是陷阱表示(用于将指针转换为整数或整数的映射函数)指针旨在与执行环境的寻址结构一致。

6.任何指针类型都可以转换为整数类型。 除了之前指定的以外,结果是实现定义的。 如果结果无法以整数类型表示,则行为未定义。 结果不必在任何整数类型的值范围内。

6.5.3.2地址和间接运营商

1.一元&运算符的操作数应该是函数指示符,[]或一元*运算符的结果,或者是一个左值,它指定一个不是位字段且没有用寄存器存储器声明的对象 – 类说明符。

3.一元&运算符产生其操作数的地址。 如果操作数具有类型”type”,则结果具有类型”指向类型”的指针。 如果操作数是一元*运算符的结果,则不会对该运算符和&运算符进行求值,结果就好像两者都被省略,除了对运算符的约束仍然适用且结果不是左值。 类似地,如果操作数是[]运算符的结果,则[]运算符和[]隐含的一元*都不会被计算,结果就像删除了&运算符并且[]运算符被更改为a +运算符。 否则,结果是指向由其操作数指定的对象或函数的指针。

4.一元*运算符表示间接。 如果操作数指向函数,则结果是函数指示符; 如果它指向一个对象,则结果是指定该对象的左值。 如果操作数具有类型”指向类型’的指针,则结果具有类型”type”。 如果为指针分配了无效值,则unary *运算符的行为未定义(*请参阅注释)。

*因此,&* E等效于E(即使E是空指针),&(E1 [E2])等于((E1)+(E2))。 如果E是函数指示符或左值是一元&运算符的有效操作数,则总是如此,*&E是函数指示符或等于E的左值。如果* P是左值并且T是名称对象指针类型,*(T)P是左值,其类型与T指向的类型兼容。 在由一元*运算符解除引用指针的无效值中,有一个空指针,一个与指向的对象类型不适当对齐的地址,以及一个对象在其生命周期结束后的地址。

6.5.4铸造操作员

5.通过带括号的类型名称在表达式之前,将表达式的值转换为命名类型。 此构造称为强制转换(强制转换不会产生左值;因此,对合格类型的强制转换与对该类型的非限定版本的强制转换具有相同的效果)。 指定不进行转换的强制转换对表达式的类型或值没有影响。

6.如果表达式的值表示的范围或精度大于强制转换(6.3.1.8)指定的类型所需的范围或精度,则即使表达式的类型与命名类型相同,强制转换也会指定转换并删除任何额外的范围和精度。

6.5.16.1简单分配

2.在简单赋值(=)中,右操作数的值被转换为赋值表达式的类型,并替换存储在左操作数指定的对象中的值。

6.7.6.2数组声明符

1.除了可选的类型限定符和关键字static之外,[和]可以分隔表达式或*。 如果它们分隔表达式(指定数组的大小),则表达式应具有整数类型。 如果表达式是常量表达式,则其值应大于零。 元素类型不应是不完整或函数类型。 可选的类型限定符和关键字static应仅出现在具有数组类型的函数参数的声明中,然后仅出现在最外层的数组类型派生中。

3.如果在声明”T D1”中,D1有以下forms之一:

D [type-qualifier-listopt assignment-expressionopt]
D [static type-qualifier-listopt assignment-expression]
D [type-qualifier-list static assignment-expression]
D [type-qualifier-listopt *]

并且在声明”T D”中为ident指定的类型是”derived-declarator-type-list T”,那么为ident指定的类型是”derived-declarator-type-list array of T” .142)(有关可选类型限定符和关键字static的含义,请参见6.7.6.3。)

4.如果大小不存在,则数组类型为不完整类型。 如果大小是*而不是表达式,则数组类型是未指定大小的可变长度数组类型,它只能用于具有函数原型范围的声明或类型名称; 143)此类数组仍然是完整类型。 如果size是一个整型常量表达式,并且元素类型具有已知的常量大小,则数组类型不是可变长度数组类型; 否则,数组类型是可变长度数组类型。 (可变长度数组是实现不需要支持的条件特性;请参阅6.10.8.3。)

5.如果size是一个不是整数常量表达式的表达式:如果它出现在函数原型范围的声明中,则将其视为*被替换为*; 否则,每次评估它时,其值应大于零。 变长数组类型的每个实例的大小在其生命周期内不会改变。 如果size表达式是sizeof运算符的操作数的一部分,并且更改size表达式的值不会影响运算符的结果,则无法指定是否计算size表达式。

6.对于两个要兼容的数组类型,两者都应具有兼容的元素类型,如果两个大小说明符都存在,并且是整数常量表达式,则两个大小说明符应具有相同的常量值。 如果在要求它们兼容的上下文中使用这两种数组类型,则如果两个大小说明符计算为不相等的值,则它是未定义的行为。

PS作为旁注,给出以下代码:

 #include  int main(int argc, char** argv) { int arr[2] = {10, 20}; X Y printf("%d,%d\n", arr[0],arr[1]); return 0; } 

其中X是以下之一:

 int *ptr = (int*)(void*)&arr; int *ptr = (int*)&arr; int *ptr = &arr[0]; 

和Y是以下之一:

 ptr[0] = 15; *ptr = 15; 

当在具有gcc版本4.2.1 20070719的OpenBSD上编译并提供-S标志时,所有文件的汇编器输出完全相同。