【问题标题】:casting pointer to array into pointer将指向数组的指针转换为指针
【发布时间】:2015-03-24 23:01:18
【问题描述】:

考虑以下 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]);

我在考虑这里的问题时发明了这个例子:Pointer-to-array overlapping end of array ...但这显然是一个独特的问题。

【问题讨论】:

  • 让我帮你查一下。总体思路是,可以将指向复合类型(联合/结构/数组)的指针转换为指向其第一个成员的指针。
  • 好问题。我在过去不止一次想知道完全相同的事情。人们几乎会认为 &arr 应该产生一个 ** 指针。
  • @juhist:事实上int *ptr = &arr; 是违反约束的。符合要求的编译器必须发出诊断。该诊断可以是警告或致命错误。 (恕我直言,默认情况下它应该是一个致命错误,但例如 gcc 的作者似乎不同意。)
  • 我在标准中没有看到(int*)&arr == &arr[0] 的保证。特别是引用N1570 draft,6.3.2.3p7中指针转换的语义没有涵盖这种情况。
  • @1sand0s:不,&arr 绝对不是地址的地址。它是数组对象的地址。 (一元 & 的操作数是数组表达式不隐式转换为指针的三个上下文之一。)

标签: c language-lawyer


【解决方案1】:

对于联合体和结构体,请参阅。 ISO 9899:2011§6.7.2.1/16f:

16 联合体的大小足以容纳其最大的成员。任何时候最多可以将其中一个成员的值存储在联合对象中。一个指向联合对象的指针,经过适当的转换,指向它的每个成员(或者如果一个成员是一个位域,则指向它所在的单元),反之亦然。

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

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

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

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

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

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

示例 2

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

sizeof array / sizeof array[0]

因此,我得出结论,指向数组的指针,转换为指向该数组元素类型的指针,指向数组中的第一个元素。此外,请注意关于指针的相等定义(§6.5.9/6f.):

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

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

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

【讨论】:

  • 好吧,我希望你能找到一些东西; @Keith Thompson 找不到任何东西。看来您的推理是理智的,我现在毫无疑问为什么普通编译器将指向数组的指针视为指向其初始成员的指针(因为具有相同的地址,而不是具有相同的类型)。
  • "元素拼写错误" --
  • 我同意你关于数组表示的推理。我不同意您的结论严格遵循表述。
  • @KeithThompson 这只是一个中间保存,因为有人咳咳认为同时编辑答案是个好主意。
  • 是的,我也这么认为。我目前正在提交缺陷报告,敬请期待。
【解决方案2】:

这里是您的代码稍作重构的版本,以便于参考:

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

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


首先:我认为p1 == p2 是真实的C 抽象内存模型的作者的意图;如果标准没有真正说明,那么这将是标准的缺陷。

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

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

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

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

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

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

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

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

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

【讨论】:

  • 请注意,根据 §6.2,兼容类型之间的转换不会改变类型中存储的值。
  • @FUZxxl 您的意思是 6.3/2“将操作数值转换为兼容类型不会导致值或表示形式发生变化。”?术语兼容类型由6.2.7定义,它基本上意味着类型是相同的。指向不同类型的两个指针不兼容。 (事实上​​,由于指向不同类型的指针可能有不同的大小和表示;一般来说,指针转换可能会改变表示)。
【解决方案3】:

直接回答:

有人能在 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;

让我们把它分解成几个部分:

  1. &arr -> 触发6.3.2.1p3 的异常,所以不是得到&arr[0],而是得到arr 的地址,这是int(*)[2] 类型(不是int* 类型),所以你是没有得到pointer to an int,你得到的是pointer to an int array

  2. (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 标志时,所有文件的汇编器输出完全相同。

【讨论】:

  • 感谢您的详细解答!但是,您说“请注意,尽管从 &arr 转换为 int* 可能不会给出正确的结果,具体取决于编译器/实现(要扩展,请参见 6.3.2.3p5/6),但在此代码的情况下,超过可能会,因为您正在处理 int 类型。” -- 在我看来,6.3.2.3p5/6 讨论将指针转换为整数,反之亦然,而我将一种类型的指针转​​换为另一种类型的指针。
  • @juhist,经过重新阅读,我同意,我已经把那条笔记拿出来了,但留下了供参考的段落
猜你喜欢
  • 2014-07-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-05-15
  • 1970-01-01
  • 1970-01-01
  • 2021-07-22
相关资源
最近更新 更多