無論什么語言,類型都涉及到了編程語法的方方面面戚揭。加強對于類型和指針的理解诱告,對于提高編程水平十分關(guān)鍵。本文會主要講解類型。
關(guān)于為什么需要內(nèi)存對齊請看這里:
內(nèi)存管理-內(nèi)存對齊
我們首先來看看這幾個字節(jié)的內(nèi)存:
FFE4 | FFE4 | FFE4 | FFE4 |
---|---|---|---|
00000000 | 11001011 | 01100101 | 00001010 |
請問地址 FFE1 上字節(jié)的值是多少精居?如果你試圖回答一個結(jié)果锄禽,那就是錯的。為什么靴姿?因為我還沒有告訴你這個字節(jié)表示什么沃但。我還沒有告訴你類型信息。
如果我說上述字節(jié)表示一個數(shù)字會怎么樣呢佛吓?你可能會回答 10宵晚,那么你又錯了。為什么维雇?因為當(dāng)我說這是數(shù)字的時候淤刃,你認(rèn)為我是指十進(jìn)制的數(shù)字。
基數(shù)(number base):
所有編號系統(tǒng)(numbering system)要發(fā)揮作用吱型,都要有一個基(base)逸贾。從你出生的時候開始,人們就教你用基數(shù) 10 來數(shù)數(shù)了津滞。這可能是因為我們大多數(shù)人都有 10 個手指和 10 個腳趾铝侵。另外,用基數(shù) 10 來進(jìn)行數(shù)學(xué)計算也很自然触徐。
基定義了編號系統(tǒng)所包含的符號數(shù)咪鲜。基數(shù) 10 會有 10 個不同的符號撞鹉,用以表示我們可以計量的無限事物嗜诀。基數(shù) 10 的編號系統(tǒng)為 0孔祸、1、2发皿、3崔慧、4、5穴墅、6惶室、7、8玄货、9皇钞。一旦超過了 9,我們需要增加數(shù)的長度松捉。例如夹界,10、100 和 1000隘世。
在計算機領(lǐng)域可柿,我們還一直使用其他兩種基鸠踪。第一種是基數(shù) 2(或二進(jìn)制數(shù)),例如上圖所表示的位复斥。第二種是基數(shù) 16(或十六進(jìn)制數(shù))营密,例如上圖中表示的地址。
在二進(jìn)制編號系統(tǒng)(基數(shù) 2)中目锭,只有兩種符號评汰,即 0 和 1。
在十六進(jìn)制數(shù)字系統(tǒng)(基數(shù) 16)中痢虹,有 16 個符號被去,這些符號分別是:0、1世分、2编振、3、4臭埋、5踪央、6、7瓢阴、8畅蹂、9、A荣恐、B液斜、C、D叠穆、E少漆、F。
如果桌上有些蘋果硼被,那些蘋果可以用任何編號系統(tǒng)來表示示损。我們可以說這里有:
10010001 個蘋果(使用 2 作為基數(shù))
145 個蘋果(使用 10 作為基數(shù))
91 個蘋果(使用 16 作為基數(shù))
所有答案都正確,只要給定了正確的基嚷硫。
注意每個編號系統(tǒng)表示那些蘋果所需要的符號數(shù)检访。基數(shù)越大仔掸,編號系統(tǒng)的效率就越高脆贵。
對于計算機地址、IP 地址和顏色代碼起暮,使用 16 作為基數(shù)卖氨,就顯得很有價值。
看看用三種基,來分別表示 HTML 的顏色(“白”)的數(shù)字:
使用 2 作為基數(shù):1111 1111 1111 1111 1111 1111(24 個字符)
使用 10 作為基數(shù):16777215(10 個字符)
使用 16 作為基數(shù):FFFFFF(6 個字符)
你會選擇哪個編號系統(tǒng)來表示顏色呢双泪?
現(xiàn)在持搜,如果我告訴你,地址 FFE1 處的字節(jié)表示一個基數(shù)為 10 的數(shù)字焙矛,你回答 10葫盼,這就正確了。
類型提供了兩條信息村斟,你和編譯器都需要它來執(zhí)行我們剛剛經(jīng)歷過的練習(xí)贫导。
要查看的內(nèi)存數(shù)量(以字節(jié)為單位)
這些字節(jié)的表示
Go 語言提供了以下基本數(shù)字類型:
無符號整數(shù)
uint8, uint16, uint32, uint64
有符號整數(shù)
int8, int16, int32, int64
實數(shù)
float32, float64
預(yù)聲明整數(shù)
uint, int, uintptr
這些關(guān)鍵字提供了所有的類型信息。
uint8 包含一個基為 10 的數(shù)字蟆盹,用 1 個存儲字節(jié)表示孩灯。uint8 的值從 0 到 255。
int32 包含一個基為 10 的數(shù)字逾滥,用 4 個存儲字節(jié)表示峰档。int32 的值從 -2147483648 到 2147483647。
預(yù)聲明整數(shù)會根據(jù)你構(gòu)建代碼時的體系結(jié)構(gòu)來進(jìn)行映射寨昙。在 64 位操作系統(tǒng)上讥巡,int 將映射到 int64,而在 32 位系統(tǒng)上舔哪,它將映射到 int32欢顷。
所有存儲在內(nèi)存中的內(nèi)容都解析為某種數(shù)字類型。在 Go 中捉蚤,字符串只是一系列 uint8 類型抬驴,并包含了一些規(guī)則,用于關(guān)聯(lián)這些字節(jié)和識別字符串的結(jié)尾位置缆巧。
在 Go 中布持,指針就是 uintptr 類型。同樣地陕悬,基于操作系統(tǒng)的體系結(jié)構(gòu)题暖,它將映射為 uint32 或者 uint64。Go 為指針創(chuàng)建了一個特殊的類型墩莫。在過去,許多 C 程序員在編寫代碼時逞敷,會認(rèn)為指針值總能符合 unsigned int狂秦。隨著時間的推移,語言和體系結(jié)構(gòu)不斷升級推捐,最終這不再是對的了裂问。由于地址變得比預(yù)先聲明的 unsigned int 更大,很多代碼都出錯了。
結(jié)構(gòu)體類型只是很多類型的組合堪簿,而這些類型也最終會解析為數(shù)字類型痊乾。
type Example struct{
BoolValue bool
IntValue int16
FloatValue float32
}
該結(jié)構(gòu)體表示一個復(fù)雜類型。它表示 7 個字節(jié)椭更,有三種不同的數(shù)字表示哪审。bool
有 1 個字節(jié),int16
有 2 個字節(jié)虑瀑,而 float32
有 4 個字節(jié)湿滓。但是,這個結(jié)構(gòu)體最終在內(nèi)存中分配了 8 個字節(jié)舌狗。
為了最大限度地減少內(nèi)存碎片整理(memory defragmentation)叽奥,分配內(nèi)存時都會將內(nèi)存邊界對齊。要確定 Go 在體系結(jié)構(gòu)上所用的對齊邊界(alignment boundary)痛侍,你可以運行 unsafe.Alignof
函數(shù)朝氓。Go 在 64 位 Darwin 平臺的對齊邊界是 8 個字節(jié)。因此在 Go 確定我們結(jié)構(gòu)體的內(nèi)存分配時主届,它將填充字節(jié)以確保最終占用的內(nèi)存是 8 的倍數(shù)赵哲。編譯器會決定在哪里添加填充。
下面的程序會顯示對于 Example
結(jié)構(gòu)體類型岂膳,Go 向內(nèi)存所插入的填充:
package main
import (
"fmt"
"unsafe"
)
type Example struct {
BoolValue bool
IntValue int16
FloatValue float32
}
func main() {
example := &Example{
BoolValue: true,
IntValue: 10,
FloatValue: 3.141592,
}
exampleNext := &Example{
BoolValue: true,
IntValue: 10,
FloatValue: 3.141592,
}
alignmentBoundary := unsafe.Alignof(example)
sizeBool := unsafe.Sizeof(example.BoolValue)
offsetBool := unsafe.Offsetof(example.BoolValue)
sizeInt := unsafe.Sizeof(example.IntValue)
offsetInt := unsafe.Offsetof(example.IntValue)
sizeFloat := unsafe.Sizeof(example.FloatValue)
offsetFloat := unsafe.Offsetof(example.FloatValue)
sizeBoolNext := unsafe.Sizeof(exampleNext.BoolValue)
offsetBoolNext := unsafe.Offsetof(exampleNext.BoolValue)
fmt.Printf("Alignment Boundary: %d\n", alignmentBoundary)
fmt.Printf("BoolValue = Size: %d Offset: %d Addr: %v\n",
sizeBool, offsetBool, &example.BoolValue)
fmt.Printf("IntValue = Size: %d Offset: %d Addr: %v\n",
sizeInt, offsetInt, &example.IntValue)
fmt.Printf("FloatValue = Size: %d Offset: %d Addr: %v\n",
sizeFloat, offsetFloat, &example.FloatValue)
fmt.Printf("Next = Size: %d Offset: %d Addr: %v\n",
sizeBoolNext, offsetBoolNext, &exampleNext.BoolValue)
}
輸出如下所示:
Alignment Boundary: 8
BoolValue = Size: 1 Offset: 0 Addr: 0x21015b018
IntValue = Size: 2 Offset: 2 Addr: 0x21015b01a
FloatValue = Size: 4 Offset: 4 Addr: 0x21015b01c
Next = Size: 1 Offset: 0 Addr: 0x21015b020
該結(jié)構(gòu)體類型的對齊邊界的確是 8 字節(jié)誓竿。
Size 大小值表示某字段讀寫時所用的內(nèi)存。不出所料谈截,該值與字段的類型信息相一致筷屡。
Offset 偏移值表示字段的開始位置,在內(nèi)存占用中的字節(jié)序號簸喂。
Addr 地址值表示每個字段開始在內(nèi)存占用中所處的位置毙死。
我們可以看到,Go 在 BoolValue 和 IntValue 字段之間填充了 1 個字節(jié)喻鳄。偏移值和兩個地址之差是 2 個字節(jié)扼倘。你還可以看到,下一個內(nèi)存分配時是從結(jié)構(gòu)體最后的字段處分配 4 個字節(jié)除呵。
我們讓結(jié)構(gòu)體只有一個 bool 字段(1 字節(jié))再菊,來證實 8 字節(jié)對齊法則。
package main
import (
"fmt"
"unsafe"
)
type Example struct {
BoolValue bool
}
func main() {
example := &Example{
BoolValue: true,
}
exampleNext := &Example{
BoolValue: true,
}
alignmentBoundary := unsafe.Alignof(example)
sizeBool := unsafe.Sizeof(example.BoolValue)
offsetBool := unsafe.Offsetof(example.BoolValue)
sizeBoolNext := unsafe.Sizeof(exampleNext.BoolValue)
offsetBoolNext := unsafe.Offsetof(exampleNext.BoolValue)
fmt.Printf("Alignment Boundary: %d\n", alignmentBoundary)
fmt.Printf("BoolValue = Size: %d Offset: %d Addr: %v\n",
sizeBool, offsetBool, &example.BoolValue)
fmt.Printf("Next = Size: %d Offset: %d Addr: %v\n",
sizeBoolNext, offsetBoolNext, &exampleNext.BoolValue)
}
其輸出如下:
Alignment Boundary: 8
BoolValue = Size: 1 Offset: 0 Addr: 0x21015b018
Next = Size: 1 Offset: 0 Addr: 0x21015b020
把兩個地址相減颜曾,你將看到兩種結(jié)構(gòu)體類型分配之間存在 8 個字節(jié)的間隙纠拔。此外,這一次的內(nèi)存分配從上一示例相同的地址開始泛豪。為了保持對齊邊界稠诲,Go 向結(jié)構(gòu)體填充了 7 個字節(jié)侦鹏。
無論如何填充,Size
值實際上表示我們可以為每個字段讀寫的內(nèi)存大小臀叙。
我們只能在使用數(shù)字類型時略水,才能操作內(nèi)存,通過賦值運算符(=)可以做到這一點劝萤。為了方便渊涝,Go 創(chuàng)建了一些可以支持賦值運算符的復(fù)雜類型。這些類型有字符串稳其、數(shù)組和切片驶赏。要查看這些類型的完整列表,請查看此文檔:http://golang.org/ref/spec#Types既鞠。
這些復(fù)雜類型其實對底層數(shù)字類型進(jìn)行了抽象煤傍,我們可以在各種復(fù)雜類型的實現(xiàn)發(fā)現(xiàn)這一點。在這種情況下嘱蛋,這些復(fù)雜類型可以像數(shù)字類型那樣直接讀取內(nèi)存蚯姆。
Go 是一種類型安全的語言。這意味著洒敏,編譯器將始終強制賦值運算符的兩邊類型保持相似龄恋。這非常重要,因為這會防止我們錯誤地讀取內(nèi)存凶伙。
假設(shè)我們想做下面的事郭毕。如果你試圖編譯代碼,你會得到一個錯誤函荣。
type Example struct{
BoolValue bool
IntValue int16
FloatValue float32
}
example := &Example{
BoolValue: true,
IntValue: 10,
FloatValue: 3.141592,
}
var pointer *int32
pointer = *int32(&example.IntValue)
*pointer = 20
我試圖獲取 IntValue 字段(2 個字節(jié))的內(nèi)存地址显押,并把它存儲在類型為 int32 的指針上。接下來傻挂,我試圖用指針乘碑,向內(nèi)存地址寫入一個 4 個字節(jié)的整數(shù)。如果可以使用該指針金拒,那么我就會違反 IntValue 字段的類型規(guī)則兽肤,并在此過程中破壞內(nèi)存。
FFE8 | FFE7 | FFE6 | FFE5 | FFE4 | FFE3 | FFE2 | FFE1 |
---|---|---|---|---|---|---|---|
0 | 0 | 0 | 3.14 | 0 | 10 | 0 | true |
pointer
FFE3
FFE8 | FFE7 | FFE6 | FFE5 | FFE4 | FFE3 | FFE2 | FFE1 |
---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 20 | 0 | true |
根據(jù)上面的內(nèi)存占用情況绪抛,指針將在 FFE3 和 FFE6 之間的 4 個字節(jié)中寫入 20资铡。IntValue 的值將如預(yù)期的那樣變?yōu)?20,但 FloatValue 的值現(xiàn)在等于 0幢码。想象一下笤休,寫入這些字節(jié)超出了該結(jié)構(gòu)體的內(nèi)存分配,并且開始破壞應(yīng)用的其他區(qū)域的內(nèi)存蛤育。隨之而來的錯誤會是隨機宛官、不可預(yù)測的。
Go 編譯器會一直保證內(nèi)存對齊和轉(zhuǎn)型是安全的瓦糕。
在下面一個轉(zhuǎn)型的示例中底洗,編譯器會報錯:
ackage main
import (
"fmt"
)
// Create a new type
type int32Ext int32
func main() {
// Cast the number 10 to a value of type Jill
var jill int32Ext = 10
// Assign the value of jill to jack
// ** cannot use jill (type int32Ext) as type int32 in assignment **
var jack int32 = jill
// Assign the value of jill to jack by casting
// ** the compiler is happy **
var jack int32 = int32(jill)
fmt.Printf("%d\n", jack)
}
首先,我們在系統(tǒng)中新建了一個 int32Ext 類型咕娄,并告訴編譯器該類型表示一個 int32亥揖。接下來,我們創(chuàng)建了一個名為 jill 的新變量圣勒,將其賦值為 10费变。編譯器允許這個賦值操作,因為數(shù)字類型在賦值運算符的右側(cè)圣贸。編譯器知道賦值是安全的挚歧。
現(xiàn)在,我們嘗試創(chuàng)建第二個變量吁峻,名為 jack滑负,其類型為 int32,我們將 jill 賦值給 jack用含。在這里矮慕,編譯器會拋出錯誤:
cannot use jill (type int32Ext) as type int32 in assignment
編譯器認(rèn)為 jill 的類型是 int32Ext,不會對賦值的安全性作出任何假設(shè)啄骇。
現(xiàn)在我們使用強制轉(zhuǎn)換痴鳄,編譯器允許賦值,并如預(yù)期打印出值來缸夹。當(dāng)我們執(zhí)行轉(zhuǎn)型時痪寻,編譯器會檢查賦值的安全性。在這里明未,編譯器確定了這是相同類型的值槽华,于是允許賦值操作。
對于某些讀者來說趟妥,這似乎很基礎(chǔ)猫态,但它是使用任何編程語言的基石。即使類型是經(jīng)過抽象的披摄,你也是在操作內(nèi)存亲雪,你應(yīng)該知道你究竟在做些什么。
有了這些基礎(chǔ)疚膊,我們才可以在 Go 中討論指針义辕,然后將參數(shù)傳遞給函數(shù)。
像往常一樣寓盗,我希望這篇文章灌砖,能夠幫助你了解一些可能存在的盲區(qū)璧函。