【问题标题】:Implement `memcpy()`: Is `unsigned char *` needed, or just `char *`?实现 `memcpy()`:是否需要`unsigned char *`,还是只需要`char *`?
【发布时间】:2019-07-24 15:29:04
【问题描述】:

我正在实现memcpy() 的一个版本,以便能够将它与volatile 一起使用。 使用char * 是否安全,还是需要unsigned char *

volatile void *memcpy_v(volatile void *dest, const volatile void *src, size_t n)
{
    const volatile char *src_c  = (const volatile char *)src;
    volatile char *dest_c       = (volatile char *)dest;

    for (size_t i = 0; i < n; i++) {
        dest_c[i]   = src_c[i];
    }

    return  dest;
}

如果缓冲区的任何单元格中的数据是&gt; INT8_MAX(我认为可能是UB),我认为unsigned 应该是避免溢出问题所必需的。

【问题讨论】:

  • 为什么你会认为它会是 UB?是否都允许访问所有位,而不管这些位基于特定类型可能代表什么数字?
  • 好的,不管它最初是什么,它都在重新解释数据,对吧?该值可能是未定义的,但总体结果是您将其原封不动地返回,并且不使用该值,所以它最终是定义的行为,对吧?
  • 未定义的行为不会得到“未定义”;这是没有意义的。但我也没有看到你在这里感知 UB 的位置。不要只是“我认为可能是 UB”,请扩展​​以清楚地详细地呈现您的问题。
  • 您正在读取一个可能完全是(uint8_t)200 的值,并且在您将其取消引用为(可能是signedchar * 的那一刻,该值神奇地转换为负值。我从来不需要做任何类型的双关语,所以我不知道什么是允许的,什么是不允许的,但这至少对我来说似乎很奇怪。
  • 你也许可以证明普通的char 是安全的,但是仅仅使用unsigned char 就可以省去你所有的努力。 (如果有问题,它可能会在实现中显示-0 值/表示,其中普通char 已签名并且不使用2 的补码。我怀疑是否存在任何此类系统。)

标签: c pointers casting char unsigned


【解决方案1】:

理论上,您的代码可能会在一台机器上运行,该机器禁止签名 char 中的一位模式。它可能使用负整数的补码或符号幅度表示,其中一位模式将被解释为带负号的 0。即使在二进制补码架构上,该标准也允许实现限制负整数的范围,以便 INT_MIN == -INT_MAX,尽管我不知道有任何实际机器可以做到这一点。

因此,根据 §6.2.6.2p2,可能存在一个有符号字符值,实现可能会将其视为陷阱表示:

这些[负整数的表示]中哪一个适用是实现定义的,符号位为1且所有值位为零的值(对于前两个[符号幅度和二进制补码]),还是带有符号位和所有值位 1(用于反码)是陷阱表示或正常值。在符号和幅度以及一个的补码的情况下,如果这个表示是一个正常值,它被称为一个负零

(字符类型不能有任何其他陷阱值,因为 §6.2.6.2 要求 signed char 没有任何填充位,这是形成陷阱表示的唯一其他方式。出于同样的原因,没有位模式是unsigned char 的陷阱表示。)

因此,如果这个假设的机器有一个 C 实现,其中 char 被签名,那么通过 char 复制任意字节可能会涉及复制陷阱表示。

对于char(如果它恰好是有符号的)和signed char 以外的有符号整数类型,读取作为陷阱表示的值是未定义的行为。但是 §6.2.6.1/5 允许读取和写入这些值仅用于字符类型

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

(第三句话有点笨拙,但为了简化:将值存储到内存中是“修改所有对象的副作用”,因此也是允许的。)

简而言之,由于该异常,您可以在 memcpy 的实现中使用 char,而不必担心未定义的行为。

但是,strcpy 并非如此。 strcpy 必须检查终止字符串的尾随 NUL 字节,这意味着它需要将从内存中读取的值与 0 进行比较。比较运算符(实际上是所有算术运算符)首先对其操作数执行整数提升,这会将char 转换为int。据我所知,陷阱表示的整数提升是未定义的行为,因此在假设机器上运行的假设 C 实现上,您需要使用 unsigned char 才能实现 strcpy

【讨论】:

  • 在使用非 2 补码的平台上,(是的,它们现在是遗物),char a = { negative 0 }; char b;b = a,必须 b 承担 '-0' or can the pesky -0` 成为 0因为它被分配给b?。如果这是真的,那么尽管关于陷阱值的讨论很好,并且使用 char 确实避免了 UB,但它并不能确保 for (size_t i = 0; i &lt; n; i++) { dest_c[i] = src_c[i]; } 按预期执行:将二进制图像形式复制到那里。
  • 这个冗长的语言法律解释应该说服读者使用char类型来操作字节。需要一位非常精明的 C 专家来解释为什么使用 char 是安全的,其中 unsigned char 对于普通程序员来说是一个明显的解决方案。使代码尽可能简单,使用unsigned char
  • @chux:有没有曾经没有对char使用二进制补码表示的符合C99或C11实现?
  • @supercat,据我所知,没有。经常提到的 Unisys ClearPath OS 2200 有一个针对 C90 的 C 编译器,而且似乎没有任何计划支持 C99 或任何更新的版本。 (尽管 C90 编译器仍在继续维护。)根据提案作者的说法,目前正在讨论从 C2x(和 C++2x)中删除除二进制补码之外的表示形式,并且在委员会的讨论中没有出现其他示例。跨度>
  • @rici:标准应该识别二进制补码实现的类别,其中字节以大端方式组装成更长的类型而没有填充位,这是一种二进制补码实现的类别,它们被组装在没有填充位的小端方式,以及对实现没有特别要求的“任何事情”类别。我看不出有任何理由提及反码或符号幅度的选择,但我也认为没有必要明确地将事物限制为二元补码。
【解决方案2】:

使用char * 是否安全,还是需要unsigned char *

也许


memcpy()等“字符串处理”函数有规范:

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

使用unsigned char 是指定的“好像”类型。尝试其他方法收效甚微——这可能有效,也可能无效。


charmemcpy() 一起使用可能有效,但将该范例扩展到其他类似功能会导致问题。

避免char 使用str...()mem...() 类似函数的一个重要原因是,有时它会产生出乎意料的功能差异。

memcmp(), strcmp() 肯定与 (signed) charunsigned char 不同。

Pedantic:在带有 signed char 的 relic 非 2 补码上,只有 '\0' 应该以 string 结尾。然而negative_zero == 0charnegative_zero 不应表示字符串 的结尾。

【讨论】:

    【解决方案3】:

    你不需要unsigned

    像这样:

    volatile void *memcpy_v(volatile void *dest, const volatile void *src, size_t n)
    {
        const volatile char *src_c  = (const volatile char *)src;
        volatile char *dest_c       = (volatile char *)dest;
    
        for (size_t i = 0; i < n; i++) {
            dest_c[i]   = src_c[i];
        }
    
        return  dest;
    }
    

    尝试在char 具有陷阱值的情况下进行确认实现最终会导致矛盾:

    • fopen("", "rb") 不需要仅使用fread()fwrite()
    • fgets()char * 作为其第一个参数,可用于二进制文件。
    • strlen() 查找从给定 char * 到下一个空值的距离。由于fgets() 保证写了一个,它不会读到数组的末尾,因此不会陷阱

    【讨论】:

    • 如果char 有陷阱表示怎么办?
    • @EricPostpischil:在内存中?这是不允许的。我可以翻出我的 K&R 书,但它不会告诉我任何办法。只有未初始化的局部变量和指针类型会这样做。
    • 是的,char may have a trap representation。 K&R 不相关;我们现在使用 ISO C。关于具有潜在未定义行为的未初始化局部变量是与陷阱表示无关的特殊规则。
    • freadfwritefgets可以和二进制文件一起使用,所以必须支持char读写任意数据的推理是不正确的。根据 C 2018 7.21.2 3,“二进制流是可以透明地记录内部数据的有序字符序列。在相同的实现下,从二进制流中读取的数据应与之前写入该流中的数据进行比较。” C 的流只需要支持写入数据的表示形式并将其读回相同的类型,结果在类型中比较相等。
    • @EricPostpischil:有没有任何非人为的 C99 或 C17 实现,其中 CHAR_MAX-CHAR_MIN 是偶数?有可能吗?如果没有,为什么还要关心 ISO C 是否允许这样的事情?
    【解决方案4】:

    unsigned 不是需要,但没有理由为此函数使用普通的char。普通的char 应该只用于实际的字符串。对于其他用途,unsigned charuint8_tint8_t 类型更精确,因为签名是明确指定的。

    如果要简化函数代码,可以去掉强制转换:

    volatile void *memcpy_v(volatile void *dest, const volatile void *src, size_t n) {
        const volatile unsigned char *src_c = src;
        volatile unsigned char *dest_c = dest;
    
        for (size_t i = 0; i < n; i++) {
            dest_c[i] = src_c[i];
        }
        return dest;
    }
    

    【讨论】:

    • 同意charunsigned char。在这种情况下,uint8_t 是否与unsigned char 一样有效?在没有发生类型双关的任何其他情况下,我总是使用uint8_t 作为字节,但在这种情况下我不知道。
    • uint8_t 不一定可用:它被指定为恰好具有 8 个值位和二进制补码表示。在拥有它的系统上,它必须与unsigned char 的类型相同,因为unsigned char 的大小必须为1 字节,不能小于8 位。 (有人可能会争辩说,char 将有一个补码表示的故意不正当系统也可能有一个单独的带有二进制补码表示的 uint8_t 类型,但我将这个讨论留给 DS9K 实现者)。
    • @AnttiHaapala:你说得对,我指的是int8_t关于负数的表示,它必须是二进制补码。
    猜你喜欢
    • 2010-09-08
    • 2017-02-09
    • 1970-01-01
    • 1970-01-01
    • 2014-03-15
    • 1970-01-01
    • 2012-03-07
    • 2013-04-29
    相关资源
    最近更新 更多