memcpy可以用于打字吗?

这是C11标准的引用:

6.5表达式

6访问其存储值的对象的有效类型是对象的声明类型(如果有)。 如果通过具有非字符类型的类型的左值将值存储到没有声明类型的对象中,则左值的类型将成为该访问的对象的有效类型以及不修改该值的后续访问的有效类型储值。 如果使用memcpymemmove将值复制到没有声明类型的对象中,或者将其复制为字符类型数组,则该访问的修改对象的有效类型以及不修改该值的后续访问的有效类型是复制值的对象的有效类型(如果有)。 对于没有声明类型的对象的所有其他访问,对象的有效类型只是用于访问的左值的类型。

7对象的存储值只能由具有以下类型之一的左值表达式访问:

– 与对象的有效类型兼容的类型,

– 与对象的有效类型兼容的类型的限定版本,

– 对应于对象的有效类型的有符号或无符号类型,

– 对应于对象有效类型的限定版本的有符号或无符号类型,

– 聚合或联合类型,其成员中包含上述类型之一(包括递归地,子聚合或包含联合的成员),或者

– 一个字符类型。

这是否意味着memcpy不能以这种方式用于类型惩罚:

 double d = 1234.5678; uint64_t bits; memcpy(&bits, &d, sizeof bits); printf("the representation of %g is %08"PRIX64"\n", d, bits); 

为什么它不会给出相同的输出:

 union { double d; uint64_t i; } u; ud = 1234.5678; printf("the representation of %g is %08"PRIX64"\n", d, ui); 

如果我使用我的版本的memcpy使用字符类型怎么办:

 void *my_memcpy(void *dst, const void *src, size_t n) { unsigned char *d = dst; const unsigned char *s = src; for (size_t i = 0; i < n; i++) { d[i] = s[i]; } return dst; } 

编辑: EOF评论说, 第6段中关于memcpy()的部分不适用于这种情况,因为uint64_t bits具有声明的类型。 我同意,但不幸的是,这无法回答memcpy是否可以用于类型惩罚的问题,它只是使第6段与评估上述例子的有效性无关。

这里是另一个使用memcpy进行类型惩罚的尝试,我相信第6段将对此进行讨论:

 double d = 1234.5678; void *p = malloc(sizeof(double)); if (p != NULL) { uint64_t *pbits = memcpy(p, &d, sizeof(double)); uint64_t bits = *pbits; printf("the representation of %g is %08"PRIX64"\n", d, bits); } 

假设sizeof(double) == sizeof(uint64_t) ,上面的代码是否已根据第6和7段定义了行为?

编辑:一些答案指出来自阅读陷阱表示的未定义行为的可能性。 这是不相关的,因为C标准明确排除了这种可能性:

7.20.1.1精确宽度整数类型

1 typedef name int N _t指定一个有符号整数类型,其宽度为N ,无填充位和二进制补码表示。 因此, int8_t表示这样的带符号整数类型,其宽度恰好为8位。

2 typedef名称uint N _t指定一个宽度为N且无填充位的无符号整数类型。 因此, uint24_t表示这种无符号整数类型,其宽度恰好为24位。

这些类型是可选的。 但是,如果实现提供宽度为8,16,32或64位的整数类型,没有填充位,并且(对于具有二进制补码表示的有符号类型),它应定义相应的typedef名称。

类型uint64_t恰好有64个值位且没有填充位,因此不能有任何陷阱表示。

有两种情况需要考虑: memcpy()进入一个具有声明类型的对象, memcpy()进入一个没有声明类型的对象。

在第二种情况下,

 double d = 1234.5678; void *p = malloc(sizeof(double)); assert(p); uint64_t *pbits = memcpy(p, &d, sizeof(double)); uint64_t bits = *pi; printf("the representation of %g is %08"PRIX64"\n", d, bits); 

行为确实是未定义的,因为p指向的对象的有效类型将变为double ,并且通过uint64_t类型的左值访问有效类型为double的对象是未定义的。

另一方面,

 double d = 1234.5678; uint64_t bits; memcpy(&bits, &d, sizeof bits); printf("the representation of %g is %08"PRIX64"\n", d, bits); 

没有定义。 C11标准草案n1570:

7.24.1字符串函数约定

3

对于本子条款中的所有函数,每个字符都应解释为它具有unsigned char类型(因此每个可能的对象表示都是有效的并且具有不同的值)。

6.5表达式

7

对象的存储值只能由具有以下类型之一的左值表达式访问:88) – 与对象的有效类型兼容的类型,

– 与对象的有效类型兼容的类型的限定版本,

– 对应于对象的有效类型的有符号或无符号类型,

– 对应于对象有效类型的限定版本的有符号或无符号类型,

– 聚合或联合类型,其成员中包含上述类型之一(包括递归地,子聚合或包含联合的成员),或者

– 一个字符类型。

所以memcpy()本身是明确定义的。

正弦uint64_t bits 具有声明的类型 ,即使其对象表示从double精度复制,它仍保留其类型。

正如chqrlie指出的那样, uint64_t不能有陷阱表示,因此在memcpy()之后访问bits 不是未定义的,只要sizeof(uint64_t) == sizeof(double) 。 但是, bits将取决于实现(例如,由于字节序)。

结论 :只要memcpy()的目标确实具有声明的类型,即不是由[m/c/re]alloc()或等效的分配, memcpy() 可以用于类型惩罚。

你提出了3种方法,它们都与C标准有不同的问题。

  1. 标准库memcpy

     double d = 1234.5678; uint64_t bits; memcpy(&bits, &d, sizeof bits); printf("the representation of %g is %08"PRIX64"\n", d, bits); 

    memcpy部分是合法的(在您的实现中提供sizeof(double) == sizeof(uint64_t) ,这不是每个标准保证的):您通过char指针访问两个对象。

    printf系列不是。 bits表示现在是双倍的。 它可能是uint64_t的陷阱表示,如6.2.6.1General§5中所定义

    某些对象表示不需要表示对象类型的值。 如果对象的存储值具有这样的表示,并且由不具有字符类型的左值表达式读取,则行为是未定义的。 如果这样的表示是由副作用产生的,该副作用通过不具有字符类型的左值表达式修改对象的全部或任何部分,则行为是未定义的。 这种表示称为陷阱表示。

    6.2.6.2整数类型表示明确

    对于unsigned char以外的无符号整数类型,对象表示的位应分为两组:值位和填充位…未指定任何填充位的值。 53

    注53说:

    填充位的某些组合可能会生成陷阱表示,

    如果您知道在您的实现中没有填充位(仍然从未见过……),则每个表示都是有效值,并且print行再次变为有效。 但它只是依赖实现,并且在一般情况下可能是未定义的行为

  2. 联盟

     union { double d; uint64_t i; } u; ud = 1234.5678; printf("the representation of %g is %08"PRIX64"\n", d, ui); 

    联合的成员不共享共同的子序列,并且您正在访问不是最后写入的值的成员。 好的常见实现将给出预期的结果,但是根据标准 ,没有明确定义应该发生什么。 6.5.2.3结构和工会成员§3中的脚注说如果导致与先前案例相同的问题:

    如果用于访问union对象内容的成员与上次用于在对象中存储值的成员不同,则该值的对象表示的适当部分将重新解释为新类型中的对象表示forms在6.2.6中描述(一个过程有时称为“类型双关语”)。 这可能是陷阱表示。

  3. 自定义memcpy

    您的实现仅执行始终允许的字符访问。 它与第一种情况完全相同:实现定义。

根据标准明确定义的唯一方法是将double的表示存储在正确大小的char数组中,然后显示char数组的字节值:

 double d = 1234.5678; unsigned char bits[sizeof(d)]; memcpy(&bits, &d, sizeof(bits)); printf("the representation of %g is ", d); for(int i=0; i 

并且只有实现对char使用恰好8位时,结果才可用。 但它是可见的,因为如果其中一个字节的值大于255,它将显示超过8个六位数。


以上所有内容仅有效,因为bits具有声明的类型。 请参阅@ EOF的答案 ,了解为什么分配的对象会有所不同

我读了第6段的话说,使用memcpy()函数将一系列字节从一个内存位置复制到另一个内存位置可以用于类型惩罚,就像使用具有两种不同类型的union可以用于类型惩罚一样。

第一次提到使用memcpy()表示如果它复制指定的字节数,并且当该变量(左值)用于存储字节时,这些字节将与源目标的变量具有相同的类型。

换句话说,如果你有一个变量double d; 然后,您为此变量(左值)分配一个值,该变量中存储的数据类型为double类型。 然后,如果您使用memcpy()函数将这些字节复制到另一个内存位置,比如变量uint64_t bits; 这些复制字节的类型仍然是double

然后,如果您通过目标变量(左值)访问复制的字节,则uint64_t bits; 在该示例中,该数据的类型被视为用于从该目标变量检索数据字节的左值的类型。 因此,字节被解释(不转换但解释)为目标变量类型,而不是源变量的类型。

通过不同类型访问字节意味着字节现在被解释为新类型, 即使字节实际上没有以任何方式改变

这也是union工作方式。 union不做任何转换。 您将字节存储到一个类型的union成员中,然后通过不同的union成员将相同的字节拉回。 字节是相同的,但字节的解释取决于用于访问存储区的union成员的类型。

我已经看到旧的C源代码中使用的memcpy()函数通过使用struct member offset和memcpy()函数将struct变量的一部分复制到其他struct变量中来帮助将struct划分为多个部分。

因为memcpy()使用的源位置的类型是存储在那里的字节的类型,使用union进行惩罚可以遇到的同类问题也适用于以这种方式使用memcpy()作为数据类型的Endianness 。

要记住的是,无论是使用union还是使用memcpy()方法,复制的字节类型都是源变量的类型,然后当您再次访问数据时,无论是通过union的不同成员还是通过memcpy()的目标变量,字节被解释为目标左值的类型。 但是实际的字节不会改变。

改变 – 见下文

虽然我从未观察到编译器将非重叠源和目标的memcpy解释为执行任何不等同于将源的所有字节读取为字符类型然后将目标的所有字节写为一个字符类型(意思是如果目标没有声明的类型,它将没有有效的类型),标准的语言将允许钝的编译器进行“优化” – 在极少数情况下,编译器将是能够识别和利用它们 – 更有可能破坏原本可行的代码(如果标准写得更好,将会很好地定义),而不是实际提高效率。

至于这是否意味着最好使用memcpy或手动字节复制循环,其目的被充分伪装成无法识别为“复制字符类型数组”,我不知道。 我认为明智的做法是避免任何人如此迟钝,以至于建议一个好的编译器应该在没有这种混淆的情况下产生虚假代码,但是由于过去几年被认为是钝的行为目前很流行,我不知道是否memcpy将成为破坏代码竞争的下一个受害者,编译器几十年来将其视为“明确定义”。

UPDATE

从6.2开始,GCC有时会忽略memmove操作,即使它们是不同类型的指针,它们也会看到目标和源识别相同的地址。 如果稍后将作为源类型写入的存储读取为目标类型,则gcc将假定后一个读取不能识别与先前写入相同的存储。 gcc的这种行为是合理的,因为标准中的语言允许编译器通过memmove复制有效类型。 目前还不清楚这是否是对memcpy规则的故意解释,但是,鉴于gcc在标准明显不允许的某些情况下也会进行类似的优化,例如当一个类型的工会成员时(例如64- bit long )被复制到临时的并从那里复制到具有相同表示的不同类型的成员(例如,64位long long )。 如果gcc发现目标将与临时位置逐位相同,则会省略写入,因此无法注意到存储的有效类型已更改。

可能会给出相同的结果,但编译器不需要保证它。 所以你根本不能依赖它。