我试图理解当将值存储到结构或联合的成员中时,类型惩罚是如何工作的。
标准N1570 6.2.6.1(p6)规定了 当值存储在…中时
这个问题很糟糕。让我们首先看看代码:
char repr[offsetof(struct first_member_padded_t, b)] = //some value memcpy(repr, &a, sizeof(a)); memcpy(&(s.a), repr, sizeof(repr));
首先要注意的是 repr 已初始化,因此其中的所有元素都是给定的值。
repr
首先 memcpy 是很好的复制它的字节 a 成 repr 。
memcpy
a
如果是第二个 memcpy 是 memcpy(&s, repr, sizeof repr); ,它会从中复制字节 repr 成 s 。这会写入字节 s.a 并且,由于的大小 repr ,进入任何填充之间 s.a 和 s.b 。根据标准的其他部分,允许访问对象的字节(并且访问权限意味着读取和写入,每3.1 1)。所以这个副本进入 s 很好,它会导致 s.a 具有相同的价值 a 具有。
memcpy(&s, repr, sizeof repr);
s
s.a
s.b
然而 memcpy 使用 &(s.a) 而不是 &s 。它使用的地址 s.a 而不是地址 s 。我们知道转换 s.a 指向字符类型的指针将允许我们访问字节 s.a (6.5 7及以上)(并将其传递给 memcpy 具有与这种转换相同的效果,如 memcpy 指定具有复制字节的效果,但不清楚它允许我们访问其他字节 s 。换句话说,我们有一个问题,我们是否可以使用 &s.a 访问除了那些以外的字节 s.a 。
&(s.a)
&s
&s.a
6.7.2.1 15告诉我们,如果指向结构的第一个成员的指针被适当地转换,结果将指向结构。所以,如果我们转换 &s.a 到指针 struct first_member_padding_t ,它会指向 s ,我们当然可以使用指针 s 访问中的所有字节 s 。因此,这也将很好地定义:
struct first_member_padding_t
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);
void *
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或标准的意图。
(void *) &s.a
(void *) &s
问题是:
这不是类型惩罚,因为这个术语是常用的。从技术上讲,代码可以访问 s.a 使用与其定义不同类型的左值(因为它使用 memcpy ,定义为使用字符类型进行复制,而定义的类型是 int ),但字节起源于 int 并且无需修改即可复制,这种复制对象字节的方式通常被认为是机械程序;它完成了一个副本而不是重新解释一个新类型的字节。类型 - 惩罚通常是指使用不同的左值来重新解释该值,例如写一个 unsigned int 和阅读 float 。
int
unsigned int
float
无论如何,打字并不是问题的主题。
标题问:
我们可以在结构或联盟成员中存储什么值?
这个标题似乎与问题的内容有关。标题问题很容易回答:我们可以存储的值 的 在 强> 成员是成员类型可以表示的那些值。但问题是继续探讨成员之间的填充。填充不会影响成员中的值。
问题引用了标准:
当值存储在结构或联合类型的对象中(包括在成员对象中)时,对应于任何填充字节的对象表示的字节采用未指定的值。
并说:
所以我把它解释为好像我们有一个对象存储到一个成员中,使得对象的大小等于s izeof(declared_type_of_the_member) + padding 与填充相关的字节将具有未指定的值
izeof(declared_type_of_the_member) + padding
标准中引用的文本表示,如果填充字节在 s 已设置为某些值,如同 memcpy ,然后我们做 s.a = something; ,然后不再需要填充字节来保存其先前的值。
s.a = something;
问题中的代码探讨了不同的情况。代码 memcpy(&(s.a), repr, sizeof(repr)); 在6.2.6.1中意义上的结构成员中不存储值。它不存储在任何一个成员中 s.a 要么 s.b 。它正在复制字节,这与6.2.6.1中讨论的不同。
memcpy(&(s.a), repr, sizeof(repr));
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 。该标准的作者不希望禁止这种治疗。
s->y = 23;
y
如果标准包含了一种方法,通过该方法可以指示对结构或联合成员的写入是否会干扰超出它们的存储,并且可能会被这种干扰打破的程序可能拒绝在可能发生的实现上运行,这将是有用的。然而,该标准的作者可能期望那些对这些细节感兴趣的程序员会知道他们的程序预期会运行什么类型的硬件,从而知道这种内存干扰是否会成为这类硬件的问题。
不幸的是,现代编译器编写者似乎解释了旨在帮助实现异常硬件的自由作为一种开放的邀请来获得“创造性”,即使是在没有这种让步的情况下有效处理代码的平台。
正如@ user694733所说,万一之间有填充 s.a 和 s.b , memcpy() 正在访问无法访问的内存区域 &a :
memcpy()
&a
int a = 1; int b; b = *((char *)&a + sizeof(int));
这是未定义的行为,它基本上是内部发生的事情 memcpy() 。