前言
我總想著搞清楚,什麼樣的技術文章才算是好的文章呢?因為寫一篇今後自己還願意閱讀的文章並不容易,暫時只能以此為目標努力。
最近開始用Go刷一些題,遇到了一些切片相關的細節問題,這裡做一些總結。切片的設計想法是由動態陣列概念而來,為了開發者可以更加方便的使一個資料結構可以自動增加和減少。但是切片本身並不是動態資料或者陣列指標。
切片的結構
type slice struct {
array unsafe.Pointer // 一個指向底層陣列的指標(切片中元素是存在這個指向的陣列中)
len int // 切片的長度:包含的元素個數
cap int // 切片的容量,len <= cap,如果len == cap,則新增元素會觸發切片的擴容
}
長度為3,容量為5的int切片的圖示如下,此時切片陣列中可訪問的部分只有下標0,1,2,超過部分不能訪問。
宣告和初始化
nil切片
宣告nil切片,宣告之後就會初始化(預設會用nil初始化),此時slice == nil成立,用於描述一個不存在的切片。
var slice []int // 此時 slice == nil 成立
空切片
宣告並初始化空切片,表示一個空的集合,空切片指向地址不是nil。
slice := make([]int, 0) // 此時 slice == nil 不成立
slice := []int{}
無論是nil切片還是空切片在呼叫內建函式append、len和cap的效果都是一樣的。
含有元素的切片
此時切片非空。
slice := []int{1, 2, 3, 4, 5} // 宣告並初始化一個切片,len和cap都是5
slice := make([]int, 4) // 宣告並初始化一個切片,第二個參數列示切片的長度len和容量cap都為4
slice := make([]int, 4, 6) // 宣告並初始化一個切片,第二個參數列示len,第三個表示cap
-----------------------------------------------------------
array := [4]int{1, 2, 3, 4}
slice := array[1:2] // 對於array[i:j]來說,新切片的len=j-i,且cap=k-i(這裡k是原陣列的大小)
測試上面第四種初始化切片的方法:
func main() {
array := [...]int{1, 2, 3, 4}
slice := array[1:2]
fmt.Printf("%p %d %d %v\n", &slice, len(slice), cap(slice), slice)
}
/*
輸出:
0xc00000c030 1 3 [2]
*/
拷貝
使用 := 拷貝
注意:下面程式碼中newSlice切片是通過slice切片宣告並初始化的,雖然兩個切片列印的地址不同,但是切片的地址指標指向的陣列是同一個。修改slice[0] = 100之後,newSlice[0]也變成100。這種規則適用於將切片作為引數傳遞給函式,在函式的內部使用的是傳入切片的值拷貝(建立一塊新的記憶體存放切片,但切片的地址指標指向的是同一個陣列)
func main() {
array := [...]int{1, 2, 3, 4}
slice := array[1:2]
newSlice := slice // 拷貝,newSlice由一塊新的記憶體存放slice切片資訊
/*
上面這種初始化newSlice的寫法等價於:
var newSlice []int
newSlice = slice
*/
fmt.Printf("%p %d %d %v\n", &slice, len(slice), cap(slice), slice)
fmt.Printf("%p %d %d %v\n", &newSlice, len(newSlice), cap(newSlice), newSlice)
slice[0] = 100
fmt.Printf("%p %d %d %v\n", &slice, len(slice), cap(slice), slice)
fmt.Printf("%p %d %d %v\n", &newSlice, len(newSlice), cap(newSlice), newSlice)
}
/*
輸出:
0xc00000c030 1 3 [2]
0xc00000c048 1 3 [2]
0xc00000c030 1 3 [100]
0xc00000c048 1 3 [100]
*/
使用copy函式拷貝
copy函式的兩個引數是兩個切片(將第二個切片的值覆蓋到第一個切片),二者地址指標指向兩個不同的陣列。
func main() {
slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{5, 4, 3}
fmt.Printf("%d %d %p %v\n", len(slice1), cap(slice1), &slice1, slice1)
fmt.Printf("%d %d %p %v\n", len(slice2), cap(slice2), &slice2, slice2)
//copy(slice2, slice1) // 只會複製slice1的前3個元素到slice2中
copy(slice1, slice2) // 只會複製slice2的3個元素到slice1的前3個位置
fmt.Printf("%d %d %p %v\n", len(slice1), cap(slice1), &slice1, slice1)
//fmt.Printf("%d %d %p %v\n", len(slice2), cap(slice2), &slice2, slice2)
slice2[0] = 200
slice1[0] = 100
fmt.Printf("%d %d %p %v\n", len(slice1), cap(slice1), &slice1, slice1)
fmt.Printf("%d %d %p %v\n", len(slice2), cap(slice2), &slice2, slice2)
}
/*
輸出:
5 5 0xc00000c018 [1 2 3 4 5]
3 3 0xc00000c030 [5 4 3]
5 5 0xc00000c018 [5 4 3 4 5]
5 5 0xc00000c018 [100 4 3 4 5] copy函式的兩個切片的地址指標分別指向兩個不同的陣列(修改值互不影響)
3 3 0xc00000c030 [200 4 3]
*/
擴容
擴容使用append方法。
len == cap 時新增元素
func main() {
slice := []int{1, 2, 3} // 此時slice的len == cap == 3,append元素會觸發擴容,擴容後切片地址指向新陣列(具體擴容策略這裡暫時不多深入)
fmt.Printf("%p %d %d %v\n", &slice, len(slice), cap(slice), slice)
newSlice := append(slice, 1)
fmt.Printf("%p %d %d %v\n", &newSlice, len(newSlice), cap(newSlice), newSlice)
slice[0] = 100
fmt.Printf("%p %d %d %v\n", &slice, len(slice), cap(slice), slice)
fmt.Printf("%p %d %d %v\n", &newSlice, len(newSlice), cap(newSlice), newSlice)
}
/*
輸出:
0xc00000c030 3 3 [1 2 3]
0xc00000c060 4 6 [1 2 3 1] // append一個元素之後,切片中指標指向一個擴容後新的陣列(地址指標變化,這裡列印出的是切片地址,無論是否擴容newSlice和slice記憶體地址必然是不同的,和二者擁有的地址指標不要搞混)
0xc00000c030 3 3 [100 2 3] // 修改slice[0]的元素為100
0xc00000c060 4 6 [1 2 3 1] // 但是newSlice[0]中的元素沒有變化(這裡才可以證明兩個切片的地址指標指向兩個不同的陣列)
*/
len < cap 時新增元素
此時呼叫append方法新增一個元素1並不會建立新的陣列,但是1會去覆蓋掉array[2]。
func main() {
array := [4]int{1, 2, 3, 4}
slice := array[0:2]
fmt.Printf("%p %d %d %v\n", &slice, len(slice), cap(slice), slice)
newSlice := append(slice, 1)
fmt.Printf("%p %d %d %v\n", &newSlice, len(newSlice), cap(newSlice), newSlice)
slice[0] = 100
fmt.Printf("%p %d %d %v\n", &slice, len(slice), cap(slice), slice)
fmt.Printf("%p %d %d %v\n", &newSlice, len(newSlice), cap(newSlice), newSlice)
fmt.Println("array = ", array)
}
/*
輸出:
0xc00000c030 2 4 [1 2] // 這裡看到從陣列中初始化的slice的len==2且cap==4
0xc00000c060 3 4 [1 2 1] // append之後,新增一個元素,len變為3(且append元素會覆蓋底層陣列,這裡1覆蓋了之前的3)
0xc00000c030 2 4 [100 2] // 注意,這裡slice列印出來的len還是2,slice的len和cap與newSlice的len和cap是隔離的,儘管它們地址指標指向的是同一個陣列,這裡修改了slice[0]=100
0xc00000c060 3 4 [100 2 1] // newSlice[0]也被修改為100
array = [100 2 1 4] // 證明指向的array陣列也被修改了
*/
結束
快過年了,祝大家新年快樂,春招offer拿不停。
建了一個春秋招備戰/內推/閒聊群,歡迎大家加入。
關注公眾號【程式設計師白澤】,帶你走近一個有點話癆的程式設計師/學生黨。