【问题标题】:What is going on with bitwise operators and integer promotion?位运算符和整数提升是怎么回事?
【发布时间】:2015-08-09 00:53:14
【问题描述】:

我有一个简单的程序。 请注意,我使用了一个大小为 1 字节的无符号固定宽度整数。

#include <cstdint>
#include <iostream>
#include <limits>

int main()
{
    uint8_t x = 12;
    std::cout << (x << 1) << '\n';
    std::cout << ~x;

    std::cin.clear();
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
    std::cin.get();

    return 0;
}

我的输出如下。

24
-13

我测试了更大的数字,操作员 &lt;&lt; 总是给我正数,而操作员 ~ 总是给我负数。然后我用sizeof() 发现...

当我使用左移位运算符 (&lt;&lt;) 时,我收到一个无符号的 4 字节整数。

当我使用按位非运算符 (~) 时,我收到一个带符号的 4 字节整数。

似乎按位非运算符(~)像算术运算符一样进行有符号整数提升。但是,左移运算符(&lt;&lt;)似乎提升为无符号积分。

我觉得有义务知道编译器何时在我背后改变了某些东西。如果我的分析是正确的,所有按位运算符是否都提升为 4 字节整数?为什么有些签名有些未签名?我很困惑!

编辑:我关于总是得到正值或总是得到负值的假设是错误的。但是,由于以下出色的答案,我了解了真正发生的事情。

【问题讨论】:

  • 如何让流将uint8_t 输出为数字而不是字符?你确定你的编译器没有将该类型别名为int
  • @AntonSamsonov 在下面的答案中,他解释说这是在按位运算发生后发生的积分提升的结果。换句话说,数据类型从uint8_t 提升为int

标签: c++ bitwise-operators unsigned-integer integer-promotion signed-integer


【解决方案1】:

[expr.unary.op]

~ 的操作数应为整数或无范围枚举类型;这 result 是其操作数的反码。 整体促销是 执行。

[expr.shift]

移位运算符&lt;&lt;&gt;&gt; 从左到右分组。 [...] 操作数应为整数或无范围枚举类型,并且执行整数提升

uint8_t的整体推广是什么(通常是unsigned_char在幕后)?

[conv.prom]

boolchar16_tchar32_twchar_t 其整数转换等级 (4.13) 小于 int 可以转换为 int 类型的纯右值,如果 int 可以代表所有 源类型的值;否则,源纯右值可以是 转换为 unsigned int 类型的纯右值。

所以int,因为uint8_t 的所有值都可以用int 表示。

int(12) &lt;&lt; 1 是什么? int(24).

~int(12) 是什么? int(-13).

【讨论】:

  • 而我在使用按位非运算符(~)后得到所有负数的原因是因为大多数二进制数的左侧尾随 0,当它们被翻转时,最左边的数字可能是 1 使数字为负。如果保存该值的内存大小为 4 字节,给我 2^32 个可能的值,并且我选择的值远小于此范围,则尤其如此。
  • 您似乎从一本深入的 C++ 书籍或手册中找到了此编译器信息。如果是,它叫什么?如果我在编译器操作上遇到困难,我想将其用作资源。
  • @WanderingIdiot “从左边拖尾”被称为“领先”。这本书被称为 C++ 标准(不是推荐的资源)。 en.cppreference.com/w/cpp/language/…
  • @DrumM 任何体面的 C++ 介绍书都应该涵盖其中的大部分内容,例如stroustrup.com/4th.html
【解决方案2】:

出于性能原因,C 和 C++ 语言将int 视为“最自然”的整数类型,而将比int“更小”的类型视为某种“存储”类型。

当您在表达式中使用存储类型时,它会自动转换为 int 或隐式转换为 unsigned int。例如:

// Assume a char is 8 bit
unsigned char x = 255;
unsigned char one = 1;

int y = x + one; // result will be 256 (too large for a byte!)
++x;             // x is now 0

发生的情况是第一个表达式中的xone 已隐式转换为整数,已计算加法并将结果存储回整数中。换句话说,没有使用两个无符号字符执行计算。

同样,如果表达式中有 float 值,编译器会首先将其提升为 double(换句话说,float 是一种存储类型,double 是自然大小对于浮点数)。这就是如果你使用printf 来打印浮点数的原因,你不需要说%lf int 格式字符串和%f 就足够了(%lf 需要scanf 但是因为那个函数存储结果,float 可以小于double)。

C++ 使事情变得相当复杂,因为在将参数传递给函数时,您可以区分 ints 和更小的类型。因此,并非总是在每个表达式中都执行转换...例如,您可以:

void foo(unsigned char x);
void foo(int x);

在哪里

unsigned char x = 255, one = 1;
foo(x);       // Calls foo(unsigned char), no promotion
foo(x + one); // Calls foo(int), promotion of both x and one to int

【讨论】:

  • 我很喜欢你关于在传递函数参数时如何不执行隐式转换的最后评论。这是一些有用的信息,谢谢。
  • 它有什么“自然”的地方?好吧,编译器可能会做它需要/想要计算结果的任何事情(以尽可能最快的方式,或考虑到其他考虑),但它应该保留(最大)操作数类型。只有程序员才应该扩展表达式类型 - 使用显式转换;任何其他行为都是违反直觉的。或者,从您的角度来看,围绕所有表达式编写显式强制转换以将它们放回原来的域是否更“自然”?
  • “同样,如果表达式中有一个浮点值,编译器会做的第一件事就是将其提升为双精度值”——这在 C++ 中是不正确的。它仅作为 默认参数提升(标准中的第 [5.2.2p7] 段)的一部分发生,仅适用于与 省略号参数规范 匹配的函数参数(...,这就是 printf 发生这种情况的原因)。在a + b中,如果ab都是float,则两者都没有提升,结果的类型为float;如果一个是float,另一个是double,那么就会发生转换,但那是另一回事。
  • @bogdan: 一个执行a = b + c 的函数,其中abcfloat,它生成与@987654356 相同的逐字节机器码@.
  • 这是一个实现细节,甚至并非所有实现都如此。我刚刚验证了 MSVC12 会为您的两种情况生成 very 不同的代码。它还会为您的第二种情况(在/W4)生成从double 转换为float 的警告,而第一种情况不会发生这种情况。您可以使用std::is_same&lt;decltype(a + b), float&gt;::value 验证b + c 的类型为float
【解决方案3】:

我测试了更大的数字,运算符

错了,测试一下:

uint8_t v = 1;
for (int i=0; i<32; i++) cout << (v<<i) << endl;

给予:

1
2
4
8
16
32
64
128
256
512
1024
2048
4096
8192
16384
32768
65536
131072
262144
524288
1048576
2097152
4194304
8388608
16777216
33554432
67108864
134217728
268435456
536870912
1073741824
-2147483648

uint8_t 是一个 8 位长的无符号整数类型,它可以表示 [0,255] 范围内的值,因为该范围包含在 int 的范围内,它被提升为 int(不是 @987654326 @)。升级到 int 优先于升级到 unsigned

【讨论】:

    【解决方案4】:

    查看two's complement 以及计算机如何存储负整数。
    试试这个

    #include <cstdint>
    #include <iostream>
    #include <limits>
    int main()
    {
    uint8_t x = 1;
    int shiftby=0;
    shiftby=8*sizeof(int)-1;
    std::cout << (x << shiftby) << '\n'; // or std::cout << (x << 31) << '\n';
    
    std::cout << ~x;
    
    std::cin.clear();
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
    std::cin.get();
    return 0;
    }
    

    输出为-2147483648

    一般来说,如果有符号数的第一位是 1,则认为它是负数。当您取大量并转移它时。如果你把它移到第一位是 1 它将是负数

    ** 编辑 **
    好吧,我可以想到移位运算符使用 unsigned int 的原因。考虑右移操作&gt;&gt;,如果你右移-12,你会得到122而不是-6。这是因为它在开头添加了一个零而不考虑符号

    【讨论】:

    • 我不相信这完全回答了我的问题,但让我按照你的说法,也许我会弄清楚这一点。当您左移 31 次时,值溢出。由此看来,左移运算符执行有符号积分提升而不是无符号积分提升。所以如果这是真的,那么这意味着编译器检查二进制数的最左边的数字以确定它的符号。一定是这样的!当操作数比int 窄时,编译器必须始终使用位运算符进行整数提升。
    • 等等,什么?我不知道你用哪个编译器来测试这个,但是 x86_64 上的 GCC 会发出一个用于有符号移位的 SAR 指令,它会保留符号。因此,(-12 >> 1) == -6。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-03-23
    • 1970-01-01
    • 1970-01-01
    • 2012-06-27
    • 2019-01-25
    • 1970-01-01
    相关资源
    最近更新 更多