memcpy可用于打字吗?

时间:2016-07-27 00:10:54

标签: c type-conversion language-lawyer

这是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;
u.d = 1234.5678;
printf("the representation of %g is %08"PRIX64"\n", d, u.i);

如果我使用我的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名称int N _t指定有符号整数类型,宽度为 N ,无填充位和二进制补码表示。因此,int8_t表示这样的有符号整数类型,其宽度恰好为8位。

     

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

     

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

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

5 个答案:

答案 0 :(得分:8)

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

在第二种情况下,

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

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

另一方面,

uint64_t

未定义。 C11标准草案n1570:

  

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

并且

  

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

     
    

- 与对象的有效类型兼容的类型,
     - 与对象的有效类型兼容的类型的限定版本,
     - 与对象的有效类型对应的有符号或无符号类型的类型,
     - 对应于合格版本的有符号或无符号类型的类型     有效的对象类型,
     - 包含上述类型之一的聚合或联合类型     其成员之间(包括递归地,分包或联合的成员)或者      - 字符类型。

  
     

脚注88)此列表的目的是指定对象可能存在或不存在别名的情况。

因此double d = 1234.5678; uint64_t bits; memcpy(&bits, &d, sizeof bits); printf("the representation of %g is %08"PRIX64"\n", d, bits); 本身定义明确。

由于memcpy() 具有声明的类型,即使其对象表示从uint64_t bits复制,它也会保留其类型。

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

结论bits 可以用于打字,前提是memcpy()的目的地确实有声明的类型,即不是由memcpy()或同等分配。

答案 1 :(得分:4)

你提出了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;
    u.d = 1234.5678;
    printf("the representation of %g is %08"PRIX64"\n", d, u.i);
    

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

      

    如果用于访问union对象内容的成员与上次使用的成员不同   在对象中存储一个值,该值的对象表示的相应部分被重新解释   作为6.2.6中描述的新类型中的对象表示(有时称为“类型”的过程)   惩罚“)。这可能是一个陷阱表示。

  3. 自定义memcpy

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

  4. 每个标准明确定义的唯一方法是将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<sizeof(bits); i++) {
        printf("%02x", (unsigned int) bits[i]);
    }
    printf("\n");
    

    如果实现仅使用char的8位,结果将仅可用。但它是可见的,因为如果其中一个字节的值大于255,它将显示超过8个六位数。

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

答案 2 :(得分:2)

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

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

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

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

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

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

我已经看到旧版C源代码中使用的memcpy()函数通过使用struct成员偏移量和struct函数将memcpy()分成几部分将struct变量的部分复制到其他struct变量中。

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

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

答案 3 :(得分:2)

已更改 - 请参阅以下

虽然我从未观察到编译器将不重叠的源和目标的memcpy解释为执行任何不等同于将源的所有字节读取为字符类型然后写入所有字节的内容目的地作为一个字符类型(意味着如果目的地没有声明的类型,它将没有有效的类型),标准的语言将允许钝的编译器进行&#34;优化&#34;哪些 - 在编译器能够识别和利用它们的极少数情况下 - 更有可能破坏否则可以工作的代码(如果标准写得更好,将会很好地定义)而不是实际提高效率

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

<强>更新

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

答案 4 :(得分:1)

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