【问题标题】:Can I turn unsigned char into char and vice versa?我可以把 unsigned char 变成 char 反之亦然吗?
【发布时间】:2013-02-11 06:51:02
【问题描述】:

我想使用一个需要这样数据的函数:

void process(char *data_in, int data_len);

所以它实际上只是在处理一些字节。

但是当涉及到原始字节时,我更喜欢使用“unsigned char”(它以某种方式“感觉”只处理正的 0 到 255 值更正确),所以我的问题是:

我可以始终安全地将unsigned char * 传递给此函数吗?

换句话说:

  • 是否保证我可以随意在 char 和 unsigned char 之间安全地转换(转换)而不丢失任何信息?
  • 是否可以随意在指向 char 和 unsigned char 的指针之间安全地转换(强制转换)而不丢失任何信息?

奖励:C 和 C++ 中的答案是否相同?

【问题讨论】:

  • 使用 char* 来表示字节是安全的,就像 IO 标准库所做的那样:std::istream& std::istream::read (char* s,streamsize n); std::ostream& std::ostream::write(char*, 流大小);

标签: c++ c


【解决方案1】:

如果您使用显式强制转换,简短的回答是肯定的,但要详细解释它,需要考虑三个方面:

1) 转换的合法性
signed T*unsigned T*(对于某些类型T)之间在任一方向转换通常是可能的,因为源类型可以首先转换为void *(这是标准转换,§4.10)和@987654325 @ 可以使用显式 static_cast (§5.2.9/13) 转换为目标类型:

static_cast<unsigned char*>(static_cast<void *>(data_in))

这可以缩写(§5.2.10/7)为

reinterpret_cast<unsigned char *>(data_in)

因为char 是标准布局类型(§3.9.1/7,8 和 §3.9/9)并且签名不会改变对齐方式(§3.9.1/1)。它也可以写成 C 风格的演员表:

(unsigned char *)(data_in)

同样,这是双向的,从 unsigned*signed* 再返回。还可以保证,如果您以一种方式应用此过程然后再返回,则指针值(即它指向的地址)不会改变(第 5.2.10/7 节)。

所有这些不仅适用于signed char *unsigned char * 之间的转换,还分别适用于char */unsigned char *char */signed char *。 (charsigned charunsigned char 在形式上是三种不同的类型,§3.9.1/1。)

需要明确的是,您使用三种投射方法中的哪一种并不重要,但您必须使用其中一种。仅仅传递指针是行不通的,因为转换虽然合法,但不是标准转换,因此不会隐式执行(如果您尝试,编译器会发出错误)。

2) 访问值的明确定义
如果在函数内部取消引用指针,即执行*data_in 以检索底层字符的泛左值,会发生什么情况;这是明确和合法的吗?这里的相关规则是严格别名规则(§3.10/10):

如果程序尝试通过以下类型之一以外的 glvalue 访问对象的存储值,则行为未定义:

  • [...]
  • 对象的动态类型对应的有符号或无符号类型,
  • [...]
  • charunsigned char 类型。

因此,此规则不允许通过unsigned char*(或char)访问signed char(或char),反之亦然——您应该能够毫无问题地执行此操作。

3) 结果值
在解除对类型转换的指针的引用后,您能否使用您获得的值?重要的是要记住,上述指针的转换和取消引用相当于重新解释(而不是改变!)存储在字符地址处的位模式。那么当有符号字符的位模式被解释为无符号字符的位模式(反之亦然)时会发生什么?

当从无符号变为有符号时,典型效果将是对于 0 到 128 之间的值没有任何反应,而大于 128 的值变为负数。反之亦然:当从有符号变为无符号时,负值将显示为大于 128 的值。

但这种行为实际上并没有得到标准的保证。标准唯一保证的是,对于所有三种类型,charunsigned charsigned char,所有位(不一定是 8,顺便说一句)都用于值表示。因此,如果您将一个解释为另一个,复制几份然后将其存储回原始位置,您可以确定不会丢失信息(根据您的要求),但您不一定知道值是什么实际上意味着(至少不是以完全可移植的方式)。

【讨论】:

  • 这是一个很好的答案,而且很有意义!但是您似乎专门针对 C++(这很棒),但是您能否更新它以包含普通 C 与 C++ 的不同之处?我特别想知道您的最后一段(关于位和信息丢失)是否也保证适用于普通 C。
  • @user2015453 谢谢——我相信所有这些也适用于 C,但我需要花一点时间来检查一下。一旦确定,我会更新答案。
  • 对于 2) 和 3) 事情有点复杂,至少对于 C 来说。这两种类型的表示都不能有填充位,这是正确的。但是签名的类型(signed char 和最终的char,如果它是签名的)可能有一个“陷阱”表示。这将是对应于“负零”的位模式,如果它是实现定义的,这是否是这些类型的有效值。例如,常量 SCHAR_MIN 可能只是 127 而不是 128。不过,我不知道有任何真正的现有架构具有此功能。
  • 有符号整数溢出仍然是 C++ 中未定义的行为。那么将unsigned char 解释为char 不构成UB?
  • @juanchopanza 这是用于有符号整数的算术运算,而不是用于类型转换/赋值。 (如果我没记错的话。)
【解决方案2】:

unsigned charsigned char 只是解释:没有发生转换。

由于您正在处理字节,为了显示意图,最好声明为

void process(unsigned char *data_in, int data_len);

[正如一位编辑所指出的:普通的char 可以是有符号或无符号类型。 C 和 C++ 标准明确允许(它始终是与 unsigned charsigned char 不同的类型,但与其中之一具有相同的范围)]

【讨论】:

  • 嗯,这不是我自己的功能,所以我不能改变它,虽然我想我可以按照你的建议改变声明......
  • @user2015453 如果不能更改定义,请不要更改声明。
【解决方案3】:

是的,您始终可以毫无问题地将 char 转换为 unsigned char &反之亦然。如果您运行以下代码,并将其与 ASCII 表(参考 http://www.asciitable.com/)进行比较,您可以自己查看证明,以及 C/C++ 如何处理转换 - 它们的处理方式完全相同:

#include "stdio.h"


int main(void) {
    //converting from char to unsigned char
    char c = 0;
    printf("%d byte(s)\n", sizeof(char));  // result: 1byte, i.e. 8bits, so there are 2^8=256 values that a char can store.
    for (int i=0; i<256; i++){
        printf("int value: %d - from: %c\tto: %c\n", c,  c, (unsigned char) c);
        c++;
    }

    //converting from unsigned char to char
    unsigned char uc = 0;
    printf("\n%d byte(s)\n", sizeof(unsigned char));
    for (int i=0; i<256; i++){
        printf("int value: %d - from: %c\tto: %c\n", uc, uc, (char) uc);
        uc++;
    }
}

我不会发布输出,因为它有太多行!在输出中可以注意到,在每个部分的前半部分,即从 i=0:127 开始,从 chars 到 unsigned chars 以及 vice-versa 的转换效果很好,没有任何修改或丢失.

但是,从 i=128:255 开始,chars 和 unsigned chars 不能被强制转换,否则您将有不同的输出,因为 unsigned char 保存 [0:256] 中的值,而 char 保存区间 [- 128:127])。尽管如此,这第二部分的行为是无关紧要的,因为在 C/C++ 中,一般来说,您只能以 chars/unsigned chars 作为 ASCII 字符开头,其只能采用 128 个不同的值和其他 128 个值(对于 chars 为正或负对于无符号字符)从不使用。

如果您从未在不代表字符的 char 中输入值,并且从未在不代表字符的 unsigned char 中输入值,那么一切都会好起来的!

额外:即使您在 C/C++ 的字符串中使用 UTF-8 或其他编码(用于特殊字符),使用这种类型转换的所有内容都可以,例如,使用 UTF-8 编码(参考。@ 987654322@):

char hearts[]   = {0xe2, 0x99, 0xa5, 0x00};
char diamonds[] = {0xe2, 0x99, 0xa6, 0x00};
char clubs[]    = {0xe2, 0x99, 0xa3, 0x00};
char spades[]   = {0xe2, 0x99, 0xa0, 0x00};
printf("hearts (%s)\ndiamonds (%s)\nclubs (%s)\nspades (%s)\n\n", hearts, diamonds, clubs, spades);

该代码的输出将是:
心 (♥)
钻石 (♦)
俱乐部 (♣)
黑桃 (♠)

即使您将其每个字符都转换为无符号字符。

所以:

  • “我可以始终安全地将 unsigned char * 传递给此函数吗?” 是的!

  • “是否保证我可以随意在 char 和 unsigned char 之间安全地转换(转换),而不会丢失任何信息?” 是的!

  • “我可以安全地在指向 char 和 unsigned char 的指针之间随意转换(强制转换),而不会丢失任何信息吗?” 是的!

  • “C 和 C++ 中的答案是否相同?” 是的!

【讨论】:

    【解决方案4】:

    您可以将指针传递给不同类型的char,但您可能需要显式转换它。保证指针具有相同的大小和相同的值。转换过程中不会丢失任何信息。

    如果您想在函数内将char 转换为unsigned char,只需将char 值分配给unsigned char 变量或将char 值转换为unsigned char

    如果您需要在不丢失数据的情况下将unsigned char 转换为char,这有点困难,但仍然可以:

    #include <limits.h>
    
    char uc2c(unsigned char c)
    {
    #if CHAR_MIN == 0
      // char is unsigned
      return c;
    #else
      // char is signed
      if (c <= CHAR_MAX)
        return c;
      else
        // ASSUMPTION 1: int is larger than char
        // ASSUMPTION 2: integers are 2's complement
        return c - CHAR_MAX - 1 - CHAR_MAX - 1;
    #endif
    }
    

    此函数会将unsigned char 转换为char,从而可以将返回的值转换回与参数相同的unsigned char 值。

    【讨论】:

    • 所以当我将 unsigned char 分配给 char 时,这种“环绕”不会自动成为语言?
    • 它只对无符号类型自动完成。有符号整数中的溢出会导致未定义的行为。您的编译器可能以与无符号溢出相同的方式处理有符号溢出这一事实要么是运气,要么是记录在案的特性。当另一个编译器发现可能存在未定义行为时,它可能会完全破坏您的代码。
    • @AlexeyFrunze:考虑一下UCHAR_MAX == 255CHAR_MIN == -127CHAR_MAX == 127unsigned char 可以代表多少个不同的值? char 可以表示多少个不同的值?当c == CHAR_MAX + 1 时,您的代码中可能会出现未定义的行为,因为可能没有转换为它的有符号值。我建议:if (c &lt;= CHAR_MAX) { return c; } else if (c &lt; (unsigned char) CHAR_MIN) { /* negative zero */ return 0; } else { return -(UCHAR_MAX - c + 1); }
    • 当从无符号到有符号的转换过程中发生溢出时,结果是实现定义的(§4.7/3),但不是UB。 (UB 发生在对有符号类型执行的算术运算导致溢出时。)
    • @jogojapan 是的,你是对的,它要么是实现定义的值,要么是实现定义的信号(至少在 C99 中)。我总是忘记这种微妙的区别。
    【解决方案5】:

    在语义上,unsigned char *char * 之间的 传递 是安全的,即使在它们之间进行转换,就像在 c++ 中一样。

    但是,请考虑以下示例代码:

    #include "stdio.h"
    
    void process_unsigned(unsigned char *data_in, int data_len) {
        int i=data_len;
        unsigned short product=1;
    
        for(; i--; product*=data_in[i]) 
            ;
    
        for(i=sizeof(product); i--; ) {
            data_in[i]=((unsigned char *)&product)[i];
            printf("%d\r\n", data_in[i]);
        }
    }
    
    void process(char *data_in, int data_len) {
        int i=data_len;
        unsigned short product=1;
    
        for(; i--; product*=data_in[i]) 
            ;
    
        for(i=sizeof(product); i--; ) {
            data_in[i]=((unsigned char *)&product)[i];
            printf("%d\r\n", data_in[i]);
        }
    }
    
    void main() {
        unsigned char 
            a[]={1, -1}, 
            b[]={1, -1};
    
        process_unsigned(a, sizeof(a));
        process(b, sizeof(b));
        getch();
    }
    

    输出:

    0 255 -1 -1

    process_unsignedprocess 中的所有代码都只是相同。唯一的区别是无符号和有符号。此示例表明,黑匣子中的代码确实受到SIGN的影响,并且在被调用者和调用者之间保证没有

    所以我想说,它只适用于通过,但不能保证任何其他可能性。

    【讨论】:

      【解决方案6】:

      您确实需要查看process() 的代码,以了解您是否可以安全地传递无符号字符。如果函数使用字符作为数组的索引,那么不,您不能使用无符号数据。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2021-01-01
        • 1970-01-01
        • 2017-02-05
        • 2022-01-09
        • 1970-01-01
        • 1970-01-01
        • 2014-12-04
        • 1970-01-01
        相关资源
        最近更新 更多