4

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

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

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

C99 规范的第 6.5 节告诉我:

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

我的问题有两个:

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

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

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

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

4

4 回答 4

5

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

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


6.3.1.3 有符号和无符号整数

  1. ...

  2. 否则,如果新类型是无符号的,则通过在新类型中可以表示的最大值的基础上反复加减一,直到该值在新类型的范围内。


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

其基本原理是,在二进制补码数学中,计算结果是以 2 的某个幂为模(即类型中的位数),这反过来又完全等同于屏蔽相关的位数。数字的补码是从 2 的幂中减去的数字。

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

IE:

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

于 2013-01-09T11:42:23.587 回答
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

于 2013-01-09T11:08:35.827 回答
0

“由于我想使用文件中的原始位模式,我怎样才能将我的有符号字符转换/转换为无符号字符,以便位模式保持不变?”

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

C11 6.3.1.1

“如果 int 可以表示原始类型的所有值(受宽度限制,对于位域),则该值将转换为 int;否则,将其转换为无符号 int。这些称为整数提升。”

此外,正如在同一个答案中所解释的,整数文字始终是 type 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 行为没有多说。等等。唯一明确提到它的运算符是 << 和 >>,其中左移负数甚至是未定义的行为,但右移是实现定义的。

于 2013-01-09T12:39:00.030 回答
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)会将带符号的字符转换为 (signed) int,但& 0xC0会丢弃更高有效字节中的任何设置位;表达式的结果将在 0x00 .. 0xFF 范围内。我相信,无论您使用符号和幅度、一个补码还是二进制补码二进制值,这都将适用。您获得的特定有符号字符值的详细位模式因底层表示而异;但总体结论是结果将在 0x00 .. 0xFF 范围内是有效的。

有一个简单的解决方案来解决这个问题——在使用它之前将它的值c[i]转换为unsigned char

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

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

当然,现在的代码只是多余的。使用& 0xC0follow by& 0x80完全等同于 just & 0x80

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

if (((unsigned char)c[i] & 0xC0) == 0x80)
于 2013-01-09T12:23:27.440 回答