切片面試題:學習切片長度、容量,切片增長的過程

個人博客地址:https://zhounanjun.gitbook.io/nanjun/

關於切片的面試題:摘自https://goquiz.github.io/#subslice-grow

func Subslice() {
   s := []int{1, 2, 3,4,5,6,7,8,9}
   ss := s[3:6]
   fmt.Printf("len ss : %d\n", len(ss))
   fmt.Printf("Cap ss : %d\n", cap(ss))
   ss = append(ss, 4)
   fmt.Printf("len ss : %d\n", len(ss))
   fmt.Printf("Cap ss : %d\n", cap(ss))
   for _, v := range ss {
      v += 10
   }

   for i := range ss {
      ss[i] += 10
   }

   fmt.Println(s)
}

大家可以看一下結果是什麼,結果是:

 

len ss : 3
Cap ss : 6
len ss : 4
Cap ss : 6
[1 2 3 14 15 16 14 8 9 0 1 3]

下面是解析過程:

首先ss := s[3:6],結果就是截取索引在[3, 6)上的數據,所以len(ss)是3,那ss的cap容量爲啥是9呢?

一個切片的容量就是該切面在底層數組山的開始位置向右擴展至數組的結束位置,在這邊就是索引位置3到索引位置8 ,一共六個元素,append()操作並沒有導致容量增加,因爲切片容量爲6,加上元素4長度纔是4,不會導致切片擴容。因爲切片沒有擴容,引用的還是底層s數組,所以更改ss上的元素的大小,數組s上的值也會跟着改變。

大家再看下這道題的打印結果是什麼:

func subSlice2() {
   s := []int{1, 2, 3}
   ss := s[1:]
   ss = append(ss, 4)

   for _, v := range ss {
      v += 10
   }

   for i := range ss {
      ss[i] += 10
   }
   fmt.Println(s)
}

 

結果是:

[1 2 3]

下面是解析過程:

想不通的是爲啥改了ss的值,但是數組s不變呢。原因是,ss:=s[1:]後,ss長度爲2,cap爲2。append後長度大於2所以切片擴容,擴容後的切片指向新的數組,與原數組s無關,所以對ss修改值,原數組的值不會變。

下面,介紹下底層append的代碼,讓大家明白這個過程。

首先append函數是go的內置函數,所以但是go並不開放內置函數的詳細代碼(如果有人看到源碼,請在博客評論中告訴我,謝謝),所以我只在builtin.go中找到如下代碼:

// The append built-in function appends elements to the end of a slice. If
// it has sufficient capacity, the destination is resliced to accommodate the
// new elements. If it does not, a new underlying array will be allocated.
// Append returns the updated slice. It is therefore necessary to store the
// result of append, often in the variable holding the slice itself:
// slice = append(slice, elem1, elem2)
// slice = append(slice, anotherSlice...)
// As a special case, it is legal to append a string to a byte slice, like this:
// slice = append([]byte("hello "), "world"...)
func append(slice []Type, elems ...Type) []Type

 

對於append函數的細節,註釋裏講了,內置函數將新元素接到切片的後面,如果切面有足夠的容量,則切片可以容納新元素。如果切面容量不夠,一個新的底層數組將會被分配。李笑來老師說過:english + compute skills = freedom。大家多看看源碼還是有好處的。繼續看下源碼slice.go下的這個growslice 函數(主要用於處理append過程中Slice的增長),主要看下cap容量這個參數是怎麼變化的?分析過程見下面的代碼中文註釋

// growslice handles slice growth during append.
// (這句話告訴你傳入參數有哪些?元素類型、老的slice、需要的容量)It is passed the slice element type, the old slice, and the desired new minimum capacity,
// and it returns a new slice with at least that capacity, with the old data
// copied into it.
// The new slice's length is set to the old slice's length,
// NOT to the new requested capacity.
// This is for codegen convenience. The old slice's length is used immediately
// to calculate where to write new values during an append.
// TODO: When the old backend is gone, reconsider this decision.
// The SSA backend might prefer the new length or to return only ptr/cap and save stack space.
func growslice(et *_type, old slice, cap int) slice {
   if raceenabled {//常量false,不用管
      callerpc := getcallerpc()
      racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, funcPC(growslice))
   }
   if msanenabled {//常量False不用管
      msanread(old.array, uintptr(old.len*int(et.size)))
   }

   if cap < old.cap {//需要的容量比老容量小,就報錯
      panic(errorString("growslice: cap out of range"))
   }

   if et.size == 0 {//類型大小爲0就不
      // append should not create a slice with nil pointer but non-zero len.
      // We assume that append doesn't need to preserve old.array in this case.
      return slice{unsafe.Pointer(&zerobase), old.len, cap}
   }
    //下面正式開始新的容量計算
   newcap := old.cap //老容量給newcap
   doublecap := newcap + newcap //兩倍老容量
   if cap > doublecap { //如果需要的容量大於兩倍老容量,那新容量就是需要的容量
      newcap = cap
   } else {//需要的容量小於兩倍老容量
      if old.len < 1024 {//且老切片長度小於1024
         newcap = doublecap//則新容量爲兩倍老容量
      } else {//老切片長度大於1024
         // Check 0 < newcap to detect overflow
         // and prevent an infinite loop.
         for 0 < newcap && newcap < cap {
            newcap += newcap / 4 //老容量以125%增長率增長,知道newcap增加到比需要的容量大
         }
         // Set newcap to the requested cap when
         // the newcap calculation overflowed.
         if newcap <= 0 {
            newcap = cap //額外情況,老容量爲0,那新容量就是需要的容量
         }
      }
   }
//下面計算溢出的,就不用看了
   var overflow bool
   var lenmem, newlenmem, capmem uintptr
   // Specialize for common values of et.size.
   // For 1 we don't need any division/multiplication.
   // For sys.PtrSize, compiler will optimize division/multiplication into a shift by a constant.
   // For powers of 2, use a variable shift.
   switch {
   case et.size == 1:
      lenmem = uintptr(old.len)
      newlenmem = uintptr(cap)
      capmem = roundupsize(uintptr(newcap))
      overflow = uintptr(newcap) > maxAlloc
      newcap = int(capmem)
   case et.size == sys.PtrSize:
      lenmem = uintptr(old.len) * sys.PtrSize
      newlenmem = uintptr(cap) * sys.PtrSize
      capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
      overflow = uintptr(newcap) > maxAlloc/sys.PtrSize
      newcap = int(capmem / sys.PtrSize)
   case isPowerOfTwo(et.size):
      var shift uintptr
      if sys.PtrSize == 8 {
         // Mask shift for better code generation.
         shift = uintptr(sys.Ctz64(uint64(et.size))) & 63
      } else {
         shift = uintptr(sys.Ctz32(uint32(et.size))) & 31
      }
      lenmem = uintptr(old.len) << shift
      newlenmem = uintptr(cap) << shift
      capmem = roundupsize(uintptr(newcap) << shift)
      overflow = uintptr(newcap) > (maxAlloc >> shift)
      newcap = int(capmem >> shift)
   default:
      lenmem = uintptr(old.len) * et.size
      newlenmem = uintptr(cap) * et.size
      capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
      capmem = roundupsize(capmem)
      newcap = int(capmem / et.size)
   }

   // The check of overflow in addition to capmem > maxAlloc is needed
   // to prevent an overflow which can be used to trigger a segfault
   // on 32bit architectures with this example program:
   //
   // type T [1<<27 + 1]int64
   //
   // var d T
   // var s []T
   //
   // func main() {
   //   s = append(s, d, d, d, d)
   //   print(len(s), "\n")
   // }
   if overflow || capmem > maxAlloc {
      panic(errorString("growslice: cap out of range"))
   }

   var p unsafe.Pointer
   if et.kind&kindNoPointers != 0 {
      p = mallocgc(capmem, nil, false)
      // The append() that calls growslice is going to overwrite from old.len to cap (which will be the new length).
      // Only clear the part that will not be overwritten.
      memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
   } else {
      // Note: can't use rawmem (which avoids zeroing of memory), because then GC can scan uninitialized memory.
      p = mallocgc(capmem, et, true)
      if writeBarrier.enabled {
         // Only shade the pointers in old.array since we know the destination slice p
         // only contains nil pointers because it has been cleared during alloc.
         bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem)
      }
   }
   memmove(p, old.array, lenmem)

   return slice{p, old.len, newcap}
}

 

容量增長的過程:

1、輸入參數,切片,需要的容量

2、要是需要的容量比兩倍老容量都大,那新的容量大小就是需要的容量大小

3、要是需要的容量比兩倍老容量小:a、且老切片的長度小於1024個,則新容量大小爲老容量的2倍 b、老切片長度大於1024,則老切片長度*(1.25)^n直達老切片長度大小超過需要的容量。

所以第二題的cap爲4,原先cap爲2,需要的cap爲3,因爲老切片長度爲2小於2014所以直接老切片長度兩倍就可以了。然後第二題的數組s的值沒有變的原因是,Append發現老切片cap已經不夠了,需要申請新切片。新切片底層數組就不是原來的數組了,所以對切片ss每個元素+10,是不影響老數組的元素的。

 

 

 

發佈了159 篇原創文章 · 獲贊 141 · 訪問量 26萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章