按位AND和签名字符

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

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

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

C99规范的第6.5节告诉我:

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

我的问题是双重的:

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

  • 是否存在这些“实现定义方面”的列表(例如MVSC和GCC)?

或者您可以采用不同的路径并认为这对于任何c[i]值的有符号和无符号字符产生相同的结果。

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

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

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


6.3.1.3有符号和无符号整数

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


这基本上是指定结果将是值的二进制补码表示。

对此的基本原理是,在二进制补码数学中,计算结果以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标志的解释)是另一回事,但这是一个实施细节)。 该标准的存在是为了确保一个实现不会自己做一些奇怪的事情,或者我想,对于一些不使用二进制补码的奇怪架构……

unsigned char UC = *(unsigned char*)&C – 这是你可以将signed C转换为unsigned保持“位模式”的方法。 因此,您可以将代码更改为以下内容:

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

说明(带参考):

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

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

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

你似乎有类似的东西:

 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]在被提升为int之前被转换为unsigned char (或者,编译器可能会提升为int ,然后强制转换为unsigned char ,然后将unsigned char提升回int ),并且unsigned value是用于&操作。

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

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

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

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

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

C11 6.3.1.1

“如果int可以表示原始类型的所有值(由宽度限制,对于位字段),则该值将转换为int;否则,将转换为unsigned int。这些称为整数提升“。

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

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

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

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

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

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

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

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

然后,如果我们知道我们的hex, (0xc0 & 0x80)是没有意义的,它总是正确的。 x & 0xC0 & 0x80始终与x & 0x80相同。 因此,将表达式简化为:

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

“在任何地方都有这些”实施定义方面“的清单

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

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