可以使用指向结构成员的指针来访问同一结构的另一个成员吗?

Can a pointer to a member of a struct be used to access another member of the same struct?

我正在尝试了解在将值存储到结构或联合的成员中时类型双关是如何工作的。

标准 N1570 6.2.6.1(p6) 指定

When a value is stored in an object of structure or union type, including in a member object, the bytes of the object representation that correspond to any padding bytes take unspecified values.

所以我将其解释为好像我们有一个对象要存储到一个成员中,这样对象的大小等于 sizeof(declared_type_of_the_member) + padding 与填充相关的字节将具有未指定的值(即使事实上我们在原始对象中定义了字节)。这是一个例子:

struct first_member_padded_t{
    int a;
    long b;
};

int a = 10;
struct first_member_padded_t s;
char repr[offsetof(struct first_member_padded_t, b)] = //some value
memcpy(repr, &a, sizeof(a));
memcpy(&(s.a), repr, sizeof(repr));
s.b = 100;
printf("%d%ld\n", s.a, s.b); //prints 10100

在我的机器上 sizeof(int) = 4, offsetof(struct first_member_padded_t, b) = 8.

这种程序的打印行为 10100 是否定义明确?我觉得是这样。

正如@user694733所说,如果s.as.b之间存在填充,memcpy()正在访问&a无法访问的内存区域:

int a = 1;
int b;
b = *((char *)&a + sizeof(int));

这是未定义的行为,它基本上就是 memcpy() 内部发生的事情。

memcpy 调用的作用

问题提得不好。先看代码:

char repr[offsetof(struct first_member_padded_t, b)] = //some value
memcpy(repr, &a, sizeof(a));
memcpy(&(s.a), repr, sizeof(repr));

首先注意repr是初始化的,所以里面的所有元素都赋值

第一个 memcpy 没问题——它将 a 的字节复制到 repr

如果第二个 memcpymemcpy(&s, repr, sizeof repr);,它会将字节从 repr 复制到 s。这会将字节写入 s.a,并且由于 repr 的大小,写入 s.as.b 之间的任何填充。根据 C 2018 6.5 7 和标准的其他部分,允许访问 object 的字节(根据 3.1 1,“访问”意味着读取和写入)。所以这个复制到 s 很好,它导致 s.aa 具有相同的值。

然而,memcpy 使用 &(s.a) 而不是 &s。它使用 s.a 的地址而不是 s 的地址。我们知道将 s.a 转换为指向字符类型的指针将允许我们访问 s.a 的字节(6.5 7 及更高版本)(并将其传递给 memcpy 具有与这样的转换,因为 memcpy 被指定为具有复制字节的效果),但不清楚它是否允许我们访问 s 中的其他字节。换句话说,我们有一个问题,我们是否可以使用 &s.a 来访问除 s.a.

之外的字节

6.7.2.1 15 告诉我们,如果指向结构第一个成员的指针被“适当地转换”,则结果指向该结构。所以,如果我们将 &s.a 转换为指向 struct first_member_padding_t 的指针,它将指向 s,并且我们当然可以使用指向 s 的指针来访问 s 中的所有字节。 =21=]。因此,这也将被明确定义:

memcpy((struct first_member_padding t *) &s.a, repr, sizeof repr);

然而,memcpy(&s.a, repr, sizeof repr);只是将&s.a转换为void *(因为memcpy被声明为取一个void *,所以&s.a是在函数调用期间自动转换)而不是指向结构类型的指针。那是一个合适的转换吗?请注意,如果我们执行 memcpy(&s, repr, sizeof repr);,它会将 &s 转换为 void *。 6.2.5 28 告诉我们指向 void 的指针与指向字符类型的指针具有相同的表示形式。所以考虑这两个语句:

memcpy(&s.a, repr, sizeof repr);
memcpy(&s,   repr, sizeof repr);

这两个语句都将 void * 传递给 memcpy,并且这两个 void * 具有相同的表示形式并指向相同的字节。现在,我们可能会迂腐而严格地解释标准,以便它们的不同之处在于后者可以用于访问 s 的所有字节,而前者则不能。然后奇怪的是我们有两个行为不同的必然相同的指针。

对 C 标准的如此严格的解释在理论上似乎是可能的——指针之间的差异可能在优化期间出现,而不是在 memcpy 的实际实现中出现——但我不知道有哪个编译器会做这个。请注意,这种解释与标准的第 6.2 节不一致,该节告诉我们有关类型和表示的信息。解释标准使得 (void *) &s.a(void *) &s 表现不同意味着具有相同值和类型的两个事物可能表现不同,这意味着一个值由比它的值和类型更多的东西组成,这看起来并不一般是 6.2 或标准的意图。

Type-Punning

问题说明:

I'm trying to understand how type-punning works when it comes to storing a value into a member of structure or union.

这不是 type-punning,因为该术语很常用。从技术上讲,代码确实使用与其定义不同类型的左值访问 s.a(因为它使用 memcpy,它被定义为像使用字符类型一样复制,而定义的类型是 int), 但是字节来源于一个int并且被不加修改地复制,而这种复制object的字节通常被认为是一个机械过程;这样做是为了实现复制而不是重新解释新类型的字节。 “Type-punning”通常是指为了重新解释值而使用不同的左值,比如写一个unsigned int,读一个float

无论如何,type-punning并不是真正的问题。

成员中的值

题目要求:

What values can we store in a struct or union members?

这个标题好像和问题的内容不符。标题问题很容易回答:我们可以存储 in 一个成员的值是该成员的类型可以表示的那些值。但问题继续探索成员之间的填充。填充不影响成员中的值。

填充采用未指定的值

题目引用标准:

When a value is stored in an object of structure or union type, including in a member object, the bytes of the object representation that correspond to any padding bytes take unspecified values.

并说:

So I interpreted it as if we have an object to store into a member such that the size of the object equals the sizeof(declared_type_of_the_member) + padding the bytes related to padding will have unspecified value…

标准中引用的文本意味着,如果 s 中的填充字节已设置为某些值,如 memcpy,然后我们执行 s.a = something;,则不再需要填充字节来保存它们以前的值。

问题中的代码探讨了不同的情况。代码 memcpy(&(s.a), repr, sizeof(repr)); 没有在 6.2.6.1 6 中表示的意义上将值存储在结构的成员中。它没有存储到任何一个成员 s.as.b。是copy bytes in,和6.2.6.1讨论的不一样。

6.2.6.1 6的意思是,比如我们执行这段代码:

char repr[sizeof s] = { 0 };
memcpy(&s, repr, sizeof s); // Set all the bytes of s to known values.
s.a = 0; // Store a value in a member.
memcpy(repr, &s, sizeof s); // Get all the bytes of s to examine them.
for (size_t i = sizeof s.a; i < offsetof(struct first_member_padding_t, b); ++i)
    printf("Byte %zu = %d.\n", i, repr[i]);

那么不一定会打印所有的零——填充中的字节可能已经改变。

在编写 C 标准来描述的语言的许多实现中,尝试在结构或联合内写入 N 字节对象会影响结构或联合内最多 N 字节的值。另一方面,在支持 8 位和 32 位存储但不支持 16 位存储的平台上,如果有人声明如下类型:

struct S { uint32_t x; uint16_t y;} *s;

然后执行s->y = 23;而不关心y后面的两个字节发生了什么,执行32位存储到y会更快,盲目覆盖它后面的两个字节,而不是执行一对 8 位写入来更新 y 的上半部分和下半部分。标准的作者不想禁止这种处理。

如果标准包含一种方法,实现可以指示对结构或联合成员的写入是否会干扰它们之外的存储,并且会被这种干扰破坏的程序可以拒绝 运行 在可能发生的实现上。然而,该标准的作者可能希望对此类细节感兴趣的程序员会知道他们的程序预期 运行 在哪种硬件上运行,从而知道此类内存干扰是否会成为此类硬件的问题硬件。

不幸的是,现代编译器作者似乎将旨在帮助实现不寻常硬件的自由解释为公开邀请,以获得 "creative" 即使目标平台可以在没有此类让步的情况下高效地处理代码。