我一直在尝试在业余时间学习C,而其他语言(C#、Java等)都有相同的概念(通常是相同的运算符)。。。
在核心层面,比特移位(<<,>>,>>)做什么,它可以帮助解决什么问题,以及弯道周围潜伏着什么陷阱?换言之,这是一个绝对的初学者指南,可以让你充分利用它的优点。
我一直在尝试在业余时间学习C,而其他语言(C#、Java等)都有相同的概念(通常是相同的运算符)。。。
在核心层面,比特移位(<<,>>,>>)做什么,它可以帮助解决什么问题,以及弯道周围潜伏着什么陷阱?换言之,这是一个绝对的初学者指南,可以让你充分利用它的优点。
当前回答
位屏蔽和移位
位移位通常用于低级图形编程。例如,以32位字编码的给定像素颜色值。
Pixel-Color Value in Hex: B9B9B900
Pixel-Color Value in Binary: 10111001 10111001 10111001 00000000
为了更好地理解,相同的二进制值标记了什么部分代表什么颜色部分。
Red Green Blue Alpha
Pixel-Color Value in Binary: 10111001 10111001 10111001 00000000
举个例子,我们想得到这个像素颜色的绿色值。我们可以很容易地通过掩蔽和转移来获得该值。
我们的口罩:
Red Green Blue Alpha
color : 10111001 10111001 10111001 00000000
green_mask : 00000000 11111111 00000000 00000000
masked_color = color & green_mask
masked_color: 00000000 10111001 00000000 00000000
逻辑&运算符确保只保留掩码为1的值。我们现在要做的最后一件事,就是通过将所有这些位右移16位(逻辑右移)来获得正确的整数值。
green_value = masked_color >>> 16
瞧,我们有一个整数表示像素颜色中的绿色量:
Pixels-Green Value in Hex: 000000B9
Pixels-Green Value in Binary: 00000000 00000000 00000000 10111001
Pixels-Green Value in Decimal: 185
这通常用于编码或解码图像格式,如jpg、png等。
其他回答
包括位移位在内的按位操作是低级硬件或嵌入式编程的基础。如果您阅读设备规范或甚至某些二进制文件格式,您将看到字节、单词和dword,它们被分解为非字节对齐的位字段,其中包含各种感兴趣的值。访问这些位字段进行读/写是最常见的用法。
图形编程中的一个简单的实际示例是16位像素表示如下:
bit | 15| 14| 13| 12| 11| 10| 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
| Blue | Green | Red |
要获得绿色值,您可以执行以下操作:
#define GREEN_MASK 0x7E0
#define GREEN_OFFSET 5
// Read green
uint16_t green = (pixel & GREEN_MASK) >> GREEN_OFFSET;
解释
为了获得从偏移量5开始到10结束(即6位长)的绿色ONLY值,您需要使用(位)掩码,当对整个16位像素应用该掩码时,将仅产生我们感兴趣的位。
#define GREEN_MASK 0x7E0
适当的掩码为0x7E0,二进制为00000111111000000(十进制为2016)。
uint16_t green = (pixel & GREEN_MASK) ...;
要应用掩码,请使用AND运算符(&)。
uint16_t green = (pixel & GREEN_MASK) >> GREEN_OFFSET;
应用掩码后,您将得到一个16位数字,这实际上只是一个11位数字,因为它的MSB位于第11位。绿色实际上只有6位长,因此我们需要使用右移(11-6=5)来缩小它,因此使用5作为偏移量(#define Green_offset 5)。
同样常见的是使用比特移位进行2次幂的快速乘法和除法:
i <<= x; // i *= 2^x;
i >>= y; // i /= 2^y;
请注意,在Java实现中,要移位的位数是根据源代码的大小进行修改的。
例如:
(long) 4 >> 65
等于2。您可能会期望将位向右移动65次会将所有内容清零,但实际上相当于:
(long) 4 >> (65 % 64)
这适用于<<、>>和>>。我没有用其他语言试过。
Bitwise运算符用于执行位级操作或以不同方式操作位。发现按位运算速度更快,有时用于提高程序的效率。基本上,按位运算符可以应用于整数类型:long、int、short、char和byte。
按位移位运算符
它们分为两类:左移和右移。
左移位(<<):左移位运算符,将值中的所有位向左移位指定次数。语法:value<<num。这里num指定值左移的位置数。也就是说,<<将指定值中的所有位向左移动num指定的位数。每向左移动一次,高阶位都会被移出(并被忽略/丢失),而在右侧输入一个零。这意味着,当向32位编译器应用左移位时,一旦移位超过位位置31,位就会丢失。如果编译器是64位的,则位位置63之后的位丢失。
输出:6,这里3的二进制表示是0…0011(考虑32位系统),因此当它移位一次时,前导零被忽略/丢失,其余31位都向左移位。最后加零。所以它变成了0…0110,这个数字的十进制表示是6。
如果是负数:
输出:-2,在java负数中,由2的补码表示。SO,-1由2^32-1表示,相当于1….11(考虑32位系统)。当移位一次时,前导位被忽略/丢失,其余31位向左移位,最后加零。所以它变成了,11…10,它的十进制等价物是-2。所以,我认为你对左移及其工作原理有足够的了解。
右移(>>):右移运算符,将值中的所有位右移指定的次数。语法:value>>num,num指定值右移的位置数。也就是说,>>将指定值中的所有位向右移动/移位num指定的位数。以下代码片段将值35向右移动两个位置:
输出:8,因为32位系统中35的二进制表示是00…00100011,所以当我们将其右移两次时,前30个前导位移到/移到右侧,丢失/忽略两个低位,并在前导位添加两个零。因此,它变成00……00001000,这个二进制表示的十进制等价物是8。或者有一个简单的数学技巧来找出以下代码的输出:为了概括这一点,我们可以说,x>>y=floor(x/pow(2,y))。考虑上面的例子,x=35,y=2,所以,35/2^2=8.75,如果我们取下限值,那么答案是8。
输出:
但记住一点,如果你取y的大值,这个技巧对y的小值很好,它会给你不正确的输出。
如果是负数:由于是负数,右移运算符在有符号和无符号两种模式下工作。在有符号右移运算符(>>)中,如果是正数,则用0填充前导位。在负数的情况下,它用1填充前导位。保留标志。这称为“标志扩展”。
输出:-5,如上所述,编译器将负值存储为2的补码。因此,-10表示为2^32-10,考虑到32位系统11…0110,以二进制表示。当我们移位/移动一次时,前31个前导位在右侧移位,低阶位丢失/忽略。所以,它变成11…0011,这个数字的十进制表示是-5(我怎么知道数字的符号?因为前导位是1)。有趣的是,如果向右移动-1,结果总是保持-1,因为符号扩展会在高位带来更多的1。
无符号右移(>>>):此运算符还将位右移。有符号和无符号之间的区别是,如果数字为负数,则后者用1填充前导位,而在任何情况下,前者都用零填充。现在问题来了,如果我们通过有符号右移运算符获得所需的输出,为什么我们需要无符号右移运算。用一个例子来理解这一点,如果您要移动不表示数值的东西,您可能不希望符号扩展发生。在处理基于像素的值和图形时,这种情况很常见。在这些情况下,无论初始值是什么,您通常都希望将零移到高位。
输出:2147483647,因为在32位系统中,-2表示为11…10。当我们将位移位1时,前31个前导位向右移动/移位,低阶位丢失/忽略,零被添加到前导位。因此,它变为011…1111(2^31-1),其十进制等价值为2147483647。
位屏蔽和移位
位移位通常用于低级图形编程。例如,以32位字编码的给定像素颜色值。
Pixel-Color Value in Hex: B9B9B900
Pixel-Color Value in Binary: 10111001 10111001 10111001 00000000
为了更好地理解,相同的二进制值标记了什么部分代表什么颜色部分。
Red Green Blue Alpha
Pixel-Color Value in Binary: 10111001 10111001 10111001 00000000
举个例子,我们想得到这个像素颜色的绿色值。我们可以很容易地通过掩蔽和转移来获得该值。
我们的口罩:
Red Green Blue Alpha
color : 10111001 10111001 10111001 00000000
green_mask : 00000000 11111111 00000000 00000000
masked_color = color & green_mask
masked_color: 00000000 10111001 00000000 00000000
逻辑&运算符确保只保留掩码为1的值。我们现在要做的最后一件事,就是通过将所有这些位右移16位(逻辑右移)来获得正确的整数值。
green_value = masked_color >>> 16
瞧,我们有一个整数表示像素颜色中的绿色量:
Pixels-Green Value in Hex: 000000B9
Pixels-Green Value in Binary: 00000000 00000000 00000000 10111001
Pixels-Green Value in Decimal: 185
这通常用于编码或解码图像格式,如jpg、png等。
比特移位运算符正是其名称所暗示的。它们会移位。以下是对不同班次操作员的简要介绍(或不那么简单)。
操作员
>>是算术(或有符号)右移运算符。>>>是逻辑(或无符号)右移运算符。<<是左移位运算符,同时满足逻辑和算术移位的需要。
所有这些运算符都可以应用于整数值(int、long、可能的short和byte或char)。在某些语言中,对任何小于int的数据类型应用移位运算符会自动将操作数的大小调整为int。
注意,<<<不是运算符,因为它是多余的。
还要注意,C和C++不区分右移运算符。它们只提供>>运算符,正确的移位行为是为签名类型定义的实现。答案的其余部分使用C#/Java运算符。
(在所有主流的C和C++实现中,包括GCC和Clang/LLVM,对有符号类型的>>都是算术。一些代码假设这一点,但这不是标准所保证的。不过,这并不是未定义的;标准要求实现以某种方式定义它。然而,负有符号数字的左移是未定义的行为(有符号整数溢出)。因此,除非您需要算术右移,否则使用无符号类型进行位移位通常是一个好主意。)
左移(<<)
整数作为一系列位存储在存储器中。例如,存储为32位整数的数字6为:
00000000 00000000 00000000 00000110
将该位模式向左移动一个位置(6<<1)将产生数字12:
00000000 00000000 00000000 00001100
如您所见,数字向左移动了一个位置,右边的最后一个数字用零填充。您可能还注意到,向左移动相当于乘以2的幂。因此,6<<1相当于6*2,6<<3相当于6*8。一个好的优化编译器将在可能的情况下用移位替换乘法。
非循环移位
请注意,这些不是循环移位。将该值向左移动一个位置(3758096384<<1):
11100000 00000000 00000000 00000000
结果3221225472:
11000000 00000000 00000000 00000000
移动到“末尾”的数字丢失。它不会缠绕。
逻辑右移(>>)
逻辑右移与左移相反。它们不是向左移动比特,而是向右移动。例如,将数字12:
00000000 00000000 00000000 00001100
向右移动一个位置(12>>>1)将返回原来的6:
00000000 00000000 00000000 00000110
所以我们看到向右移动相当于除以2的幂。
丢失的比特不见了
然而,移位不能回收“丢失”的位。例如,如果我们改变这种模式:
00111000 00000000 00000000 00000110
向左4个位置(939524102<<4),我们得到2147483744:
10000000 00000000 00000000 01100000
然后向后移动((939524102<<4)>>4),我们得到134217734:
00001000 00000000 00000000 00000110
一旦丢失位,我们就无法恢复原始值。
算术右移(>>)
算术右移与逻辑右移完全相同,不同的是,它不是用零填充,而是用最高有效位填充。这是因为最高有效位是符号位,或区分正数和负数的位。通过用最高有效位填充,算术右移是保符号的。
例如,如果我们将此位模式解释为负数:
10000000 00000000 00000000 01100000
我们的电话号码是2147483552。用算术移位(-214743552>>4)将其向右移动4个位置,可以得到:
11111000 00000000 00000000 00000110
或号码134217722。
所以我们看到,通过使用算术右移而不是逻辑右移,我们保留了负数的符号。再一次,我们看到我们正在进行2次幂除法。