直接回答:
有人能在 C 标准中找到指定代码确实有效的相关部分吗?
- 6.3.2.1 左值、数组和函数指示符,第 1 段
- 6.3.2.3 指针,第 1,5 和 6 段
- 6.5.3.2 地址和间接运算符,第 3 段
还是代码未定义行为?
您发布的代码不是未定义的,但它“可能”是特定于编译器/实现的(根据第 6.3.2.3 p5/6 节)
我实质上要问的是为什么&arr 转换为void * 与arr 转换为void * 时相同?
这意味着问为什么int *ptr = (int*)(void*)&arr 给出与int *ptr = (int*)(void*)arr; 相同的结果,但根据您发布的代码,您实际上是在问为什么int *ptr = (int*)(void*)&arr 给出与int *ptr = (int*)&arr 相同的结果。
无论哪种方式,我都会扩展您的代码实际上在做什么以帮助澄清:
每 6.3.2.1p3:
除非它是 sizeof 运算符、_Alignof 运算符或一元 & 运算符的操作数,或者是用于初始化数组的字符串字面量,否则是类型为 ''array of type'' 的表达式转换为类型为“类型指针”的表达式,它指向数组对象的初始元素,而不是左值。如果数组对象有注册存储类,则行为未定义。
并且根据 6.5.3.2p3:
一元 & 运算符产生其操作数的地址。如果操作数的类型为“type”,则结果的类型为“pointer to 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;
让我们把它分解成几个部分:
-
&arr -> 触发6.3.2.1p3 的异常,所以不是得到&arr[0],而是得到arr 的地址,这是int(*)[2] 类型(不是int* 类型),所以你是没有得到pointer to an int,你得到的是pointer to an int array
-
(int*)&arr,(即转换为int*)-> 根据6.5.3.2p3,&arr 获取变量arr 的地址,返回指向它的类型的指针,所以简单地说@987654356 @ 将给出 “不兼容的指针类型” 的警告(因为 ptr 的类型为 int* 和 &arr 的类型为 int(*)[2]),这就是为什么您需要转换为 @ 987654361@.
进一步根据 6.3.2.3p1:“指向 void 的指针可以转换为指向任何对象类型的指针或从指向任何对象类型的指针转换。指向任何对象类型的指针可以转换为指向 void 的指针并再次返回;结果应与原始指针比较”.
因此,您声明 int* ptr = (int*)(void*)&arr; 将产生与 int* ptr = (int*)&arr; 相同的结果,因为您正在使用和转换为/从的类型。另请注意:ptr[0] = 5; 与*ptr = 5 相同,其中ptr[1] = 5; 也与*++ptr = 5; 相同
一些参考资料:
6.3.2.1 左值、数组和函数指示符
1. 左值是一个表达式(对象类型不是 void),它可能指定一个对象(*见注);如果左值在评估时未指定对象,则行为未定义。当一个对象被称为具有特定类型时,该类型由用于指定该对象的左值指定。可修改的左值是没有数组类型,没有不完整类型,没有 const 限定类型,并且如果它是结构或联合,则没有任何成员(包括递归地,任何成员或元素所有包含的聚合或联合)具有 const 限定类型。
*名称“左值”最初来自赋值表达式 E1 = E2,其中左操作数 E1 必须是(可修改的)左值。将其视为表示对象“定位器值”可能更好。有时被称为“右值”的东西在本国际标准中被描述为“表达式的值”。左值的一个明显示例是对象的标识符。再举一个例子,如果 E 是一个一元表达式,它是一个指向对象的指针,那么 *E 是一个左值,它指定 E 指向的对象。
2. 除非它是 sizeof 运算符、_Alignof 运算符、一元 & 运算符、++ 运算符、-- 运算符或 . 的左操作数的操作数。运算符或赋值运算符,将不具有数组类型的左值转换为存储在指定对象中的值(不再是左值);这称为左值转换。如果左值具有限定类型,则该值具有左值类型的非限定版本;此外,如果左值具有原子类型,则该值具有左值类型的非原子版本;否则,该值具有左值的类型。如果左值的类型不完整且没有数组类型,则行为未定义。如果左值指定了一个可以使用寄存器存储类声明的具有自动存储持续时间的对象(从未使用过它的地址),并且该对象未初始化(未使用初始化程序声明并且在使用之前未对其进行分配),行为未定义。
3. 除非它是 sizeof 运算符、_Alignof 运算符或一元 & 运算符的操作数,或者是用于初始化数组的字符串文字,否则类型为 ''array of type'' 的表达式将转换为类型为“类型指针”的表达式,它指向数组对象的初始元素并且不是左值。如果数组对象具有寄存器存储类,则行为未定义。
6.3.2.3 指针
1. 指向 void 的指针可以转换为或从指向任何对象类型的指针转换。指向任何对象类型的指针都可以转换为指向 void 的指针并再次返回;结果应与原始指针比较。
5. 整数可以转换为任何指针类型。除非前面指定,结果是实现定义的,可能没有正确对齐,可能不指向引用类型的实体,并且可能是陷阱表示(用于将指针转换为整数或整数的映射函数)指针旨在与执行环境的寻址结构保持一致)。
6. 任何指针类型都可以转换为整数类型。除非前面指定,结果是实现定义的。如果结果不能以整数类型表示,则行为未定义。结果不必在任何整数类型的值范围内。
6.5.3.2 地址和间接运算符
1. 一元 & 运算符的操作数应该是一个函数指示符,一个 [] 或一元 * 运算符的结果,或者一个左值,它指定一个不是位域且未使用寄存器存储声明的对象 -类说明符。
3. 一元 & 运算符产生其操作数的地址。如果操作数的类型为“type”,则结果的类型为“pointer to type”。如果操作数是一元 * 运算符的结果,则该运算符和 & 运算符都不会被计算,结果就像两者都被省略了,除了对运算符的约束仍然适用并且结果不是左值。类似地,如果操作数是 [] 运算符的结果,则 & 运算符和 [] 所隐含的一元 * 都不会被计算,结果就像 & 运算符被删除并且 [] 运算符被更改为+ 运算符。否则,结果是指向其操作数指定的对象或函数的指针。
4. 一元 * 运算符表示间接。如果操作数指向一个函数,则结果是一个函数指示符;如果它指向一个对象,则结果是一个指定该对象的左值。如果操作数的类型为“类型指针”,则结果的类型为“类型”。如果已为指针分配了无效值,则一元 * 运算符的行为是未定义的(*参见注释)。
*因此,&*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 具有以下形式之一:
D[ type-qualifier-listopt assignment-expressionopt ]
D[静态类型限定符列表选择赋值表达式]
D[ 类型限定符列表静态赋值表达式 ]
D[ 类型限定符列表选择 * ]
并且在声明''T D''中为ident指定的类型是''derived-declarator-type-list T'',那么为ident指定的类型是''derived-declarator-type-list array of T'' .142) (有关可选类型限定符和关键字 static 的含义,请参见 6.7.6.3。)
4.如果大小不存在,则数组类型是不完整类型。如果大小是 * 而不是表达式,则数组类型是未指定大小的可变长度数组类型,它只能用于具有函数原型范围的声明或类型名称中;143) 这样的数组仍然是完整类型。如果大小是整数常量表达式并且元素类型具有已知的常量大小,则数组类型不是变长数组类型;否则,数组类型是可变长度数组类型。 (可变长度数组是实现不需要支持的条件特性;参见 6.10.8.3。)
5.如果大小是一个不是整数常量表达式的表达式:如果它出现在函数原型范围的声明中,它被视为被*替换;否则,每次对其进行评估时,它的值都应大于零。可变长度数组类型的每个实例的大小在其生命周期内不会改变。如果 size 表达式是 sizeof 运算符的操作数的一部分,并且更改 size 表达式的值不会影响运算符的结果,则未指定是否计算 size 表达式。
6. 对于要兼容的两个数组类型,两者都应具有兼容的元素类型,并且如果两个大小说明符都存在,并且都是整数常量表达式,则两个大小说明符应具有相同的常量值。如果在需要它们兼容的上下文中使用这两种数组类型,则如果这两个大小说明符计算为不相等的值,则这是未定义的行为。
附:作为旁注,给定以下代码:
#include <stdio.h>
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 标志时,所有文件的汇编器输出完全相同。