前置知识:无

建议:不要跳过

二进制和位的概念

计算机只认识二进制

  • 无符号整数(所有位都表示数值):
    • uint32:32 个二进制位
    • uint64:64 个二进制位
  • 有符号整数(最高位表示符号,0:正数;1:负数):
    • int32:32 个二进制位
    • int64:64 个二进制位

以 4 位为例:

# 无符号
1    0   1   1
-----------------
2^3 2^2 2^1 2^0
-----------------
8 +  0 + 2 + 1 = 11

# 有符号
0    0   1   1
-----------------
+   2^2 2^1 2^0
-----------------
+    0 + 2 + 1 = 3

一个 n 进制整数,能表示的范围:

  • 无符号整数:
  • 有符号整数: (负数: 个;零:1 个;正数: 个)

正数怎么用二进制表示

直接转换成二进制

如:136uint8

负数怎么用二进制表示

十进制 -> 二进制

负数的绝对值取反后加一,或负数的绝对值减一后取反

-1int8 中,二进制表示为?

-1的绝对值=1  0000 0001
取反          1111 1110
加一          1111 1111

-1的绝对值=1  0000 0001
减一          0000 0000
取反          1111 1111

-1 在 int8 中的二进制表示法为:0b 1111 1111

-128int8 中,二进制表示为?

-128的绝对值=128 1000 0000
取反             0111 1111
加一             1000 0000

-128的绝对值=128 1000 0000
减一             0111 1111
取反             1000 0000

-128 在 int8 中的二进制表示法为:0b 1000 0000

取反后加一 == 减一后取反

func main() {
  var i int8 = -128
  for {
    if ^i+1 != ^(i - 1) {
      fmt.Println("!!!") // 没有打印
    }
    if i == 127 {
      break
    }
    i++
  }
}

注意,不要这样写,会死循环:

func main() {
  var i int8
  for i = -128; i <= 127; i++ { // 死循环:i == 127 时 i++,i 溢出,变成了 -128
    if ^i+1 != ^(i - 1) {
      fmt.Println("!!!") // 没有打印
    }
  }
}

二进制 -> 十进制

二进制取反后加一,或二进制减一后取反

0b 1111 1001int8 中,十进制是多少?

二进制    1111 1001
取反      0000 0110
加一      0000 0111   (等于7,原二进制最高位为1,所以是负数)

0b 1111 1001 在 int8 中的十进制为:-7

0b 1000 0000int8 中,十进制是多少?

二进制    1000 0000
取反      0111 1111
加一      1000 0000   (等于128,原二进制最高位为1,所以是负数)


0b 1000 0000 在 int8 中的十进制为:-128

十进制与二进制对应

int8 为例

十进制    -128       ...  -1         0          1          ...  127
二进制    1000 0000  ...  1111 1111  0000 0000  0000 0001  ...  0111 1111

一共2^8(256)个,其中负数2^7(128)个,零1个,正数2^7-1(127)个

打印二进制

func main() {
  vals := []int8{-128, -127, -1, 0, 1, 127}
  for _, val := range vals {
    printInt8(val)
  }
}
 
func printInt8(val int8) {
  for bit := 7; bit >= 0; bit-- {
    if val&(1<<bit) != 0 { // 注意:这里不能用 == 1 判断,这里不是 1,而是 2^bit
      fmt.Print("1")
    } else {
      fmt.Print("0")
    }
  }
  fmt.Println()
}

直接定义二进制、八进制、十六进制

func main() {
  a := 0b1001100 // 二进制
  b := 76        // 十进制
  c := 0o114     // 八进制
  d := 0114      // 八进制的另一种写法(不要用)
  e := 0x4c      // 十六进制
 
  fmt.Println(a == b, c == d, a == c, a == e) // true true true true
}
1 个八进制对应 3 个二进制位
二进制  001  001  100
八进制  1    1    4

1 个十六进制对应 4 个二进制位
二进制    0100  1100
十六进制  4     c

常见的位运算

  • a&b:位与

  • a|b:位或

  • a^b:位异或

  • ^a:位取反,其他语言中为 ~a

  • a<<b:位左移

  • a>>b:位右移,最左侧二进制位用符号位补

  • a>>>b:无符号位右移,最左侧二进制位用 0 补,go 中无此符号,因为 go 中有无符号整数

  • a&^b:位清除,go 独有,相当于 a & (^b),意为清除 ab 的位。

  • a 的相反数:

    • 算术运算:-a
    • 位运算: ^a + 1

特别的,有符号整数的最小值,它的相反数、绝对值都是它自己

func main() {
  minn := math.MinInt
  fmt.Println(minn == -minn, minn == ^minn+1) // true true
}

补充:

位运算与逻辑运算

|& 是位运算或、位运算与;||&& 是逻辑或、逻辑与,两者是有区别的

逻辑运算具有”短路性”;相对地,位运算具有”穿透性”

为什么这么设计二进制?

原码、反码、补码,用补码存储

正数三码合一;负数取反加一

为什么设计的这么复杂?

答:为了加法的逻辑是一套逻辑,没有条件转移

无论正数还是负数,加法还是减法,都走一套逻辑(小学的加法进位逻辑),计算中可能出现溢出,舍弃掉溢出的结果是对的!

而不需要根据被加数(被减数)是正负、加数(减数)是正负而进行不同的运算逻辑,这正是这样设计的原因。

关于溢出,你要保证自己的计算是不溢出的,因为溢出对于计算机做运算来说,是正常的(就是要舍弃溢出才能保证结果正确),计算机根本不清楚溢出是你传入的数值过大(过小)导致的,还是正常的溢出。所以它不会也没办法做检查,你要是传入了溢出的值,那么算错,怪你自己。

那么为啥加法逻辑如此重要呢?要保证极致的运算速度?

因为计算机根本不会加减乘除等算数运算,只会位运算,使用位运算拼出加法运算,而减乘除等其他算数运算,是由加法高效的拼出来的,所以要保证加法足够高效

如何用位运算实现加减乘除?

后面会讲