直接将左移操作的结果赋值给变量和在C中使用左移赋值操作符有什么区别?

18
在下面的表达式中,左移操作的结果被赋值给变量i
int i;
i = 7 << 32;
printf("i = %d\n",i);
在下面的表达式中,执行左移赋值运算。
int x = 7;
x <<= 32;
printf("x = %d\n",x);

以上两个表达式得到了不同的结果。但对于下面的两个表达式并非如此。它们都得到了相同的结果。那么导致上述表达式返回不同值的原因是什么呢?

int a;
a = 1 + 1;
printf("a = %d\n",a);

int b = 1;
b += 1;
printf("b = %d\n",b);

5
你的编译器对于 int 数据类型使用了多少位? - paddy
5
尝试将其位移2位而不是32位,看看结果! - Swanand
19
提示:使用所有警告进行编译。 - paddy
那些代码块不是表达式,它们是一组语句。 - jpmc26
重复:https://dev59.com/H2s05IYBdhLWcg3wLO6Q - alinsoar
3个回答

26

C标准说明:

如果右操作数为负数或大于等于左表达式类型中的位数,则结果未定义

所以,这是未定义行为,因为int通常是32位大小,这意味着只有031步是明确定义的


1
31步骤也是未定义的,除非输入为零(将1移入符号位)。 - M.M
1
这取决于编译器,因为int类型的大小通常为32位。 - glglgl
1
@M.M 不,1在左侧的掉落是可以的。实际上,只有当移位比底层类型中的位数大于或等于该数时,无论这些位的实际值如何,才是未定义的。 - Marc Schütz
1
@M.M ISO99 6.5.7:表示它对无符号类型是定义良好的。它确实声明对有符号整数是未定义的,但在我看来,那只是有符号溢出规则的一个例子,而不是左移特定的情况。 - Marc Schütz
1
@MarcSchütz 无论你想如何分类,它仍然适用 - 假设是32位int,1 << 317 << 29等都是未定义行为,记住规则的简单方法是尝试将1左移至符号位始终是UB。 - M.M

5

我同意Cody Gray的评论。给未来到这里的人一个提示,解决此种歧义的方法是使用无符号长整型。

unsigned long long int b = 7ULL<<32; // ULL here is important, as it tells the compiler that the number being shifted is more than 32bit.

unsigned long long int a = 7;
a <<=32;

在这种情况下,64位移是否也是相同的情况? - alinsoar
代码中的任何魔数通常是字长,除非进行了类型转换。在这种情况下,由于您正在尝试向右移动32位,即等于字长的大小,我们需要告诉编译器不要将其视为一个字宽数据(32位)。 - Shuvam
3
在考虑将某个值向左或向右移动n位时,建议使用stdint中明确保证位长度的类型,如int64_tuint64_t等。这样做可以保证操作的精度和可靠性。 - cat
1
是的,+1并且再次强调:当您对位数做出假设时,始终使用_stdint_类型。 - paddy
@paddy stdint仅适用于您需要访问标准库的情况!有时(例如在嵌入式系统中),您必须编写自己的库。因此,我认为“总是”在这里并不适用! - Shuvam

1

ISO/IEC 9899中的抽象操作语义如下:

6.5.7 Bitwise shift operators --- Semantics

3 .... ... . 如果右操作数的值为负数或大于等于提升后的左操作数的宽度,则行为未定义。

在您的情况下,拆卸并查看发生了什么,我们会看到如下:

[root@arch stub]# objdump -d a.out | sed '/ <main>/,/^$/ !d'
00000000004004f6 <main>:
  4004f6:       55                      push   %rbp
  4004f7:       48 89 e5                mov    %rsp,%rbp
  4004fa:       48 83 ec 10             sub    $0x10,%rsp
  4004fe:       c7 45 fc 07 00 00 00    movl   $0x7,-0x4(%rbp)
  400505:       b8 20 00 00 00          mov    $0x20,%eax
  40050a:       89 c1                   mov    %eax,%ecx
  40050c:       d3 65 fc                shll   %cl,-0x4(%rbp)  <<== HERE IS THE PROBLEM
  40050f:       8b 45 fc                mov    -0x4(%rbp),%eax
  400512:       89 c6                   mov    %eax,%esi
  400514:       bf b4 05 40 00          mov    $0x4005b4,%edi
  400519:       b8 00 00 00 00          mov    $0x0,%eax
  40051e:       e8 cd fe ff ff          callq  4003f0 <printf@plt>
  400523:       b8 00 00 00 00          mov    $0x0,%eax
  400528:       c9                      leaveq 
  400529:       c3                      retq   
  40052a:       66 0f 1f 44 00 00       nopw   0x0(%rax,%rax,1)

生成的代码确实尝试移位,但是shll %cl,-0x4(%rbp)(左移长整型)没有效果。
在这种情况下,未定义的行为存在于汇编中,即SHL操作。

3
在这种情况下的未定义行为出现在汇编中,即在 SHL 操作中。这是不正确的。在 x86 上,通过超量移位是有定义的,您可以在文档中看到(http://x86.renejeschke.de/html/file_module_x86_id_285.html)。如果您使用汇编语言编写代码,则不会有问题。之所以它是未定义行为,是由于C语言标准规定的要求。关于”...`shll %cl,-0x4(%rbp)`(左移 long 类型)没有效果“,我不知道您为什么这么说。它绝对会产生影响。 - Cody Gray
1
从技术上讲,shll %cl, -0x4(%rbp)确实修改了堆栈上的值。为什么不会呢?该指令要求通过rbp偏移量为-4处的值按cl的值进行移位操作,这正是它所做的。现在,x86 ISA明确记录了32位操作数的掩码移位次数为5位,这(A)使得这是一种良好定义的行为,并且(B)意味着将一个值左移32位本质上是一个恒等操作,因为它计算出原始值。因此,在这个意义上,-0x4(%rbp)处的值没有被修改,但它也不应该被修改。 - Cody Gray
1
我很难理解你的观点。从文档中可以清楚地看出,在x86上,超出位数的移位不是未定义行为;事实上,它是非常明确定义的,因此我们可以准确预测将7左移32位的结果将会是什么。汇编代码中没有“问题”。问题在于编译器正在将C源代码转换为机器代码,而C源代码无效,因为表现出未定义的行为。答案的前半部分是正确的;后半部分是误导性的或者完全错误的。 - Cody Gray
1
换句话说,如果一个人用x86汇编语言手写这个程序,如果这个人的意图与左移操作的实际效果不同,我们可能会说它有一个“错误”,但我们不会说它具有未定义的行为,因为所有的机器指令都有明确定义的行为。(我们可能不喜欢英特尔规定shll将移位计数减少模32,但他们确实进行了规定。)然而,被转换成这个汇编转储的C程序确实具有未定义的行为,因为C标准没有规定过宽的移位计数应该做什么。 - zwol
2
@dan我认为您误解了我的观点,就像alinsoar可能误解了一样。我完全同意这是C中的未定义行为,仅仅因为一个特定的编译器在针对一个特定的架构时似乎始终将代码转换为特定的机器指令,并不会使其变得不确定。我的问题在于回答中声称“这种情况下的未定义行为在汇编语言中,即在SHL操作中。”这是错误的,行为在汇编语言中不是未定义的,而是在C中未定义的。 - Cody Gray
显示剩余9条评论

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