在试图将类型的值存储到结构或联合的成员中时,我试图了解类型处理的工作原理。
标准N1570 6.2.6.1(p6)
指定
将值存储在结构或联合类型的对象中时, 在成员对象中包括对象表示形式的字节 与任何填充字节相对应的值均未指定。
因此,我将其解释为好像有一个对象要存储到一个成员中,使得该对象的大小等于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
的行为是否正确?我就是这样。
答案 0 :(得分:3)
这个问题提出得不好。首先看一下代码:
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
。
如果第二个memcpy
是memcpy(&s, repr, sizeof repr);
,它将把字节从repr
复制到s
。这将把字节写入s.a
,并且由于repr
的大小,将字节写入s.a
和s.b
之间的任何填充中。根据C 2018 6.5 7和其他标准,允许访问对象的字节(“访问”表示按照3.1 1进行读写)。因此,将此副本复制到s
是好的,它会导致s.a
的值与a
相同。
但是,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
的指针来访问{ {1}}。因此,这也将得到很好的定义:
s
但是,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
。 6.2.5 28告诉我们,指向void *
的指针与指向字符类型的指针具有相同的表示形式。因此,请考虑以下两个语句:
void
这两个语句都将memcpy(&s.a, repr, sizeof repr);
memcpy(&s, repr, sizeof repr);
传递到void *
,并且这两个memcpy
彼此具有相同的表示形式,并指向相同的字节。现在,我们可能会严格而严格地解释标准,以使它们有所不同,因为后者可以用于访问void *
的所有字节,而前者则不能。然后奇怪的是,我们有两个必须相同的指针,它们的行为不同。
在理论上对C标准进行严格的解释似乎是可能的-指针之间的差异可能在优化过程中出现,而不是在s
的实际实现中出现-但我不知道有任何编译器可以做到这一点。请注意,这种解释与标准的6.2节不一致,后者告诉我们有关类型和表示形式的信息。解释标准,以使memcpy
和(void *) &s.a
的行为不同,这意味着具有相同值和类型的两个事物的行为可能不同,这意味着值除了其值和类型之外还包含其他内容,通常是6.2或标准的意图。
问题指出:
在试图将类型的值存储到结构或联合的成员中时,我试图了解类型处理的工作原理。
这不是常用的术语,因此不进行类型处理。从技术上讲,代码确实使用与定义不同类型的左值访问(void *) &s
(因为它使用s.a
,它定义为像字符类型一样进行复制,而定义的类型为{{1}) }),但是字节起源于memcpy
且未经修改即被复制,这种复制对象字节的方式通常被视为机械过程;这样做是为了实现复制,而不是重新解释新类型的字节。 “类型操纵”通常是指使用不同的左值来重新解释该值,例如编写int
和读取int
。
无论如何,类型拼写并不是问题的实质。
标题问:
我们可以在结构或联合成员中存储哪些值?
该标题似乎与问题的内容背道而驰。标题问题很容易回答:我们可以在成员中存储的值是成员类型可以代表的值。但是问题还是继续探讨成员之间的填充。填充不会影响成员中的值。
问题引用了标准:
当将值存储在结构或联合类型的对象中(包括在成员对象中)时,与任何填充字节对应的对象表示形式的字节将采用未指定的值。
并说:
因此,我将其解释为好像有一个对象要存储到成员中,以便该对象的大小等于s
unsigned int
,与填充相关的字节将具有未指定的值…
标准中带引号的文字表示,如果float
中的填充字节已设置为某些值,例如izeof(declared_type_of_the_member) + padding
,然后我们进行s
,则填充不再需要字节来保存其以前的值。
问题中的代码探讨了另一种情况。代码memcpy
不会在6.2.6.1 6.含义中将值存储在结构的成员中。它不会存储在成员s.a = something;
或memcpy(&(s.a), repr, sizeof(repr));
中。它正在复制字节,这与6.2.6.1中讨论的内容有所不同。
6.2.6.1 6表示例如,如果我们执行以下代码:
s.a
然后不一定要打印所有零-填充中的字节可能已更改。
答案 1 :(得分:1)
在编写C标准以描述该语言的许多实现中,尝试在struct或union中写入N字节对象将影响struct或union中最多N个字节的值。另一方面,在支持8位和32位存储区但不支持16位存储区的平台上,如果有人声明了以下类型:
struct S { uint32_t x; uint16_t y;} *s;
然后执行s->y = 23;
而不关心y
之后的两个字节发生了什么,对y
执行32位存储会更快,盲目地覆盖两个字节然后执行一对8位写操作来更新y
的上半部分和下半部分。该标准的作者不想禁止这种处理。
如果该标准包含一种实现方式,该实现方式可以指示对结构体或联合成员的写操作是否可能会干扰它们之外的存储,而被此类干扰破坏的程序可以拒绝在该标准的实现中运行,则将很有帮助可能发生。但是,该标准的作者可能希望对这些细节感兴趣的程序员会知道他们的程序预期将在哪种硬件上运行,从而知道此类内存是否会成为此类硬件的问题。 >
不幸的是,现代编译器作者似乎将旨在协助非常规硬件实现的自由解释为公开邀请,以获取“创造力”,即使目标是可以在没有此类让步的情况下有效处理代码的平台。
答案 2 :(得分:0)
正如@ user694733所说,如果s.a
和s.b
之间存在填充,memcpy()
将访问&a
无法访问的存储区域:
int a = 1;
int b;
b = *((char *)&a + sizeof(int));
这是未定义的行为,基本上就是memcpy()
内部发生的事情。