引用和指針非常相似部宿,它們都用來讓一個(gè)變量提供對(duì)另一個(gè)變量的訪問。
引用
需要從類型和傳遞兩個(gè)角度分別看待引用瓢湃。
- 從類型角度理张,類型可分為值類型和引用類型,一般而言绵患,我們說到引用雾叭,強(qiáng)調(diào)的都是類型。
- 從傳遞角度落蝙,有值傳遞织狐、址傳遞和引用傳遞,傳遞是在函數(shù)調(diào)用時(shí)才會(huì)提到的概念筏勒,用于表明實(shí)參與形參的關(guān)系移迫。
什么是引用
引用的實(shí)現(xiàn)主要有兩種。
- C++ 的實(shí)現(xiàn)管行,引用其實(shí)一種便于使用指針的語法糖起意,是某塊內(nèi)存的別名,對(duì)已存在的變量可以聲明別名病瞳,這種別名稱為引用變量。
- Python 中的實(shí)現(xiàn)悲酷,本質(zhì)是底層結(jié)構(gòu)中包含指向?qū)嶋H內(nèi)容的指針套菜。
參數(shù)傳遞
參數(shù)傳遞有值傳遞、址傳遞和引用傳遞设易。
值傳遞
函數(shù)調(diào)用時(shí)逗柴,實(shí)參通過拷貝將自身內(nèi)容傳遞給形參,形參實(shí)際上是實(shí)參值的一個(gè)拷貝顿肺,此時(shí)戏溺,針對(duì)函數(shù)中形參的任何操作,僅僅是針對(duì)實(shí)參的副本屠尊,不影響原始值的內(nèi)容旷祸。
址傳遞
值傳遞中有一個(gè)特殊形式,如果傳遞參數(shù)的類型是指針讼昆,我們就會(huì)稱之為址傳遞托享。
引用傳遞
實(shí)參地址在函數(shù)調(diào)用被傳遞給形參(即實(shí)參和形參擁有相同地址),則可以認(rèn)為是引用傳遞。此時(shí)闰围,針對(duì)函數(shù)中形參的操作會(huì)影響到實(shí)參赃绊。
C++ 支持引用傳遞。
Go 語言是值傳遞
func fn(m map[int]int) {
fmt.Printf("fc: %p\n", &m)
m = make(map[int]int)
fmt.Printf("fn:%v\n", m == nil)
}
func main() {
var m map[int]int
fmt.Printf("main: %p\n", &m)
fn(m)
fmt.Printf("main:%v\n", m == nil)
}
輸出如下:
main: 0xc000006028
fc: 0xc000006038
fn:false
main:true
通過打印信息可以看到羡榴,實(shí)參和形參地址不同碧查,且對(duì)形參賦值不影響實(shí)參。因此校仑,Go 語言沒有引用傳遞忠售。
而址傳遞可以看做值傳遞中的一個(gè)特殊形式,因此可以說肤视,Go 語言是值傳遞档痪。
Go 引用類型
如果按照 C++ 中引用的實(shí)現(xiàn)機(jī)制,則 Go 語言沒有引用變量邢滑,Go 程序中定義的每個(gè)變量都占用一個(gè)唯一的內(nèi)存位置腐螟。創(chuàng)建兩個(gè)共享同一內(nèi)存位置的變量是不可能的±Ш螅可以創(chuàng)建兩個(gè)指向同一內(nèi)存位置的變量乐纸,不過這與兩個(gè)變量共享同一內(nèi)存位置是不同的。
如果按照 python 中引用的實(shí)現(xiàn)機(jī)制摇予,即結(jié)構(gòu)體中包含指針成員汽绢。對(duì)類型進(jìn)行分類:
值類型:基本數(shù)據(jù)類型 int、float侧戴、bool宁昭、string 以及數(shù)組和 struct。值類型變量直接存儲(chǔ)值酗宋,內(nèi)存通常在棧中分配积仗。
引用類型:指針、slice蜕猫、map寂曹、chan、interface回右、function隆圆。引用類型變量存儲(chǔ)的是一個(gè)地址,這個(gè)地址存儲(chǔ)最終的值翔烁,內(nèi)存通常在堆上分配渺氧,通過 GC 回收。引用類型都可以用 nil 進(jìn)行賦值租漂。
slice阶女、map 和 channel 的底層實(shí)現(xiàn)
slice颊糜、map 和 channel 的實(shí)現(xiàn)機(jī)制是結(jié)構(gòu)體中包含指針成員。它們都可以使用內(nèi)置函數(shù) make 進(jìn)行初始化秃踩。
map
map 實(shí)際是指向 runtime.hmap 結(jié)構(gòu)體的指針衬鱼。
當(dāng)我們寫如下代碼時(shí)
m := make(map[int]int)
編譯器會(huì)自動(dòng)去調(diào)用 runtime.makemap
func makemap(t *maptype, hint int64, h *hmap, bucket unsafe.Pointer) *hmap
從 runtime.makemap 返回的值的類型是指向 runtime.hmap 結(jié)構(gòu)體的指針。
那么如果 map 是指針憔杨,那是不是應(yīng)該這樣表示 *map[key]value 鸟赫?事實(shí)是編譯器將類型從 *map[int]int 重命名為 map[int]int 。
channel
也是 runtime 類型的指針消别。
slice
slice 的結(jié)構(gòu)包含三個(gè)成員抛蚤,分別是切片的底層數(shù)組地址、切片長度和容量大小寻狂。
type slice struct {
array unsafe.Pointer
len int
cap int
}
指針
什么是指針
計(jì)算機(jī)內(nèi)存可以看做一串單元格岁经,每個(gè)單元格都有一個(gè)地址,是其所在的內(nèi)存位置蛇券,每個(gè)單元格存儲(chǔ)一個(gè)值缀壤。如果你知道某個(gè)單元格的內(nèi)存地址,就可以訪問該單元格并更新或讀取里面的內(nèi)容纠亚。而 CPU 所做的一切都是為獲取和存儲(chǔ)值到內(nèi)存單元中塘慕。
在代碼中,通過變量就可以操作存儲(chǔ)在內(nèi)存中的值蒂胞,變量只是一個(gè)由數(shù)字字母組成的图呢、標(biāo)識(shí)存儲(chǔ)位置的假名,由編譯器為變量分配唯一的內(nèi)存地址骗随。一個(gè)變量對(duì)應(yīng)了一段內(nèi)存空間蛤织,這段內(nèi)存空間存儲(chǔ)了該變量對(duì)應(yīng)類型的值。
指針變量的值是另一個(gè)變量的內(nèi)存地址鸿染。通過指針瞳筏,就可以更新或讀取另一個(gè)變量的值,而不需要用到變量名牡昆。
對(duì)于每一種類型,不管是自定義的還是 Go 語言內(nèi)置的摊欠,都有相應(yīng)的指針類型丢烘。例如內(nèi)置類型 int,對(duì)應(yīng)的指針類型是 *int些椒。如果你自己聲明了類型 User播瞳,對(duì)應(yīng)的指針類型就是 *User。
所有的指針類型有相同的特點(diǎn)免糕。首先赢乓,它們以 * 符號(hào)開頭忧侧;其次,占用相同的內(nèi)存空間并且都表示一個(gè)地址牌芋,使用 4 個(gè)(32 位機(jī)器)或 8 個(gè)字節(jié)(64 位機(jī)器)長度表示一個(gè)地址蚓炬。
設(shè)計(jì)指針的目的是實(shí)現(xiàn)函數(shù)間值共享,即使該值不在函數(shù)自己棧幀里躺屁,也能對(duì)其進(jìn)行讀寫操作肯夏。
與其他變量相比,指針變量并沒有特別之處犀暑,因?yàn)樗鼈円彩亲兞垦被鳎袃?nèi)存地址和值。
底層原理
指針的聲明和使用
指針由 * 操作符和存儲(chǔ)值的類型表示耐亏。
*
也用于指針變量的解引用徊都,使得我們可以訪問指針指向的值。
var i int = 10 // 聲明int類型變量i广辰,初始值10
var ptr *int = &i // 聲明指針變量ptr暇矫,初始值為i的地址。& 操作符用于獲取變量的地址轨域。
fmt.Println(ptr, *ptr) // *ptr對(duì)應(yīng)指針指向的變量的值 0xc000018060 10
*ptr = 12 // 更新指針指向的變量的值袱耽,實(shí)際是指針變量解引用,將結(jié)果存儲(chǔ)在 i 指向的內(nèi)存位置
fmt.Println(*ptr, i) // 12 12
*int
類型的指針干发,指向的必須是 int 類型變量的地址朱巨,若指向其他類型變量地址,編譯報(bào)錯(cuò)枉长。
str := "go"
var ip *int
ip = &str // cannot use &str (type *string) as type *int in assignment
空指針
一個(gè)指針已聲明而沒有賦值時(shí)冀续,稱為空指針,值為 nil必峰。任何類型的指針的零值都是 nil洪唐。
var ip *int
fmt.Println(ip) // nil
fmt.Printf("ip 的值為:%x", ip) // ip 的十六進(jìn)制的值為:0
指針相等判斷
指針之間也是可以進(jìn)行相等判斷的,只有當(dāng)它們指向同一個(gè)變量或全部是 nil 時(shí)才相等吼蚁。
指針作為函數(shù)參數(shù)使用
func a(p *int) {
*p++
}
func main() {
i := 10
a(&i)
fmt.Println(i) // 打印11凭需,a函數(shù)中的指針p指向main函數(shù)中的i的內(nèi)存位置
}
new 函數(shù)創(chuàng)建指針
內(nèi)建函數(shù) new 也是一種創(chuàng)建指針的方法。new(type)
表示創(chuàng)建一個(gè) type 類型的匿名變量肝匆,初始化為 type 類型的零值粒蜈,并返回變量的指針,指針類型為 *type旗国。new 適用于“值類型”枯怖,如 int、數(shù)組能曾、結(jié)構(gòu)體等度硝。
p := new(int) // p, *int 類型, 指向匿名的 int 變量
fmt.Println(*p) // 0
*p = 2 // 設(shè)置 int 匿名變量的值為 2
fmt.Println(*p) // 2