在Java中,二维数组是多个一维数组。这意味着这些一维数组在内存上不是连续的。
相比之下,在C语言中,二维数组实际上是一个大小为总行数 * 总列数的一维数组。因为Go语言从C语言中借鉴了很多概念。
那么我的问题是:Go语言中的二维数组的内存表示是像C语言还是像Java呢?
在Java中,二维数组是多个一维数组。这意味着这些一维数组在内存上不是连续的。
相比之下,在C语言中,二维数组实际上是一个大小为总行数 * 总列数的一维数组。因为Go语言从C语言中借鉴了很多概念。
那么我的问题是:Go语言中的二维数组的内存表示是像C语言还是像Java呢?
在Go语言中,切片(slices)经常被误解为数组(arrays),因此我将对两者进行回答。
引用自规范:数组类型:
数组类型始终是一维的,但可以组成多维类型。
这就是你清晰明了的答案。但是,由于数组在Go中是值,而不像切片那样是描述符(头),所以答案并不仅仅如此。
看一个简单的例子:
x := [5][5]byte{}
fmt.Println(&x[0][3])
fmt.Println(&x[0][4])
fmt.Println(&x[1][0])
输出结果(在Go Playground上尝试):
0x10432203
0x10432204
0x10432205
正如您所看到的,分配和使用数组的内存是连续的:第二行从上一行的最后一个元素地址的下一个地址开始。
检查数组的大小:
fmt.Println(unsafe.Sizeof([4][6]int{})) // 96
fmt.Println(unsafe.Sizeof([6][4]int{})) // 96
无论您切换行和列,其大小都相同。
在切片的情况下也是如此:多维切片是切片的切片。规范:切片类型:
切片是对底层数组连续段的描述,并提供对从该数组中编号序列的元素的访问。
...
像数组一样,切片始终是一维的,但可以组合以构建更高维度的对象。
切片是描述符,一个切片头包含指向底层(备份)数组的元素的指针、长度和容量。因此,总切片数在内存使用方面很重要。
请参阅此示例:
x := make([][]byte, 2)
for i := range x {
x[i] = make([]byte, 1000)
}
fmt.Println(len(x), len(x)*len(x[0]))
y := make([][]byte, 1000)
for i := range y {
y[i] = make([]byte, 2)
}
fmt.Println(len(y), len(y)*len(y[0]))
输出(请在Go Playground中尝试):
2 2000
1000 2000
多维切片 x
和 y
的元素总数均为 2000(字节),但是 x
只存储有 2
个拥有 1000
元素的切片,而 y
则存储有 1000
个拥有 2
元素的切片。
这意味着 x
需要 2
个切片头,而 y
需要 1000
个切片头(每个元素需要一个,加上 x
和 y
自身需要的)!
切片头由 reflect.SliceHeader
表示:
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
在32位架构上,切片头的大小为12字节,在64位架构上为24字节。因此,在32位架构中,x
的元素需要占用2000个字节加上2x12个字节的内存,即2024字节,而y
的元素则需要占用2000个字节加上1000x12个字节的内存,即14,000字节。
还要注意,多维切片的元素可能包含不同长度的切片:
对于数组的数组,内部数组是根据构造方式始终具有相同的长度;然而,对于切片的切片(或者是切片的数组),内部长度可以动态变化。此外,内部切片必须逐个初始化。
就像这个例子:
x := make([][]byte, 2)
x[0] = []byte{1, 2}
x[1] = []byte{1, 2, 3, 4}
fmt.Println(x[0])
fmt.Println(x[1])
输出(您可以在Go Playground上尝试):
[1 2]
[1 2 3 4]