在C语言中左移位操作的含义

5

我一直在进行一些关于位操作的愚蠢测试,然后发现了这个问题。我执行了以下代码:

int main(){
  unsigned int i;
  for (i=1; i<34; i++)
  {
    unsigned long temp = i;
    unsigned long mul = 1;
    unsigned long val;
    unsigned long one = 1;

    // Way 1
    while (temp--)
      mul = mul << one;

    // Way 2
    val = (one<<i);

    printf(" \n 1<<%i \n mul: 0x%X , val: 0x%X\n",i, mul, val); 
  }
}

当 i>31 时,我知道会产生溢出。我认为代码的两个部分(way1 和 way2)应该输出相同的结果。但是最终结果是这样的:
 /* ... correct results from i=1 to i=31 ... */
 1<<30 
 mul: 0x40000000 , val: 0x40000000 

 1<<31 
 mul: 0x80000000 , val: 0x80000000 

 1<<32 
 mul: **0x0** , val: **0x1** 

 1<<33 
 mul: **0x0** , val: **0x2**

为什么两种指令都是左移,程序会产生不同的输出?似乎way2部分执行了一个圆形的移位,但我不知道为什么,我真的认为“mul”总是得到正确的值。
我在Intel 32位机器上编译,gcc版本为4.4.7。

5
根据C标准,移位操作的移位量大于操作数宽度时结果未定义。 - Eugene Sh.
这就是问题所在,没错。在这种情况下,它可能会在进行移位操作之前将右操作数与0x3F进行AND运算。 - Mr Lister
@MichaelBurr 主要问题在于one是一个变量,而不是常量。 - Mr Lister
1
除了移位操作的未定义行为,你的 printf 也会引发未定义行为。 - too honest for this site
显示剩余2条评论
4个回答

9
也许是因为这是未定义的行为?根据 §6.5.7:
如果右操作数的值为负数或大于等于提升后的左操作数的宽度,则行为未定义。

好的,我可以预期两种行为中的一种,但在两种情况下只有一种。你不认为这两个操作是相等的吗?编辑:我看到了deepmax的答案,现在我明白了。谢谢! - Alexi
@Alexi 你理解未定义的含义吗? - Eugene Sh.
@EugeneSh。是的,我的问题在于我理解了“未定义行为”的含义,但我不知道为什么有两种不同的“未定义行为”与(表面上)相同的操作。 - Alexi

2

如果发生

val = (one<<i);

i大于或等于32时,行为未定义。

 

然而,在以下情况下:

while (temp--)
   mul = mul << one;

如果位移大于32,它将会被移动为0,并且结果将被定义为0。


是的,我认为你是对的。有了Aif的答案,我觉得很清楚了。非常感谢! - Alexi

0

当你这样做时:

val = (one<<i);

你正在进行一次左移操作,移动的位数是i。如果i大于31,则会导致未定义行为,这意味着结果可能不是您所期望的。

来自C标准第6.5.7.3节:

对每个操作数执行整数提升。结果的类型是提升后的左操作数的类型。如果右操作数的值为负或大于等于提升后的左操作数的宽度,则行为是未定义的。

然而,当您执行此操作时:

while (temp--)
  mul = mul << one;

你正在进行一个左移 i 次的操作。这是明确定义的,因此它会给出你期望的值。

另外,你在打印一个 long 时使用了 %X,但应该使用 %lX。这也会导致未定义的行为。


-3

当我使用-Wall编译您的代码时,我收到了一些警告:

BASH> gcc -Wall left-shift.c 
left-shift.c: In function ‘main’:
left-shift.c:21:12: warning: format ‘%X’ expects argument of type ‘unsigned int’, but argument 3 has type ‘long unsigned int’ [-Wformat=]
     printf(" \n 1<<%i \n mul: 0x%X , val: 0x%X\n",i, mul, val); 
            ^
left-shift.c:21:12: warning: format ‘%X’ expects argument of type ‘unsigned int’, but argument 4 has type ‘long unsigned int’ [-Wformat=]

所以我把printf改成了

printf(" \n 1<<%i \n mul: 0x%lX , val: 0x%lX\n",i, mul, val);

通过这个更改,“mul”和“val”显示相同的结果:

 1<<30 
 mul: 0x40000000 , val: 0x40000000

 1<<31 
 mul: 0x80000000 , val: 0x80000000

 1<<32 
 mul: 0x100000000 , val: 0x100000000

 1<<33 
 mul: 0x200000000 , val: 0x200000000

系统信息:

BASH> gcc --version
gcc (Ubuntu 5.3.1-14ubuntu2.1) 5.3.1 20160413
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
BASH> uname -a
Linux bm-pc-ubuntu 4.4.0-24-generic #43-Ubuntu SMP Wed Jun 8 19:27:37 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux
BASH> lsb_release --all
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 16.04 LTS
Release:    16.04
Codename:   xenial

这是因为你的机器是64位的,而他的是32位的。 - s7amuser
忽略这个警告,会给我带来比原来更奇怪的输出:1<<30 乘:0x40000000,值:0x400000001<<31 乘:0x80000000,值:0x800000001<<32 乘:0x0,值:0x01<<33 乘:0x0,值:0x0 - Boris Mühmer
这太棒了...甚至是绝对合理的!是否有GCC开关可以“警告未定义行为”? - Boris Mühmer
错误的类型说明符即使在32位计算机上也会产生未定义行为,其中“int”和“long”具有相同的宽度。 - too honest for this site
@bsmr:你已经收到了关于检测到 UB 的警告。但是编译器如何能够在运行时警告 UB,而又不解决停机问题呢? - too honest for this site
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接