c进阶篇(一)——移位操作详解

前言:
  嵌入式编程中,很多时候都要进行比特级的操作,移位运算可以简化一些乘除操作。这篇文章带你真正了解移位操作。

1 数据的存储

  对数据进行移位操作,首先要了解数据在计算机中是如何存储的。计算机关于数据涉及到原码、反码、补码三种。

1.1 原码

  原码是对数字的二进制表示方法。有符号数的原码最高位是符号位,其余为数值域;无符号数则只有数值域,没有符号位。符号位用0表示非负数(0和正数),1表示复数。
  由于无符号数没有符号位,因此数据域比有符号数多一位,不能表示负数但能表示更多的非负数。
  表示的数值范围是偶数(2^N),比如int8_t能表示256个数,int16_t能表示65536个数等。由于实际运用中不需要-0,便人为规定将-0当做值最小的负数处理,因此int8_t表示的数值范围为-128127,int16_t表示的数值范围为-3276832767。

int8_t 原码
0 0000 0000b
5 0000 0101b
-5 1000 0101b
127 0111 1111b
-127 1111 1111b
-0 1000 0000b
-128 -
uint8_t 原码
0 0000 0000b
5 0000 0101b
255 1111 1111

1.2 反码

  反码就是符号位不变,数据域的二进制按位取反。

int8_t 原码 反码
0 0000 0000b 1111 1111b
5 0000 0101b 1111 1010b
-5 1000 0101b 1111 1010b
127 0111 1111b 1000 0000b
-127 1111 1111b 1000 0000b
-0 1000 0000b 1111 1111b
-128 - -
uint8_t 原码 反码
0 0000 0000b 1111 1111b
5 0000 0101b 1111 1010b
255 1111 1111 0000 0000b

1.3 补码

  计算机中数据都是以补码形式存储的,这样做的目的是可以将符号位和数值域统一处理;同时,加法减法可以统一处理。
   非负数补码为其原码,负数补码为其反码 + 1 。如果已知一个负数的补码求原码,可以先将补码 - 1然后求反码,也可再次求反码 + 1即为原码。

int8_t 原码 反码 补码
0 0000 0000b 1111 1111b 0000 0000b
5 0000 0101b 1111 1010b 0000 0101b
-5 1000 0101b 1111 1010b 1111 1011b
127 0111 1111b 1000 0000b 0111 1111b
-127 1111 1111b 1000 0000b 1000 0001b
-0 1000 0000b 1111 1111b -
-128 - - 1000 0000b
uint8_t 原码 反码 补码
0 0000 0000b 1111 1111b
5 0000 0101b 1111 1010b
255 1111 1111 0000 0000b

2 移位的原理

  由于数据在计算机中存储的是补码,因此移位操作移动的也是补码,而不是原码。我们知道非负数和负数的补码规则是不同的,因此非负数和负数的移位操作也存在差异。另外移位也有左移和右移的区别。移位移动的是整数,而浮点数在计算机中存储的格式较为复杂,因此目前c不支持浮点数移位操作。
  有符号数含有符号位,符号位为0表示非负数1表示负数,符号位的数是参与移位的,右移操作时非负数在高位补0,负数在高位补1,因此负数右移后还是负数,非负数右移后还是非负数。但左移有符号数时非负数可能变负数,负数也可能变非负数,这取决于移动的值。
  通常我们认为的右移动是除2^N,左移是乘2^N,只适用于无符号整形,并且在值不溢出的情况下才适用。
  以下演示1Byte数据的移位,更多字节数据的移位原理同理。

2.1 非负数移位

  计算机中存储补码,由于非负数的补码等于原码,因此对非负数移位操作实际上移动的值是原码。有符号类型非负数符号位是0。

2.1.1 右移

  非负数右移,高位补0,低位移出舍弃,由于高位总是补0而符号位0表示非负数,因此有符号类型非负数右移也总是非负数;对于无符号类型,只有数据域无符号位,高位补0,低位移出舍弃。

int8_t 补码 >> 1 >> 2
0 0000 0000b 0000 0000b 0000 0000b
85 0101 0101b 0010 1010b(42d) 0001 0101b(21d)
127 0111 1111b 0011 1111b(63d) 0001 1111b(31)
uint8_t 补码 >> 1 >> 2
0 0000 0000b 0000 0000b 0000 0000b
170 1010 1010b 0101 0101b(85d) 0010 1010b(42d)
255 1111 1111b 0111 1111b(127d) 0011 1111b(63d)

2.1.2 左移

  非负数左移,低位补0,高位移出舍弃,由于低位总是补0而符号位也参与移位,因此有符号类型非负数左移可能变为负数;对于无符号类型,只有数据域无符号位,低位补0,高位移出舍弃。

int8_t 补码 << 1 << 2
0 0000 0000b 0000 0000b 0000 0000b
85 0101 0101b 1010 1010b(-86d) 0101 0100b(84d)
127 0111 1111b 1111 1110b(-2d) 1111 1100b(-4d)
uint8_t 补码 << 1 << 2
0 0000 0000b 0000 0000b 0000 0000b
170 1010 1010b 0101 0100b(84d) 1010 1000b(168d)
255 1111 1111b 1111 1110b(254d) 1111 1100b(252d)

2.2 负数移位

  计算机中存储补码,由于负数的补码不是原码,负数补码为符号位不变反码 + 1。有符号类型负数符号位是1。

2.2.1 右移

  负数右移,高位补1,低位移出舍弃,由于高位总是补1而符号位1表示负数,因此有符号类型负数右移也总是负数;对于无符号类型,只有数据域无符号位,高位补1,低位移出舍弃。

int8_t 补码 >> 1 >> 2
-1 1111 1111b l111 1111b(-1d) l111 1111b(-1d)
-42 1101 0110b 1110 1011b(-21d) 1111 0101b(-11d)
-128 1000 0000b 1100 0000b(-64d) 1110 0000b(-32d)

2.2.2 左移

  负数左移,低位补0,高位移出舍弃,由于低位总是补0而符号位也参与移位,因此有符号类型负数左移可能变为非负数;对于无符号类型,只有数据域无符号位,低位补0,高位移出舍弃。

int8_t 补码 << 1 << 2
-1 1111 1111b l111 1110b(-2d) l111 1100b(-4d)
-42 1101 0110b 1010 1100b(-84d) 0101 1000b(88d)
-128 1000 0000b 0000 0000b(0d) 0000 0000b(0d)

3算术移位和逻辑移位

  上面讲的移位规则其实可以分为两类,算术移位和逻辑移位。

3.1 算术移位

   对于有符号数进行的移位称为算术移位 ,算术移位需要考虑符号位。

  • 算术移位右移时,非负数高位补0,负数高位补1,因此算术右移符号位不变,低位移出舍弃;
  • 算术移位左移时,不论正负数低位都补0,因此算术左移可能改变符号位,高位移出舍弃;

3.2 逻辑移位

   对于无符号数进行的移位称为逻辑移位 ,无符号位。

  • 逻辑移位右移时,高位补0,低位移出舍弃;
  • 逻辑移位左移时,低位补0,高位移出舍弃;