在C中,当左侧操作数具有负值时,左移位操作会调用未定义的行为。
ISO C99(6.5.7 / 4)的相关引用
E1的结果<< E2是E1左移E2位位置;腾出的位用零填充。如果E1具有无符号类型,则结果的值为E1×2 E2 ,减少模数 比结果类型中可表示的最大值多一个。如果E1有签名 类型和非负值,E1×2 E2 在结果类型中可表示,那么 结果价值;否则,行为未确定。
但在C ++中,行为定义明确。
ISO C ++ - 03(5.8 / 2)
E1的值<&lt; E2是E1(解释为位模式)左移E2位位置;空位是零填充的。如果E1具有无符号类型,则结果的值为E1乘以上升到功率E2的数量2,如果E1的类型为无符号长,则减少模ULONG_MAX + 1,否则为UINT_MAX + 1。 [注意:标题中定义了常量ULONG_MAX和UINT_MAX)。 ]
这意味着
int a = -1, b=2, c;
c= a << b ;
在C中调用未定义的行为,但行为在C ++中定义良好。
是什么迫使ISO C ++委员会考虑与C中的行为相对应的行为?
另一方面,当左操作数为负时,对于按位右移操作,行为为implementation defined
,对吗?
我的问题是为什么左移操作在C中调用未定义的行为,为什么右移操作符只调用实现定义的行为?
P.S:请不要给出答案,例如“这是未定义的行为,因为标准是这样说的”。 :P
答案 0 :(得分:32)
您复制的段落是关于无符号类型的。在C ++中,行为 未定义。从最后的C ++ 0x草案开始:
E1的值<&lt; E2是E1 左移E2位位置;腾空 位是零填充的。如果E1有 unsigned type,结果的值 是E1×2E ^ 2,减少模数 比最大值可表示 在结果类型中。否则,如果是E1 有一个签名类型和非负面 值,E1×2E ^ 2可表示 结果类型,那就是 结果价值; 否则, 行为未定义。
编辑:看看C ++ 98论文。它根本没有提到签名类型。所以它仍然是未定义的行为。
右移负面是实施定义,对。为什么?在我看来:实现定义很容易,因为左边的问题没有截断。当你向左移动时,你不仅要说出从右边偏移了什么,还要说出其余部分会发生什么,例如:用两个补码表示,这是另一个故事。
答案 1 :(得分:16)
在C中,按位左移操作在调用时调用未定义的行为 左侧操作数具有负值。 [...] 但在C ++中,行为定义明确。 [...]为什么[...]
简单的答案是:因为标准是这样说的。
更长的答案是:它可能与C和C ++除了2的补码之外还允许负数的其他表示这一事实有关。对将要发生的事情提供较少的保证使得可以在其他硬件上使用这些语言,包括模糊和/或旧机器。
出于某种原因,C ++标准化委员会感觉像是对位表示如何变化提供了一些保证。但由于负数仍然可以通过1的补码或符号+幅度表示,因此产生的价值可能性仍然不同。
假设16位整数,我们将
-1 = 1111111111111111 // 2's complement
-1 = 1111111111111110 // 1's complement
-1 = 1000000000000001 // sign+magnitude
向左移动3,我们将得到
-8 = 1111111111111000 // 2's complement
-15 = 1111111111110000 // 1's complement
8 = 0000000000001000 // sign+magnitude
是什么迫使ISO C ++委员会很好地考虑这种行为 定义与C?中的行为相反?
我猜他们做了这个保证,以便你可以使用&lt;&lt;适当的时候你知道你在做什么(即当你确定你的机器使用2的补码时)。
另一方面,行为是按位定义的实现 当左操作数为负时右移操作,对吗?
我必须检查标准。但你可能是对的。在2的补码机上没有符号扩展的右移不是特别有用。因此,当前状态肯定比要求腾空位填零是更好的,因为它为进行符号扩展的机器留出了空间 - 即使它没有保证。
答案 2 :(得分:7)
要回答标题中所述的真实问题:对于有符号类型的任何操作,如果数学运算的结果不适合目标类型(欠量或溢出),则会出现未定义的行为。有符号整数类型就是这样设计的。
对于左移操作,如果值为正或0,则操作符的定义为2的幂乘法是有意义的,所以一切正常,除非结果溢出,没什么可惊讶的。
如果该值为负,则可以使用2的幂对乘法进行相同的解释,但如果您只考虑位移,则这可能会令人惊讶。显然,标准委员会希望避免这种含糊不清。
我的结论:
如果你想乘以一个 价值(签字或不签)的权力,只做两个 那就像
i *(1u&lt;&lt; k)
在任何情况下,你的编译器都会将它转换为合适的汇编程序。
答案 3 :(得分:3)
很多这类事情是在一条指令中实际支持的常见CPU之间的平衡,以及即使需要额外指令也能够期望编译器编写者保证的有效性。通常,使用位移操作符的程序员希望它们使用这些指令映射到CPU上的单个指令,这就是为什么存在未定义或实现行为的原因,其中CPU具有对“边缘”条件的各种处理,而不是强制执行行为并具有操作出乎意料地慢了。请记住,即使对于更简单的用例,也可以进行额外的前/后或处理说明。在某些CPU生成陷阱/异常/中断(与C ++ try / catch类型异常不同)或通常无用/无法解释的结果时,可能需要未定义的行为,而如果标准委员会当时考虑的所有CPU集合都在至少某些已定义的行为,然后他们可以定义行为实现。
答案 4 :(得分:1)
我的问题是为什么左移操作在C中调用未定义的行为,为什么右移操作符只调用实现定义的行为?
LLVM的人推测移位运算符存在约束,因为指令在各种平台上的实现方式。来自What Every C Programmer Should Know About Undefined Behavior #1/3:
...我的猜测是,这是因为各种CPU上的基础移位操作对此做了不同的事情:例如,X86将32位移位量截断为5位(因此32位移位是相同的作为0位的移位),但PowerPC将32位移位量截断为6位(因此移位32会产生零)。由于这些硬件差异,C ...
完全没有定义该行为
Nate讨论的是关于转移大于寄存器大小的数量。但它最接近我发现解释权威的转移限制。
我认为第二个原因是2恭维机器上潜在的符号变化。但是我从来没有在任何地方看过它(没有冒犯@sellibitze(我碰巧同意他的意见))。
答案 5 :(得分:0)
C ++ 03中的行为与C ++ 11和C99中的行为相同,您只需要超越左移规则。
标准第5p5节说:
如果在评估表达式期间,结果未在数学上定义或未在其类型的可表示值范围内,则行为未定义
在C99和C ++ 11中特别称为未定义行为的左移表达式与在可表示值范围之外的结果进行求值的相同。
事实上,关于使用模运算的无符号类型的句子专门用于避免生成超出可表示范围的值,这将自动成为未定义的行为。
答案 6 :(得分:0)
在C89中,左移负值的行为在二进制补码平台上明确定义,这两个平台没有在有符号和无符号整数类型上使用填充位。有符号和无符号类型的值共同位于相同的位置,并且有符号类型的符号位可以去的唯一位置与无符号类型的高位值位于同一位置,而这又是必须的在其他一切的左边。
C89规定的行为对于没有填充的二进制补码平台是有用且合理的,至少在将它们视为乘法不会导致溢出的情况下是这样。在其他平台上,或者在寻求可靠地捕获有符号整数溢出的实现上,该行为可能不是最佳的。 C99的作者可能希望在C89强制行为不太理想的情况下允许实现灵活性,但理由中的任何内容都没有暗示质量实现不应该继续以旧方式运行的情况。没有令人信服的理由不这样做。
不幸的是,即使从未有过任何C99的实现,也没有使用二进制补码数学,C11的作者拒绝定义常见情况(非溢出)行为; IIRC,声称这样做会妨碍“优化”。当左侧操作数为负时,让左移运算符调用未定义行为允许编译器假设只有当左侧操作数为非负时才能进行移位。这允许编译器接收如下代码:
int do_something(int x)
{
if (x >= 0)
{
launch_missiles();
exit(1);
}
return x<<4;
}
认识到永远不会使用x
的负值调用此类方法,因此可以删除if
测试并使launch_missiles()
调用无条件。由于已知exit
不返回,因此编译器也可以省略x<<4
的计算。如果不是这样的规则,程序员必须插入某种笨重的__assume(x >= 0);
指令来请求这样的行为,但是使负值左移不定义行为消除了让程序员显然想要这样的需要语义(由于执行左移)使代码混乱。
注意,顺便说一句,在代码确实调用do_something(-1)
的假设事件中,它会参与未定义的行为,因此调用launch_missiles将是完全合法的事情。
答案 7 :(得分:-1)
移位的结果取决于数字表示。仅当数字表示为二进制补码时,移位才会表现为乘法。但问题不在于负数。考虑以超过8(也称为偏移二进制)表示的4位带符号数。数字1表示为1 + 8或 1001 如果我们把它转移到比特,我们得到 0010 这是-6的表示。类似地,-1表示为-1 + 8 0111 变成了 1110 当左移时,表示为+6。按位行为是明确定义的,但数字行为高度依赖于表示系统。