本文程式路徑是
gopl/basictypes/number
.
1. 整數類型和浮點數類型
1.1 整數類型
go 語言和 c 語言非常像,整數也分為有符号和無符号。在 go 語言裡,整數在記憶體占用的寬度有 8bit, 16bit, 32bit, 64bit 這四種。有符号是
int8, int16, int32, int64
,而無符号是
uint8, uint16, uint32, uint64
.
除了以上 8 種整數類型外,還有:
-
類型,32 bit 整數,主要用來表示 unicode 碼點。rune
-
類型,8 bit 整數,用來表示原始位元組資料。byte
-
和int
類型,寬度和平台及編譯器相關,32 bit 或 64 bit.uint
-
,寬度和平台及編譯器相關,用于存放指針。它保證放得下一個指針。uintptr
在 go 裡使用
Printf
函數列印整數使用
%d
來格式化輸出,無論它是有符号還是無符号都适用。這一點和 c 語言有一點差別,c 語言使用
u%
格式化輸出無符号整數。
關于整數類型的使用規則,和 c 語言并沒有多大差別,概念都是相通的。無論是 c 還是 go,關于有符号、無符号、溢出、進位我需要提兩嘴,因為很多同學這幾個概念不清楚。
1.2 浮點數類型
go 裡的浮點數類型和 c 語言一樣,有 32 bit 的
float32
(對應 c 語言的
float
類型) 和 64bit 的
float64
(對應 c 語言的
double
類型)。
關于浮點數,這裡就隻講這麼多了。因為你完全可以和 c 語言類比。
2. 符号
以 8 bit 數字為例講解。
2.1 映射規則
在記憶體裡用 8bit 表示整數,一共可以表示 256 個數。也就是從
00000000
~
11111111
. 在程式裡,需要将這 256 個數和數學中的 256 個數一一對應起來。
對應規則是怎樣的呢?你可以這樣(方案一):
00000000 -> 0
00000001 -> 1
...
01111111 -> 127
...
10000000 -> 128
11111111 -> 255
也可以這樣(方案二):
00000000 -> 0
00000001 -> 1
...
01111111 -> 127
...
10000000 -> -128
10000001 -> -127
...
11111111 -> -1
甚至可以這樣(方案三):
00000000 -> -128
00000001 -> -127
...
01111111 -> -1
10000000 -> 0
10000001 -> 1
...
11111111 -> 127
然而計算機科學發展了這麼多年,實際上隻有方案一和方案二被實際采用,而方案三棄之,因為它不太好用。
實際上方案一就是我們人類所說的無符号數映射規則,而方案二則是有符号數的映射規則 。
2.2 關于計算
下面是重點:
cpu 執行加減法計算的時候,并不在乎參與運算的數字是有符号還是無符号。
什麼意思呢?cpu 在執行 1000000 + 01000000 (二進制) 的時候,并不關心這個二進制數字映射的實際數字是什麼,換句話說,cpu 不關注數字是否是有符号還是無符号的。
對于 cpu 來說,1000000 + 01000000 執行結果就是 11000000.
接下來我們以『人類視角』看待上面的運算,如果上面的數字是無符号的,相當于:
128+64=196
128
+
64
=
196
如果上面的數字是有符号的,相當于:
−128+64=−64
−
128
+
64
=
−
64
沒錯,11000000 在無符号下是 196,在有符号下就是 -64.
既然如此,那符号有什麼作用?記住下面一點:
符号在程式裡:
- 在比較大小的時候有用。
- 輸出到螢幕的時候有用。
舉例:
對于有符号來說,10000000 < 01000000,對無符号來說,10000000 > 01000000.
對于無符号來說,10000000 輸出到螢幕是 128,對于無符号來說,它輸出到螢幕是 -128.
下面是一個運作執行個體:
a
是以有符号的視角去看待二進制數,而
ua
是以無符号視角看待。
c
是兩個數字的和,顯示為 -5 是因為我們以有符号去看待
c
,如果以無符号視角看待
c
,那就是 251.
// demo01.go
package main
import "fmt"
func main() {
var a int8 = -10
var b int8 = 5
var ua uint8 = uint8(a)
var ub uint8 = uint8(b)
fmt.Printf("a = %d, b = %d\n", a, b)
fmt.Printf("ua = %d, ub = %d\n", ua, ub)
// 1. 運算和符号無關. Print 輸出數字,需要顯式告訴它是否有無符号。
c := a + b
fmt.Printf("c = a + b = %d\n", c)
fmt.Printf("uint8(c) = %d\n", uint8(c))
uc := ua + ub
fmt.Printf("uc = ua + ub = %d\n", uc)
fmt.Printf("int8(uc) = %d\n", int8(uc))
// 2. 比較大小和符号有關
var x bool = a < b
fmt.Printf("a < b = %v\n", x)
fmt.Printf("ua < ub = %v\n", ua < ub)
}
圖1 數字運算
另外,還舉了個例子,符号會影響大小比較。正如 a < b 為真,ua < ub 為假。
3. 進位和溢出
- 進位是對 cpu 說的,溢出是對人說的。
- 進位不在乎兩個數是否有符号,溢出在乎是否是有符号。
3.1 進位
兩個二進進制數相加,如果最高位進位超過了數字寬度,則産生進位。
例如 8 bit 寬度的 2 進制數 11111111 + 00000001 = (1)00000000,括号裡産生的進位被丢棄。
3.2 溢出
以有符号視角看待兩個數,如果運算結果超出表示範圍,則溢出。以 8bit 整數為例,如果是有符号,則範圍是 -128 ~ 127. 如果是無符号,則範圍是 0 ~ 255.
繼續用 3.1 中的例子。如果用有符号視角看待上面的計算,相當于 -1 + 1 = 0,結果在 -128 ~ 127 之間,沒有溢出。
如果以無符号視角看待,相當于 255 + 1 = 256,結果超出了 0 ~ 255,溢出。
上面的例子告訴我們,兩個數想加是否溢出,取決于你以什麼視角看待它,它是受人類主觀影響的。而是否進位,則取決于 cpu 自己,不受人類左右。
4. 輸出格式控制
4.1 整數輸出
下面是整數所使用的格式控制。
// demo02.go
package main
import "fmt"
func main() {
var a int32 = 0x0123abcd
// # 表示列印字首。x, X 表示以小寫或大寫 16 進制列印
fmt.Printf("%d %x %#x %#X\n", a, a, a, a)
// [1] 表示再次引用第一個資料
fmt.Printf("%d %[1]x %#[1]x %#[1]X\n", a)
var b byte = 'a'
var c rune = '犇'
// %c 表示列印 ascii 或 unicode 字元,%q 表示列印字元,外面帶個單引号
fmt.Printf("%d %c %q\n", b, b, b)
fmt.Printf("%d %[1]c %[1]q\n", c)
}
圖2 整數輸出格式控制
4.2 浮點數輸出
package main
import "math"
import "fmt"
func main() {
var a float32 = 3.1415926
var b float64 = 6.02214129e23
// 浮點數無法使用 %d 控制。
// %g 表示以更加緊湊的格式輸出,%f 可以控制浮點數輸出的寬度和精度,以及填充符号
fmt.Printf("%d %g %f\n", a, a, a)
fmt.Printf("%d %g %f\n", b, b, b)
// 列印 e 的 0 次方到 7 次方
for x := 0; x < 8; x++ {
fmt.Printf("x=%d e^x=%8.3f\n", x, math.Exp(float64(x)))
}
}
圖3 浮點數輸出格式控制
5. 總結
- 掌握 go 裡的 8 種整數類型
- 了解符号和有符号的作用,符号在哪裡會産生影響
- 了解進位和溢出
- 掌握整數和浮點數的輸出格式控制
- 使用 go 程式模拟 11111111 + 00000001 (二進制),然後以有符号和無符号的方式分别列印到螢幕,看看輸出結果。
- 解釋 1 中的輸出結果,解理符号的作用。
- 在 1 中,哪種算是溢出?