按位AND和签名字符

时间:2013-01-09 10:57:08

标签: c bit-manipulation

我有一个文件,我已经读入数据类型signed char的数组。我不能改变这个事实。

我现在想这样做:!((c[i] & 0xc0) & 0x80)其中c[i]是签名字符之一。

现在,我从C99 standard的第6.5.10节知道“每个操作数[按位AND]应该具有整数类型。”

C99规范的第6.5节告诉我:

  

一些运算符(一元运算符〜,二元运算符<<,>>,&,^和|,   统称为按位运算符)应具有具有整数类型的操作数。   这些运营商回归   取决于整数的内部表示的值,和   因此,对于签名类型,实现定义方面

我的问题是双重的:

  • 由于我想使用文件中的原始位模式,如何将signed char转换/转换为unsigned char以使位模式保持不变?

  • 在任何地方都有这些“实现定义方面”的列表(例如MVSC和GCC)吗?

或者你可以采取不同的路线并争辩说,对于c[i]的任何值,这对于有符号和无符号字符都会产生相同的结果。

当然,我会奖励对相关标准或权威文本的引用,并阻止“知情”的推测。

4 个答案:

答案 0 :(得分:5)

正如其他人所指出的那样,在所有情况下,你的实现都是基于两个补码,并且会给出你期望的结果。

但是,如果您担心涉及有符号值的操作的结果,并且您关心的是位模式,则只需直接转换为等效的无符号类型。结果在标准下定义:


6.3.1.3有符号和无符号整数

  1. ...

  2. 否则,如果新类型是无符号的,则通过重复添加或转换该值 减去一个可以在新类型中表示的最大值 直到该值在新类型的范围内。


  3. 这实际上是指定结果将是值的二进制补码表示。

    基本原理是,在二进制补码数学中,计算结果以2的幂(即该类型中的位数)为模,这反过来恰好相当于屏蔽相关的位数。而数字的补码是从2的幂中减去的数字。

    因此,添加负值与添加任何值的值相同,该值与该值相差2倍。

    即:

            (0 + signed_value) mod (2^N)
    ==
          (2^N + signed_value) mod (2^N)
    ==
      (7 * 2^N + signed_value) mod (2^N)
    

    等。 (如果你知道模数,那应该是非常明显的真实)

    因此,如果你有一个负数,增加2的幂将使其为正(-5 + 256 = 251),但是底部的'N'位将完全相同(0b11111011)并且它不会影响数学运算的结果。因为值被截断以适合类型,结果就是你期望的二进制值,即使结果“溢出”(即如果数字为正数,你可能认为会发生这种情况 - 这种包装也是明确定义的行为)。

    所以在8位二进制补码中:

    • -5与251(即256-5)-0b11111011
    • 相同
    • 如果添加30和251,则得到281.但是大于256,而281 mod 256等于25.与30 - 5完全相同。
    • 251 * 2 = 502. 502 mod 256 = 246. 246和-10均为0b11110110。

    同样,如果你有:

    unsigned int a;
    int b;
    
    a - b == a + (unsigned int) -b;
    

    在幕后,这种演员阵容不太可能通过算术实现,并且肯定会是从一个寄存器/值到另一个寄存器/值的直接分配,或者只是完全优化,因为数学不区分有符号和无符号(解释) CPU标志是另一回事,但这是一个实现细节)。该标准的存在是为了确保一个实现不会自己做一些奇怪的事情,或者我想,对于一些不使用二进制补码的奇怪架构......

答案 1 :(得分:1)

unsigned char UC = *(unsigned char*)&C - 这是您将签名的C转换为无符号保留“位模式”的方法。因此,您可以将代码更改为:

!(( (*(unsigned char*)(c+i)) & 0xc0) & 0x80)

解释(带参考):

761 当指向对象的指针转换为指向字符类型的指针时,结果指向对象的最低寻址字节

1124 当应用于具有char,unsigned char或signed char(或其限定版本)类型的操作数时,结果为1

这两个意味着unsigned char指针指向与原始signed char指针相同的字节。

答案 2 :(得分:0)

您似乎有类似的内容:

signed char c[] = "\x7F\x80\xBF\xC0\xC1\xFF";

for (int i = 0; c[i] != '\0'; i++)
{
    if (!((c[i] & 0xC0) & 0x80))
        ...
}

您(正确)关注signed char类型的符号扩展。但实际上,(c[i] & 0xC0)会将签名字符转换为(带符号)int,但& 0xC0将丢弃更重要字节中的任何设置位;表达式的结果将在0x00 .. 0xFF范围内。我相信,无论你使用符号和数字,一个补码还是两个补码二进制值,这都是适用的。您获得的特定签名字符值的详细位模式取决于基础表示;但结果将在0x00 .. 0xFF范围内的总体结论是有效的。

可以轻松解决此问题 - 在使用之前将c[i]的值转换为unsigned char

if (!(((unsigned char)c[i] & 0xC0) & 0x80))

c[i]在升级为unsigned char之前会转换为int(或者,编译器可能会升级到int,然后强制转换为unsigned char然后将unsigned char提升回int),并在&操作中使用无符号值。

当然,代码现在只是多余的。使用& 0xC0后跟& 0x80完全等同于& 0x80

如果您正在处理UTF-8数据并查找延续字节,则正确的测试是:

if (((unsigned char)c[i] & 0xC0) == 0x80)

答案 3 :(得分:0)

  

“因为我想使用文件中的原始位模式,   如何将我的签名字符转换/转换为unsigned char以使该位   模式保持不变?“

正如有人在之前对同一主题的问题回答中所解释的那样,任何小整数类型(无论是有符号还是无符号),只要在表达式中使用,都会被提升为int类型。

C11 6.3.1.1

  

“如果int可以表示原始类型的所有值(如   受宽度限制,对于位字段),该值被转换为   一个int;否则,它将转换为unsigned int。这些是   称为整数促销。“

此外,正如同一答案所解释的,整数文字总是int类型。

因此,您的表达式将归结为伪代码(int) & (int) & (int)。将对三个临时int变量执行操作,结果将为int类型。

现在,如果原始数据包含可能被解释为特定签名表示的符号位的位(实际上这将是所有系统上的两个补码),您将遇到问题。因为这些位将在从signed char升级到int时保留。

然后是比特&无论其整数操作数(C11 6.5.10 / 3)的内容如何,​​运算符都会对每个位执行AND,无论是否有符号。如果您在原始签名字符的签名位中有数据,则现在将丢失。因为整数文字(0xC0或0x80)没有设置与符号位对应的位。

解决方案是防止符号位转移到“临时int”。一种解决方案是将c [i]转换为unsigned char,这是完全明确定义的(C11 6.3.1.3)。这将告诉编译器“这个变量的整个内容是一个整数,没有符号位需要关注”。

更好的是,养成在每种形式的位操作中始终使用无符号数据的习惯。纯粹的,100%安全的,符合MISRA-C标准的重写表达方式是:

if ( ((uint8_t)c[i] & 0xc0u) & 0x80u) > 0u)

u后缀实际上强制表达式为unsigned int,但最好始终强制转换为预期类型。它告诉读者代码“我实际上知道我在做什么,我也理解C中所有奇怪的隐式促销规则”。

然后,如果我们知道我们的十六进制,(0xc0 & 0x80)毫无意义,它总是如此。 x & 0xC0 & 0x80始终与x & 0x80相同。因此,将表达式简化为:

if ( ((uint8_t)c[i] & 0x80u) > 0u)

  

“在任何地方都有”实施定义方面“的列表

是的,C标准在附录J.3中方便地列出了它们。在这种情况下,您遇到的唯一实现定义方面是整数的签名实现。在实践中,这总是两个补充。

编辑: 问题中引用的文本涉及各种逐位运算符将产生实现定义的结果。这只是简单地提到了实现定义,即使在附录中没有确切的参考。实际的第6.5章并没有对&的impl.defined行为说太多。 |明确提到它的唯一运算符是<<和>>,左移移负数甚至是未定义的行为,但右移它是实现定义的。