前置知识:无
建议:不要跳过
二进制和位的概念
计算机只认识二进制
- 无符号整数(所有位都表示数值):
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 个;正数: 个)
正数怎么用二进制表示
直接转换成二进制
如:136
在 uint8
中
负数怎么用二进制表示
十进制 -> 二进制
负数的绝对值取反后加一,或负数的绝对值减一后取反
-1
在 int8
中,二进制表示为?
-1的绝对值=1 0000 0001
取反 1111 1110
加一 1111 1111
-1的绝对值=1 0000 0001
减一 0000 0000
取反 1111 1111
-1 在 int8 中的二进制表示法为:0b 1111 1111
-128
在 int8
中,二进制表示为?
-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 1001
在 int8
中,十进制是多少?
二进制 1111 1001
取反 0000 0110
加一 0000 0111 (等于7,原二进制最高位为1,所以是负数)
0b 1111 1001 在 int8 中的十进制为:-7
0b 1000 0000
在 int8
中,十进制是多少?
二进制 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)
,意为清除a
中b
的位。 -
a
的相反数:- 算术运算:
-a
- 位运算:
^a + 1
- 算术运算:
特别的,有符号整数的最小值,它的相反数、绝对值都是它自己
func main() {
minn := math.MinInt
fmt.Println(minn == -minn, minn == ^minn+1) // true true
}
补充:
位运算与逻辑运算
|
、&
是位运算或、位运算与;||
、&&
是逻辑或、逻辑与,两者是有区别的
逻辑运算具有”短路性”;相对地,位运算具有”穿透性”
为什么这么设计二进制?
原码、反码、补码,用补码存储
正数三码合一;负数取反加一
为什么设计的这么复杂?
答:为了加法的逻辑是一套逻辑,没有条件转移
无论正数还是负数,加法还是减法,都走一套逻辑(小学的加法进位逻辑),计算中可能出现溢出,舍弃掉溢出的结果是对的!
而不需要根据被加数(被减数)是正负、加数(减数)是正负而进行不同的运算逻辑,这正是这样设计的原因。
关于溢出,你要保证自己的计算是不溢出的,因为溢出对于计算机做运算来说,是正常的(就是要舍弃溢出才能保证结果正确),计算机根本不清楚溢出是你传入的数值过大(过小)导致的,还是正常的溢出。所以它不会也没办法做检查,你要是传入了溢出的值,那么算错,怪你自己。
那么为啥加法逻辑如此重要呢?要保证极致的运算速度?
因为计算机根本不会加减乘除等算数运算,只会位运算,使用位运算拼出加法运算,而减乘除等其他算数运算,是由加法高效的拼出来的,所以要保证加法足够高效
如何用位运算实现加减乘除?