在有符号字符上按位与

Posted

技术标签:

【中文标题】在有符号字符上按位与【英文标题】:Bitwise AND on signed chars 【发布时间】:2013-01-09 10:57:08 【问题描述】:

我有一个文件已读入数据类型为signed char 的数组中。我无法改变这个事实。

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

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

C99 规范的第 6.5 节告诉我:

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

我的问题有两个:

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

在任何地方(比如 MVSC 和 GCC)是否有这些“实现定义的方面”的列表?

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

我自然会奖励参考相关标准或权威文本,劝阻“知情”猜测。

【问题讨论】:

不引用标准或任何东西,但你不在这里回答你自己的问题吗? “我如何转换/投射” @fge,不知道能不能保证成功。 @Richard 如果您进行类型转换,那么位模式不能保持不变。 请注意!((c[i] & 0xc0) & 0x80)等价于!(c[i] & 0x80),两个and没有意义。 我不是语言律师类型,但从有符号类型到无符号类型的 IIRC 转换是明确定义的(我认为它被定义为添加 INT_MAX+1 以使结果为正,并且作为无符号值包装,一切都按预期工作)。 【参考方案1】:

正如其他人指出的那样,您的实现很可能是基于二进制补码的,并且会给出您期望的结果。

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


6.3.1.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 标志的数量是另一回事,但这是一个实现细节)。该标准的存在是为了确保实现不会自己做一些奇怪的事情,或者我想,对于一些不使用二进制补码的奇怪架构......

【讨论】:

我不确定,如果你的例子多一点,那么它会很有帮助..在我看来,你回答了这个问题.. @GrijeshChauhan 不,它没有。 (无符号类型的数学定义为 2 的补码,没有任何溢出问题 - 即它会换行,因此所有这一切的结果是“位模式”将保持不变) 是的,您是正确的,此代码 char c =-128; printf("%d %d\n",(unsigned char)c, c); 打印 128 -128 并且 char(128)==char(-128) 的位模式对于 1 个字节是相同的。好答案+1.【参考方案2】:

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指针相同的字节。

【讨论】:

根据我的问题,@kerim,您应该参考标准或权威资料来解释您的推理。这属于“知情”推测的范畴。 @Richard,我的回答有什么不清楚的地方?您可以convert 指针,指向的数据不会在您这样做时改变,有符号/无符号字符的大小相同......您还能想到什么? @kerim 兄弟,你没有测试所有可能的情况..并且在 typecase 位模式肯定会改变......再想一想,再试一次 @GrijeshChauhan - 请帮助我理解我错在哪里。我只是无法开箱即用,看不到这里的问题.... @Richard 您最初引用了一个已经过时 23 年的撤回标准的早期草案。所以你真的不能抱怨别人缺乏可靠的来源。【参考方案3】:

你似乎有类似的东西:

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),并且在& 操作中使用无符号值。

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

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

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

【讨论】:

【参考方案4】:

“由于我想使用文件中的原始位模式, 如何将我的签名字符转换/转换为无符号字符,以便该位 模式保持不变?”

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

C11 6.3.1.1

"如果一个 int 可以表示原始类型的所有值(如 受宽度限制,对于位域),值转换为 一个整数;否则,它将转换为无符号整数。这些都是 称为整数促销。”

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

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

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

然后按位 & 运算符对每个位执行 AND,无论其整数操作数 (C11 6.5.10/3) 的内容如何,​​无论它是否有符号。如果您在原始签名字符的签名位中有数据,那么它现在将丢失。因为整数字面量(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 行为没有多说。等等。唯一明确提到它的运算符是 >,其中左移负数甚至是未定义的行为,但右移是实现定义的。

【讨论】:

以上是关于在有符号字符上按位与的主要内容,如果未能解决你的问题,请参考以下文章

位运算(按位与按位或异或)

位运算(按位与按位或异或)

位运算符按位与按位或按位非左移右移原码反码补码

运算符的计算(按位与按位或异或取反)以及原码反码补码

C语言里的按位异或运算符

c语言的按位运算符怎么操作!?