分析append引出的切片內存問題


分析append引出的切片內存問題


今天羣裏討論一個關於切片append的坑

a := []int{1}
a = append(a,2)
a = append(a,3)

b := append(a,4)
c := append(a,5)
fmt.Println(a)
fmt.Println(b)
fmt.Println(c)

在這裏插入圖片描述
一般思路我們是想到添加元素後b爲[1,2,3,4],c爲[1,2,3,5],但是爲什麼結果會是兩個[1,2,3,5],4去哪兒了?其實一般思路是錯誤的。在理解append函數用法之前,我們應該先了解golang的切片內存模式是什麼樣的。
將源代碼改爲輸出切片的長度和容量,還有切片引用的地址

a := []int{1}
fmt.Println("cap(a) =", cap(a), "len(a)=", len(a), "ptr(a) =", &a[0])

a = append(a, 2)
fmt.Println("cap(a) =", cap(a), "len(a)=", len(a), "ptr(a) =", &a[0])

a = append(a, 3)
fmt.Println("cap(a) =", cap(a), "len(a)=", len(a), "ptr(a) =", &a[0])

b := append(a, 4)
fmt.Println("cap(a) =", cap(a), "len(a)=", len(a), "len(b)=", len(b), "ptr(a) =", &a[0], "ptr(b) =", &b[0])

c := append(a, 5)
fmt.Println("cap(a) =", cap(a), "len(a)=", len(a), "len(c)=", len(c), "ptr(a) =", &a[0], "ptr(c) =", &c[0])

在這裏插入圖片描述


先來理解以下代碼深入理解切片內存,運行觀察結果

//創建底層數組,在底層數組基礎上創建切片
	fmt.Println("------1.創建底層數組,在底層數組基礎上創建切片------")
	num := [10]int{0,1,2,3,4,5,6,7,8,9}//len=10
	s1 := num[:5]
	s2 := num[3:8]
	s3 := num[5:]
	s4 := num[:]
	fmt.Println("num:", num)//[0,1,2,3,4,5,6,7,8,9]
	fmt.Println("s1:", s1)//[0,1,2,3,4]
	fmt.Println("s2:", s2)//[3,4,5,6,7]
	fmt.Println("s3:", s3)//[5,6,7,8,9]
	fmt.Println("s4:", s4)//[0,1,2,3,4,5,6,7,8,9]
	fmt.Printf("%p\n",&num) //0xc000014230
	fmt.Printf("%p\n",s1)  //0xc000014230,與num的地址相同
	fmt.Printf("s1  len:%d ,cap:%d \n",len(s1),cap(s1)) // len:5 ,cap:10
	fmt.Printf("s2  len:%d ,cap:%d \n",len(s2),cap(s2)) // len:5 ,cap:7
	fmt.Printf("s3  len:%d ,cap:%d \n",len(s3),cap(s3)) // len:5 ,cap:5
	fmt.Printf("s4  len:%d ,cap:%d \n",len(s4),cap(s4)) //len:10 ,cap:10

    //修改底層數組
    fmt.Println("------2.修改底層數組------")
	num[4] = 100
	fmt.Println(s1) //[0 1 2 3 100]
	fmt.Println(s2) //[3 100 5 6 7]
	fmt.Println(s3) //[5 6 7 8 9]
	fmt.Println(s4) //[0 1 2 3 100 5 6 7 8 9]

	//修改切片
	fmt.Println("------3.修改切片------")
	s2[1] = 99
	fmt.Println(num) //[0 1 2 3 99 5 6 7 8 9]
	fmt.Println(s1) //[0 1 2 3 99]
	fmt.Println(s2) //[3 99 5 6 7]
	fmt.Println(s3) //[5 6 7 8 9]
	fmt.Println(s4) //[0 1 2 3 99 5 6 7 8 9]

	fmt.Println("------4.使用append修改切片內容------")
	s1 = append(s1,1,1,1,1)
	fmt.Println(num) //[0 1 2 3 99 1 1 1 1 9]
	fmt.Println(s1) //[0 1 2 3 99 1 1 1 1]
	fmt.Println(s2) //[3 99 1 1 1]
	fmt.Println(s3) //[1 1 1 1 9]
	fmt.Println(s4) //[0 1 2 3 99 1 1 1 1 9]
	fmt.Println(len(s1),cap(s1)) //len=9,cap=10

	fmt.Println("-------5.append添加元素擴容-------")
	s1 = append(s1,2,2,2,2,2) //因爲s1的len=9,cap=10,添加5個元素cap不夠,只能擴容重新指向一個新的底層數組
	fmt.Println(num) //[0 1 2 3 99 1 1 1 1 9]
	fmt.Println(s1) //[0 1 2 3 99 1 1 1 1 2 2 2 2 2]
	fmt.Println(s2) //[3 99 1 1 1]
	fmt.Println(s3) //[1 1 1 1 9]
	fmt.Println(s4) //[0 1 2 3 99 1 1 1 1 9]
	fmt.Printf("%p\n",&num) //0xc000014230,地址沒變
	fmt.Printf("%p\n",s1) //0xc000086000,地址改變,這是因爲擴容後重新指向了一個新的底層數組
	fmt.Println(len(s1),cap(s1)) //s1的len=14,,cap=20.cap擴容是在原來10的基礎上成倍擴容

觀察以上代碼,不難發現,每一個切片s都引用了同一個底層數組num,即切片本身不存儲數據,都是底層數組存儲數據,而切片只不過是對底層數組的一段引用,直接修改切片內容的同時,底層數組也會隨其改變。往切片添加數據時,若沒有超過切片的cap,那麼直接添加,並且添加的值會相應覆蓋底層數組的原值(如上述代碼4步驟中往s1後添加多個數據)


切片是如何內存分配以及擴容的?

s := []int{1,2,3} //len:3,cap:3
s = append(s,4,5) //len:5,cap:6
s = append(s,6,7,8) //len:8,cap:12
s = append(s,9,10) //len:10,cap:12

在這裏插入圖片描述

再修改代碼運行結果

a := []int{1}
	a = append(a,2)
	a = append(a,3)

	b :=append(a,4)
    fmt.Println(b)
	fmt.Println("----分割線-----")
	c :=append(a,5)
	fmt.Println(a)
	fmt.Println(b)
	fmt.Println(c)

在這裏插入圖片描述
很明顯問題出在b和c定義賦新切片的操作
關於切片的append方法,官方給出的解釋如下

如何使用append函數?

slice2 := append(slice1,23,15)

以上對切片slice1進行append操作,該操作遵循以下原則:
1.append函數對一個切片slice1進行追加操作,並返回另一個長度爲len(slice1)+追加個數的切片,原切片不被改動,兩個切片所指向的底層數組可能是同一個也可能不是,取決於第二條
2.slice1是對底層數組的一段引用,若append追加完之後沒有突破slice1的容量,則實際上追加的數據改變了其底層數組對應的值,並且append函數返回對底層數組的新引用(切片);若append追加的數量突破了slice1的最大容量(底層數組長度固定,無法增加長度賦予新值),則Go會在內存中申請新的數組(數組內的值爲追加操作之後的值),並返回對新數組的引用(切片)

(人話來說就是先判斷切片容量,沒滿則修改,滿就擴容成新的返回新的切片)

在這裏插入圖片描述

切片共享內部結構

在這裏插入圖片描述
在這裏插入圖片描述

a := []int{1,2}
//fmt.Println(cap(a))//2
b := append(a[0:1],3)
fmt.Println(a[1:2])//[3]
c := append(a[1:2],4)
//fmt.Println(cap(c))//2
fmt.Println(a)//[1,3]
fmt.Printf("a的地址是: %p \n",&a[0])//0xc0000a0090 
fmt.Println(b)//[1,3]//切片容量沒滿,可修改內容
fmt.Printf("b的地址是: %p \n",&b[0])//0xc0000a0090 
fmt.Println(c)//[3,4]//切片容量已滿,新切片
fmt.Printf("c的地址是: %p \n",&c[0])//0xc0000a00c0 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章