按位运算符和有符号类型

Bitwise operators and signed types

本文关键字:符号 类型 运算符      更新时间:2023-10-16

我正在阅读C++入门,我对一些评论感到有些困惑,这些评论讨论了按位运算符如何处理有符号类型。我会引用:

报价 #1

(在谈论按位运算符时)"如果操作数是有符号的,并且 它的值为负数,然后处理"符号位"的方式 许多按位运算都与计算机相关。此外 执行左移以更改符号位的值是 未定义"

报价 #2

(当谈论右移运算符时)"如果该操作数是 无符号,则运算符在左侧插入 0 值位;如果它 是有符号类型,结果是实现定义 - 要么复制 的符号位或 0 值位

插入左侧。

按位运算符将小整数(如 char)提升为有符号整数。当按位运算符经常对有符号运算符类型提供未定义或实现定义的行为时,这种对有符号整数的提升是否存在问题?为什么标准不将 char 提升为无符号 int?


编辑:这是我拿出来的问题,但我把它放回上下文,下面有一些答案。

一个练习稍后问

"在具有 32 位 int s 和 8 位 char s 的机器上,~'q' << 6的值是多少,它使用拉丁语 1 字符集,其中'q'具有01110001位模式

?"

好吧,"q"是一个字符文字,将被提升为 int,给出

~'q' == ~0000000 00000000 00000000 01110001 == 11111111 11111111 11111111 10001110

下一步是对上面的位应用左移运算符,但正如引用 #1 提到的

"做一个左移来改变符号位的值是 未定义"

好吧,我不完全知道哪个位是符号位,但答案肯定是不确定的?

你说得很对——表达式~'q' << 6是根据标准定义的未定义行为。 它比你说的更糟糕,因为~运算符被定义为计算值的"一补码",这对于有符号(2s补码)整数毫无意义——术语"一个人的补码"只对无符号整数真正意味着任何东西。

执行按位运算时,如果需要严格定义良好(根据标准)的结果,通常必须确保所操作的值是无符号的。 您可以使用显式强制转换或在二进制操作中使用显式无符号常量(U -后缀)来执行此操作。 使用有符号和无符号 int 执行二进制操作将作为无符号完成(有符号值将转换为无符号)。

C

和 C++ 与整数提升略有不同,所以这里需要小心——C++在与其他操作数进行比较之前,会将小于 int 的无符号值转换为 int(有符号),看看应该做什么,而 C 将首先比较操作数。

阅读标准的确切文本可能是最简单的,而不是像 Primer Plus 那样的摘要。(摘要必须省略细节,因为是摘要!

相关部分是:

[换班]

  1. 班次操作员从左到右<<>>分组。 操作数应为整型或无作用域枚举类型,并执行整型升级。结果的类型是提升的左操作数的类型。如果右操作数为负数,或者大于或等于提升的左操作数的长度(以位为单位),则行为未定义。

  2. E1 << E2的值E1 E2位位置左移;空出的位填充为零。如果E1具有无符号类型,则结果的值为 E1 × 2 E2,比结果类型中可表示的最大值多减少模 1。否则,如果E1具有有符号类型和非负值,并且 E2 E1 × 2结果类型的相应无符号类型中表示,则该值转换为结果类型是结果值;否则,行为是未定义的。

[expr.unary.op]/10

˜ 的操作数应具有整型或无作用域枚举类型;结果是其操作数的补码。进行整体促销。结果的类型是升级的操作数的类型。

请注意,它们都不执行通常的算术转换(这是大多数二进制运算符完成的转换为通用类型)。

整体促销:

[会议舞会]/1

boolchar16_tchar32_twchar_t 以外的整数类型的 prvalue 如果整数转换秩小于 int 秩,则可以转换为类型 int 的 prvalue,如果int可以表示源类型的所有值;否则,源 prvalue 可以转换为 unsigned int 类型的 prvalue。

("其他"列表中还有其他类型的条目,我在这里省略了它们,但您可以在标准草案中查找)。


关于整数提升需要注意的是,它们是保值的,如果你有价值-30 char,那么提升后它将是一个价值-30 int。 您无需考虑"符号扩展"之类的事情。

您对~'q'的初始分析是正确的,并且结果的类型为 int(因为int可以表示正常系统上char的所有值)。

事实证明,任何设置了最高有效位的int都表示负值(在标准的另一部分中有关于此的规则,我在这里没有引用),因此~'q'是一个负int

查看 [expr.shift]/2,我们看到这意味着左移会导致未定义的行为(该段前面的任何情况都没有涵盖)。

当然,通过编辑问题,我的答案现在部分回答了一个与提出的问题不同的问题,所以这里尝试回答"新"问题:

促销规则(什么转换为什么)在标准中得到了很好的定义。类型char可以是signed也可以是unsigned - 在某些编译器中,您甚至可以给编译器一个标志,说"我想要无符号字符类型"或"我想要有符号的字符类型" - 但大多数编译器只是将char定义为signedunsigned

默认情况下,常量(如 6)是有符号的。当在代码中写入操作(例如'q' << 6)时,编译器会将任何较小的类型转换为任何较大的类型[或者,如果您通常执行任何算术运算,则char转换为int],因此'q'成为'q'的整数值。如果要避免这种情况,则应使用 6u 或显式强制转换,例如 static_cast<unsigned>('q') << 6 - 这样,就可以确保操作数转换为无符号,而不是有符号。

操作未定义,因为不同的硬件行为不同,并且存在具有"奇怪"编号系统的架构,这意味着标准委员会必须在"排除/使操作效率极低"或"以不太明确的方式定义标准"之间进行选择。在一些架构中,溢出整数也可能是一个陷阱,如果你移动以改变数字上的符号,这通常算作溢出 - 并且由于陷阱通常意味着"你的代码不再运行",那不是你的普通程序员所期望的 ->属于"未定义行为"的保护伞。大多数处理器不会,如果你这样做,就不会发生任何真正糟糕的事情。

旧答案:因此,避免这种情况的解决方案是在移动它们之前始终将有符号值(包括char)转换为无符号(或接受您的代码可能无法在另一个编译器、具有不同选项的同一编译器或同一编译器的下一个版本上运行)。

还值得注意的是,结果值"几乎总是你所期望的"(因为编译器/处理器只会对值执行左移或右移,在右移时使用符号位向下移位),它只是未定义或实现定义,因为某些机器架构可能没有硬件来"正确执行此操作", C 编译器仍然需要在这些系统上工作。

符号位是二进制补码中的最高位,您不会通过移动该数字来更改它:

       11111111 11111111 11111111 10001110 << 6 =
111111 11111111 11111111 11100011 10000000
^^^^^^--- goes away.
result=11111111 11111111 11100011 10000000 

或作为十六进制数:0xffffe380。