區(qū)別:1、go語言可以使用new關(guān)鍵字來分配內(nèi)存創(chuàng)建指定類型的指針,而c語言不行。2、c語言中數(shù)組名arr代表的是數(shù)組首元素的地址,相當(dāng)于“&arr[0]”;go語言中數(shù)組名arr不代表數(shù)組首元素的地址,代表的是整個數(shù)組的值。3、go語言不支持指針運(yùn)算,而c語言支持指針運(yùn)算。4、
php入門到就業(yè)線上直播課:進(jìn)入學(xué)習(xí)
Apipost = Postman + Swagger + Mock + Jmeter 超好用的API調(diào)試工具:點(diǎn)擊使用
本教程操作環(huán)境:windows7系統(tǒng)、GO 1.18版本、Dell G3電腦。
C 和 Go 都是有指針概念的語言,這篇文章主要借這兩者之間的異同來加深對 Go 指針的理解和使用。
運(yùn)算符
C 和 Go 都相同:
-
&
運(yùn)算符取出變量所在的內(nèi)存地址 -
*
運(yùn)算符取出指針變量所指向的內(nèi)存地址里面的值,也叫 “ 解引用 ”
C 語言版示例:
#include <stdio.h> int main() { int bar = 1; // 聲明一個指向 int 類型的值的指針 int *ptr; // 通過 & 取出 bar 變量所在的內(nèi)存地址并賦值給 ptr 指針 ptr = &bar; // 打印 ptr 的值(為地址),*prt 表示取出指針變量所指向的內(nèi)存地址里面的值 printf("%p %dn", ptr, *ptr); return (0); } // 輸出結(jié)果: // 0x7ffd5471ee54 1
Go 語言版示例:
package main import "fmt" func main() { bar := 1 // 聲明一個指向 int 類型的值的指針 var ptr *int // 通過 & 取出 bar 變量所在的內(nèi)存地址并賦值給 ptr 指針 ptr = &bar // 打印 ptr 變量儲存的指針地址,*prt 表示取出指針變量所指向的內(nèi)存地址里面的值 fmt.Printf("%p %dn", ptr, *ptr) } // 輸出結(jié)果: // 0xc000086020 1
Go 還可以使用 new
關(guān)鍵字來分配內(nèi)存創(chuàng)建指定類型的指針。
// 聲明一個指向 int 類型的值的指針 // var ptr *int ptr := new(int) // 通過 & 取出 bar 變量所在的內(nèi)存地址并賦值給 ptr 指針 ptr = &bar
數(shù)組名和數(shù)組首地址
對于一個數(shù)組
// C int arr[5] = {1, 2, 3, 4, 5}; // Go // 需要指定長度,否則類型為切片 arr := [5]int{1, 2, 3, 4, 5}
在 C 中,數(shù)組名 arr
代表的是數(shù)組首元素的地址,相當(dāng)于 &arr[0]
而 &arr
代表的是整個數(shù)組 arr 的首地址
// C // arr 數(shù)組名代表數(shù)組首元素的地址 printf("arr -> %pn", arr); // &arr[0] 代表數(shù)組首元素的地址 printf("&arr[0] -> %pn", &arr[0]); // &arr 代表整個數(shù)組 arr 的首地址 printf("&arr -> %pn", &arr); // 輸出結(jié)果: // arr -> 0061FF0C // &arr[0] -> 0061FF0C // &arr -> 0061FF0C
運(yùn)行程序可以發(fā)現(xiàn) arr
和 &arr
的輸出值是相同的,但是它們的意義完全不同。
首先數(shù)組名 arr
作為一個標(biāo)識符,是 arr[0]
的地址,從 &arr[0]
的角度去看就是一個指向 int 類型的值的指針。
而 &arr
是一個指向 int[5] 類型的值的指針。
可以進(jìn)一步對其進(jìn)行指針偏移驗(yàn)證
// C // 指針偏移 printf("arr+1 -> %pn", arr + 1); printf("&arr+1 -> %pn", &arr + 1); // 輸出結(jié)果: // arr+1 -> 0061FF10 // &arr+1 -> 0061FF20
這里涉及到偏移量的知識:一個類型為 T
的指針的移動,是以 sizeof(T)
為移動單位的。
-
arr+1
: arr 是一個指向 int 類型的值的指針,因此偏移量為1*sizeof(int)
-
&arr+1
: &arr 是一個指向 int[5] 的指針,它的偏移量為1*sizeof(int)*5
到這里相信你應(yīng)該可以理解 C 語言中的 arr
和 &arr
的區(qū)別了吧,接下來看看 Go 語言
// 嘗試將數(shù)組名 arr 作為地址輸出 fmt.Printf("arr -> %pn", arr) fmt.Printf("&arr[0] -> %pn", &arr[0]) fmt.Printf("&arr -> %pn", &arr) // 輸出結(jié)果: // arr -> %!p([5]int=[1 2 3 4 5]) // &arr[0] -> 0xc00000c300 // &arr -> 0xc00000c300
&arr[0]
和 &arr
與 C 語言一致。
但是數(shù)組名 arr
在 Go 中已經(jīng)不是數(shù)組首元素的地址了,代表的是整個數(shù)組的值,所以輸出時會提示 %!p([5]int=[1 2 3 4 5])
指針運(yùn)算
指針本質(zhì)上就是一個無符號整數(shù),代表了內(nèi)存地址。
指針和整數(shù)值可以進(jìn)行加減法運(yùn)算,比如上文的指針偏移例子:
-
加
n
: 一個類型為T
的指針,以n*sizeof(T)
為單位向高位移動。 -
減
n
: 一個類型為T
的指針,以n*sizeof(T)
為單位向低位移動。
其中 sizeof(T)
代表的是數(shù)據(jù)類型占據(jù)的字節(jié),比如 int
在 32 位環(huán)境下為 4 字節(jié),64 位環(huán)境下為 8 字節(jié)
C 語言示例:
#include <stdio.h> int main() { int arr[] = {1, 2, 3, 4, 5}; // ptr 是一個指針,為 arr 數(shù)組的第一個元素地址 int *ptr = arr; printf("%p %dn", ptr, *ptr); // ptr 指針向高位移動一個單位,移向到 arr 數(shù)組第二個元素地址 ptr++; printf("%p %dn", ptr, *ptr); return (0); } // 輸出結(jié)果: // 0061FF08 1 // 0061FF0C 2
在這里 ptr++
從 0061FF08
移動了 sizeof(int) = 4
個字節(jié)到 0061FF0C
,指向了下一個數(shù)組元素的地址
Go 語言示例:
package main import "fmt" func main() { arr := [5]uint32{1, 2, 3, 4, 5} // ptr 是一個指針,為 arr 數(shù)組的第一個元素地址 ptr := &arr[0] fmt.Println(ptr, *ptr) // ptr 指針向高位移動一個單位,移向到 arr 數(shù)組第二個元素地址 ptr++ fmt.Println(ptr, *ptr) } // 輸出結(jié)果: // 編譯報錯: // .main.go:13:5: invalid operation: ptr++ (non-numeric type *uint32)
編譯報錯 *uint32
非數(shù)字類型,不支持運(yùn)算,說明 Go 是不支持指針運(yùn)算的。
這個其實(shí)在 Go Wiki[1] 中的 Go 從 C++ 過渡文檔中有提到過:Go has pointers but not pointer arithmetic.
Go 有指針但不支持指針運(yùn)算。
另辟蹊徑
那還有其他辦法嗎?答案當(dāng)然是有的。
在 Go 標(biāo)準(zhǔn)庫中提供了一個 unsafe
包用于編譯階段繞過 Go 語言的類型系統(tǒng),直接操作內(nèi)存。
我們可以利用 unsafe
包來實(shí)現(xiàn)指針運(yùn)算。
func Alignof(x ArbitraryType) uintptr func Offsetof(x ArbitraryType) uintptr func Sizeof(x ArbitraryType) uintptr type ArbitraryType func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType type IntegerType type Pointer func Add(ptr Pointer, len IntegerType) Pointer
核心介紹:
-
uintptr
: Go 的內(nèi)置類型。是一個無符號整數(shù),用來存儲地址,支持?jǐn)?shù)學(xué)運(yùn)算。常與unsafe.Pointer
配合做指針運(yùn)算 -
unsafe.Pointer
: 表示指向任意類型的指針,可以和任何類型的指針互相轉(zhuǎn)換(類似 C 語言中的void*
類型的指針),也可以和uintptr
互相轉(zhuǎn)換 -
unsafe.Sizeof
: 返回操作數(shù)在內(nèi)存中的字節(jié)大小,參數(shù)可以是任意類型的表達(dá)式,例如fmt.Println(unsafe.Sizeof(uint32(0)))
的結(jié)果為4
-
unsafe.Offsetof
: 函數(shù)的參數(shù)必須是一個字段 x.f,然后返回 f 字段相對于 x 起始地址的偏移量,用于計算結(jié)構(gòu)體成員的偏移量
原理:
Go 的 uintptr
類型存儲的是地址,且支持?jǐn)?shù)學(xué)運(yùn)算
*T
(任意指針類型) 和 unsafe.Pointer
不能運(yùn)算,但是 unsafe.Pointer
可以和 *T
、 uintptr
互相轉(zhuǎn)換
因此,將 *T
轉(zhuǎn)換為 unsafe.Pointer
后再轉(zhuǎn)換為 uintptr
,uintptr
進(jìn)行運(yùn)算之后重新轉(zhuǎn)換為 unsafe.Pointer
=> *T
即可
代碼實(shí)現(xiàn):
package main import ( "fmt" "unsafe" ) func main() { arr := [5]uint32{1, 2, 3, 4, 5} ptr := &arr[0] // ptr(*uint32類型) => one(unsafe.Pointer類型) one := unsafe.Pointer(ptr) // one(unsafe.Pointer類型) => *uint32 fmt.Println(one, *(*uint32)(one)) // one(unsafe.Pointer類型) => one(uintptr類型) 后向高位移動 unsafe.Sizeof(arr[0]) = 4 字節(jié) // twoUintptr := uintptr(one) + unsafe.Sizeof(arr[0]) // ??!twoUintptr 不能作為臨時變量 // uintptr 類型的臨時變量只是一個無符號整數(shù),并不知道它是一個指針地址,可能被 GC // 運(yùn)算完成后應(yīng)該直接轉(zhuǎn)換回 unsafe.Pointer : two := unsafe.Pointer(uintptr(one) + unsafe.Sizeof(arr[0])) fmt.Println(two, *(*uint32)(two)) } // 輸出結(jié)果: // 0xc000012150 1 // 0xc000012154 2
甚至還可以更改結(jié)構(gòu)體的私有成員:
// model/model.go package model import ( "fmt" ) type M struct { foo uint32 bar uint32 } func (m M) Print() { fmt.Println(m.foo, m.bar) } // main.go package main import ( "example/model" "unsafe" ) func main() { m := model.M{} m.Print() foo := unsafe.Pointer(&m) *(*uint32)(foo) = 1 bar := unsafe.Pointer(uintptr(foo) + 4) *(*uint32)(bar) = 2 m.Print() } // 輸出結(jié)果: // 0 0 // 1 2
小 Tips
Go 的底層 slice
切片源碼就使用了 unsafe
包
// slice 切片的底層結(jié)構(gòu) type slice struct { // 底層是一個數(shù)組指針 array unsafe.Pointer // 長度 len int // 容量 cap int }
總結(jié)
-
Go 可以使用
&
運(yùn)算符取地址,也可以使用new
創(chuàng)建指針 -
Go 的數(shù)組名不是首元素地址
-
Go 的指針不支持運(yùn)算
-
Go 可以使用
unsafe
包打破安全機(jī)制來操控指針,但對我們開發(fā)者而言,是 "unsafe" 不安全的