1. go中的切片
在go中,"切片是指向数组的指针"这句话是不对的。切片就是切片,有自己的属性和方法,只是借用了数组来存储实际的数据。 切片的数据结构大概为
type slice struct {
point int
len int
cap int
}
即,管理一个数组上以point指向元素的数组下标为起点,加上len长度为终点,最大扩容到cap长度为终点的一段数据。
举例:
func main() {
D := [10]int{0,1,2,3,4,5,6}
A := D[0:3:3]
B := D[2:5:9]
C := D[4:7]
fmt.Printf("A: %v\n", A)
fmt.Printf("B: %v\n", B)
fmt.Printf("C: %v\n", C)
fmt.Printf("D: %v\n", D)
}
注意:
-
- 数组没有指定值的位置会用默认值填充,此处即为0
-
- 创建切片时第三个参数表示切片容量到数组的哪个索引位置,没有就默认到数组末尾,这个下面有用处,所以分三种设置做对比
-
- 从数组取切片时是前闭后开区间,与通常规定操作一致。
运行输出:
A: [0 1 2]
B: [2 3 4]
C: [4 5 6]
D: [0 1 2 3 4 5 6 0 0 0]
修改切片内容的操作如下:
func main() {
D := [10]int{0,1,2,3,4,5,6}
A := D[0:3]
B := D[2:5]
C := D[4:7]
A[2] = 999
B[2] = 666
fmt.Printf("A: %v\n", A)
fmt.Printf("B: %v\n", B)
fmt.Printf("C: %v\n", C)
fmt.Printf("D: %v\n", D)
}
运行输出:
A: [0 1 999]
B: [999 3 666]
C: [666 5 6]
D: [0 1 999 3 666 5 6 0 0 0]
可以看出所有修改都同步了,并且体现在数组D上。
再来看Append操作。Append是扩展切片的长度,但是如果长度超过了预设的容量,就需要换一个底层数组。看下面的程序:
func main() {
D := [10]int{0,1,2,3,4,5,6}
A := D[0:3:3]
B := D[2:5:7]
C := D[4:7]
A = append(A, 333)
B = append(B, 666)
C = append(C, 999)
fmt.Printf("A: %v\n", A)
fmt.Printf("B: %v\n", B)
fmt.Printf("C: %v\n", C)
fmt.Printf("D: %v\n", D)
}
运行输出:
A: [0 1 2 333]
B: [2 3 4 666]
C: [4 666 6 999]
D: [0 1 2 3 4 666 6 999 0 0]
可以看到A因为预设了[0:3:3]的原因,容量只有3,当前已满,再增加一个333,就切换了新的数组,所以A的修改只体现在自身,对B、数组D都没有影响。
而B的容量为7,C的容量为5,都有空间,所以修改体现在了数组D上。
将切片A扩展到容量4,但是增加两个元素:
func main() {
D := [10]int{0,1,2,3,4,5,6}
A := D[0:3:4]
B := D[2:5:7]
C := D[4:7]
A = append(A, 333)
A = append(A, 332)
B = append(B, 666)
C = append(C, 999)
fmt.Printf("A: %v\n", A)
fmt.Printf("B: %v\n", B)
fmt.Printf("C: %v\n", C)
fmt.Printf("D: %v\n", D)
}
运行输出:
A: [0 1 2 333 332]
B: [2 333 4 666]
C: [4 666 6 999]
D: [0 1 2 333 4 666 6 999 0 0]
可以看到添加333时还没有超出切片A的容量,所以333还在数组D上做修改,而添加332时已经超出了A的容量,A换了一个新的数组(现有数据0、1、2、333复制过去),并且在新数组添加332,而不影响原来的数组D。
上面看上去切片的效果很像指针,为什么强调不要把切片理解为数组的指针呢?这里还有一个非常重要的问题,切片作为形参传递给实参时,如果按指针理解很有可能考虑不到形参的改变有时候可能并不会修改形参这个问题(指针不会有这样的结果)。看代码:
func main() {
slice := make([]int, 2, 3)
for i := 0; i < len(slice); i++ {
slice[i] = i
}
fmt.Printf("slice: %v, addr: %p \n", slice, slice)
changeSlice(slice)
fmt.Printf("slice: %v, addr: %p \n", slice, slice)
}
func changeSlice(s []int){
s = append(s, 3)
s = append(s, 4)
s[1] = 111
fmt.Printf("func s: %v, addr: %p \n", s, s)
}
运行输出:
slice: [0 1], addr: 0xc0000a0140
func s: [0 111 3 4], addr: 0xc0000ca030
slice: [0 1], addr: 0xc0000a0140
把changeSlice的s[1] = 111操作提前:
func main() {
slice := make([]int, 2, 3)
for i := 0; i < len(slice); i++ {
slice[i] = i
}
fmt.Printf("slice: %v, addr: %p \n", slice, slice)
changeSlice(slice)
fmt.Printf("slice: %v, addr: %p \n", slice, slice)
}
func changeSlice(s []int){
s[1] = 111
s = append(s, 3)
s = append(s, 4)
fmt.Printf("func s: %v, addr: %p \n", s, s)
}
运行输出:
slice: [0 1], addr: 0xc0000a0140
func s: [0 111 3 4], addr: 0xc0000ca030
slice: [0 111], addr: 0xc0000a0140
首先,从实参和形参的地址可以看出来,实参和形参是两个切片,传参过程中是复制关系,这个不重要,指针传递时也是这样。
第二,实参和形参指向同一个数组,这个也不重要,指针传递时形参和实参也是指向同一片内存区域。
但是上面的代码,先扩容,形参slice的底层数组更换了(相当于形参指针指向了新的内存区域,即给指针重新赋值,但是并没有显式进行这个操作,不深入了解切片可能看不出来),所以s[1] = 111不会影响到实参slice的底层数组,修改也就不会体现在实参slice中。下面的代码,先修改,修改发生在形参slice的底层数组上,也是实参slice的底层数组。所以修改体现在实参slice中。
2. jdk1.6的subString
这一段来自阿里大佬的博文,可以参见。
//JDK 1.6
String(int offset, int count, char value[]) {
this.value = value;
this.offset = offset;
this.count = count;
}
public String substring(int beginIndex, int endIndex) {
//check boundary
return new String(offset + beginIndex, endIndex - beginIndex, value);
}
//JDK 1.7
public String(char value[], int offset, int count) {
//check boundary
this.value = Arrays.copyOfRange(value, offset, offset + count);
}
public String substring(int beginIndex, int endIndex) {
//check boundary
int subLen = endIndex - beginIndex;
return new String(value, beginIndex, subLen);
}
可以看到jdk1.6也是使用的原来的string的底层char数组,而jdk1.7更换为直接新建一个底层数组了。
假设原来有一个string A非常长(大佬的博客有关于string最长限制的讲述,可以去看一下,最大int32大小的长度),我们用subString取一小段之后获得B,并且再也不需要A。此时A应该被GC,但是B引用了A的底层数组,导致这个数组一直无法被回收,造成内存泄漏。
虽然jdk1.6中也有办法便面这个问题,例如:
B = A.substring(x, y) + ""
和空字符串相加,这个方法执行过后重新创建了底层数组。就像go中append超出容量新建底层数组一样。两个string再也不相关。
不大明白,go为什么要用这种设计。