9.3 指標和記憶體分配詳解
9.3 指標和記憶體分配詳解
定義
瞭解指標之前,先講一下什麼是變數。
每當我們編寫任何程式時,我們都需要在記憶體中儲存一些資料/資訊。資料儲存在特定地址的儲存器中。記憶體地址看起來像 0xAFFFF
(這是記憶體地址的十六進位制表示)。
現在,要訪問資料,我們需要知道儲存它的地址。我們可以跟蹤儲存與程式相關的資料的所有記憶體地址。但想象一下,記住所有記憶體地址並使用它們訪問資料會有非常困難。這就是為什麼引入變數。
變數是一種佔位符,用於引用計算機的記憶體地址,可理解為記憶體地址的標籤。
什麼是指標
指標是儲存另一個變數的記憶體地址的變數。所以指標也是一種變數,只不過它是一種特殊變數,它的值存放的是另一個變數的記憶體地址。
在上面的例子中,指標 p
包含值 0x0001
,該值是變數的地址 a
。
Go型別佔用記憶體情況
unsafe包可以獲取變數的記憶體使用情況
Go語言提供以下基本數字型別:
無符號整數 uint8,uint16,uint32,uint64
符號整數 int8,int16,int32,int64
實數 float32,float64 Predeclared
整數(依賴系統型別,跟系統有關) uint,int,uintptr (指標)
32位系統
uint=uint32 int=int32 uintptr為32位的指標
64位系統
uint=uint64 int=int64 uintptr為64位的指標
示例:
package main import ( "fmt" "unsafe" ) func main() { var uint8Value uint8 var uint16Value uint16 var uint32Value uint32 var uint64Value uint64 var int8Value int8 var int16Value int16 var int32Value int32 var int64Value int64 var float32Value float32 var float64Value float64 fmt.Println("uint8Value = Size:", unsafe.Sizeof(uint8Value)) //uint8Value = Size: 1 fmt.Println("uint16Value = Size:", unsafe.Sizeof(uint16Value)) //uint16Value = Size: 2 fmt.Println("uint32Value = Size:", unsafe.Sizeof(uint32Value)) //uint32Value = Size: 4 fmt.Println("uint64Value = Size:", unsafe.Sizeof(uint64Value))// uint64Value = Size: 8 fmt.Println("int8Value = Size:", unsafe.Sizeof(int8Value)) //int8Value = Size: 1 fmt.Println("int16Value = Size:", unsafe.Sizeof(int16Value))//int16Value = Size: 2 fmt.Println("int32Value = Size:", unsafe.Sizeof(int32Value))//int32Value = Size: 4 fmt.Println("int64Value = Size:", unsafe.Sizeof(int64Value)) //int64Value = Size: 8 fmt.Println("float32Value = Size:", unsafe.Sizeof(float32Value)) //float32Value = Size: 4 fmt.Println("float64Value = Size:", unsafe.Sizeof(float64Value))//float64Value = Size: 8 }
上面的是基本型別,接下來了解下複雜型別,以結構體型別為例
type Example struct { BoolValuebool IntValueint16 FloatValue float32 }
該結構代表複雜型別。它代表7個位元組,帶有三個不同的數字表示。bool是一個位元組,int16是2個位元組,float32增加4個位元組。但是,在此結構的記憶體中實際分配了8個位元組。
所有記憶體都分配在對齊邊界上,以最大限度地減少記憶體碎片整理。要確定對齊邊界Go用於您的體系結構,您可以執行unsafe.Alignof函式。Go為64bit Darwin平臺的對齊邊界是8個位元組。因此,當Go確定結構的記憶體分配時,它將填充位元組以確保最終記憶體佔用量是8的倍數。編譯器將確定新增填充的位置。
什麼是記憶體對齊呢?
記憶體對齊,也叫邊界對齊(boundary alignment),是處理器為了提高處理效能而對存取資料的起始地址所提出的一種要求。編譯器為了使我們編寫的C程式更有效,就必須最大限度地滿足處理器對邊界對齊的要求。
從處理器的角度來看,需要儘可能減少對記憶體的訪問次數以實現對資料結構進行更加高效的操作。為什麼呢?因為儘管處理器包含了快取,但它在處理資料時還得讀取快取中的資料,讀取快取的次數當然是越少越好!如上圖所示,在採用邊界對齊的情況下,當處理器需要訪問a_變數和b_變數時都只需進行一次存取(圖中花括號表示一次存取操作)。若不採用邊界對齊,a_變數只要一次處理器操作,而b_變數卻至少要進行兩次操作。對於b_,處理器還得呼叫更多指令將其合成一個完整的4位元組,這樣無疑大大降低了程式效率。
以下程式顯示Go插入到Example型別struct的記憶體佔用中的填充:
package main import ( "fmt" "unsafe" ) type Example struct { BoolValuebool IntValueint16 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("example Size: %d\n", unsafe.Sizeof(example)) 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) }
輸出:
example Size: 8 Alignment Boundary: 8 BoolValue = Size: 1 Offset: 0 Addr: 0xc00004c080 IntValue = Size: 2 Offset: 2 Addr: 0xc00004c082 FloatValue = Size: 4 Offset: 4 Addr: 0xc00004c084 Next = Size: 1 Offset: 0 Addr: 0xc00004c088
型別結構的對齊邊界是預期的8個位元組。
大小值顯示將讀取和寫入該欄位的記憶體量。正如所料,大小與型別資訊一致。
偏移值顯示進入記憶體佔用的位元組數,我們將找到該欄位的開頭。
地址是可以找到記憶體佔用內每個欄位的開頭的地方。
我們可以看到Go在BoolValue和IntValue欄位之間填充1個位元組。偏移值和兩個地址之間的差異是2個位元組。您還可以看到下一個記憶體分配是從結構中的最後一個欄位開始4個位元組。
指標的使用
宣告一個指標
使用以下語法宣告型別為T的指標
var p *int
指標的 零值 是 nil
。這意味著任何未初始化的指標都將具有該值 nil
。讓我們看一個完整的例子
package main import "fmt" func main() { var p *int &p=1 }
注意:當指標沒有指向的時候,不能對(*point)進行操作包括讀取,否則會報空指標異常。
示例:
package main func main() { var p *int *p = 1 //panic: runtime error: invalid memory address or nil pointer dereference }
解決方法即給該指標分配一個指向,即初始化一個記憶體,並把該記憶體地址賦予指標變數
示例:
import "fmt" func main() { var p *int var m int p = &m *p = 1 fmt.Println("m=", m) fmt.Println("p=", p) }
或還可以使用內建 new()
函式建立指標。該 new()
函式將型別作為引數,分配足夠的記憶體以容納該型別的值,並返回指向它的指標。
import "fmt" func main() { var p *int p = new(int) *p = 1 fmt.Println("p=", *p) }
初始化指標
您可以使用另一個變數的記憶體地址初始化指標。可以使用 &
運算子檢索變數的地址
var x = 100 var p *int = &x
注意我們如何使用 &
帶變數的運算子 x
來獲取其地址,然後將地址分配給指標 p
。
就像Golang中的任何其他變數一樣,指標變數的型別也由編譯器推斷。所以你可以省略 p
上面例子中指標的型別宣告,並像這樣寫
var p = &a
取消引用指標
您可以 *
在指標上使用運算子來訪問儲存在指標所指向的變數中的值。這被稱為 解除引用 或 間接
package main import "fmt" func main() { var a = 100 var p = &a fmt.Println("a = ", a) fmt.Println("p = ", p) fmt.Println("*p = ", *p) }
輸出:
a =100 p =0xc00004c080 *p =100
您不僅可以使用 *
運算子訪問指向變數的值,還可以更改它。以下示例 a
通過指標設定儲存在變數中的值 p
package main import "fmt" func main() { var a = 1000 var p = &a fmt.Println("a (before) = ", a) // Changing the value stored in the pointed variable through the pointer *p = 2000 fmt.Println("a (after) = ", a) }
輸出:
a (before) =1000 a (after) =2000
指標指向指標
指標可以指向任何型別的變數。它也可以指向另一個指標。以下示例顯示如何建立指向另一個指標的指標
package main import "fmt" func main() { var a = 7.98 var p = &a var pp = &p fmt.Println("a = ", a) fmt.Println("address of a = ", &a) fmt.Println("p = ", p) fmt.Println("address of p = ", &p) fmt.Println("pp = ", pp) // Dereferencing a pointer to pointer fmt.Println("*pp = ", *pp) fmt.Println("**pp = ", **pp) }
Go中沒有指標算術
如果您使用過C / C ++,那麼您必須意識到這些語言支援指標演算法。例如,您可以遞增/遞減指標以移動到下一個/上一個記憶體地址。您可以向/從指標新增或減去整數值。您也可以使用關係運算符比較兩個三分球 ==
, <
, >
等。
但Go不支援對指標進行此類算術運算。任何此類操作都將導致編譯時錯誤
package main func main() { var x = 67 var p = &x var p1 = p + 1 // Compiler Error: invalid operation }
但是,您可以使用 ==
運算子比較相同型別的兩個指標的相等性。
package main import "fmt" func main() { var a = 75 var p1 = &a var p2 = &a if p1 == p2 { fmt.Println("Both pointers p1 and p2 point to the same variable.") } }
Go中傳遞簡單型別
import "fmt" func main() { p := 5 change(&p) fmt.Println("p=", p)//p= 0 } func change(p *int) { *p = 0 }
Go中所有的都是按值傳遞,對於複雜型別,傳的是指標的拷貝
package main import "fmt" func main() { var m map[string]int m = map[string]int{"one": 1, "two": 2} n := m fmt.Printf("%p\n", &m) //0xc000074018 fmt.Printf("%p\n", &n) //0xc000074020 fmt.Println(m)// map[two:2 one:1] fmt.Println(n)//map[one:1 two:2] changeMap(m) fmt.Printf("%p\n", &m) //0xc000074018 fmt.Printf("%p\n", &n) //0xc000074020 fmt.Println(m)//map[one:1 two:2 three:3] fmt.Println(n)//map[one:1 two:2 three:3] } func changeMap(m map[string]int) { m["three"] = 3 fmt.Printf("changeMap func %p\n", m) //changeMap func 0xc000060240 }
直接傳指標 也是傳指標的拷貝
package main import "fmt" func main() { var m map[string]int m = map[string]int{"one": 1, "two": 2} n := m fmt.Printf("%p\n", &m) //0xc000074018 fmt.Printf("%p\n", &n) //0xc000074020 fmt.Println(m)// map[two:2 one:1] fmt.Println(n)//map[one:1 two:2] changeMap(&m) fmt.Printf("%p\n", &m) //0xc000074018 fmt.Printf("%p\n", &n) //0xc000074020 fmt.Println(m)//map[one:1 two:2 three:3] fmt.Println(n)//map[two:2 three:3 one:1] } func changeMap(m *map[string]int) { //m["three"] = 3 //這種方式會報錯 invalid operation: m["three"] (type *map[string]int does not support indexing) (*m)["three"] = 3//正確 fmt.Printf("changeMap func %p\n", m) //changeMap func 0x0 }
總結:
-
Go 不能進行指標運算。
-
指標傳遞是很廉價的,只佔用 4 個或 8 個位元組。當程式在工作中需要佔用大量的記憶體,或很多變數,或者兩者都有,使用指標會減少記憶體佔用和提高效率。
-
指標也是一種型別,不同於一般型別,指標的值是地址,這個地址指向其他的記憶體,通過指標可以讀取其所指向的地址所儲存的值。
-
函式方法的接受者,也可以是指標變數。簡單型別和複雜型別在傳遞的時候不同,複雜型別傳值或傳指標都是指標拷貝。
-
只宣告未賦值的變數,golang都會自動為其初始化為零值,基礎資料型別的零值比較簡單,引用型別和指標的零值都為nil,nil型別不能直接賦值,因此需要通過new開闢一個記憶體,或指向一個變數。
參考資料
http : //golang.org/doc/faq#Pointers
https://www.callicoder.com/golang-pointers/
https://www.ardanlabs.com/blog/2013/07/understanding-pointers-and-memory.html
https://www.ardanlabs.com/blog/2013/07/understanding-type-in-go.html
links
- 目錄
- 上一節:
- 下一節: