二维数组的内存表示是什么?

16

在Java中,二维数组是多个一维数组。这意味着这些一维数组在内存上不是连续的。

相比之下,在C语言中,二维数组实际上是一个大小为总行数 * 总列数的一维数组。因为Go语言从C语言中借鉴了很多概念。

那么我的问题是:Go语言中的二维数组的内存表示是像C语言还是像Java呢?


3
为什么在你的问题上加上Java和C的标签,当它只涉及Go?请问您需要翻译哪些内容? - GhostCat
1
因为我认为我的问题涉及到Java和C语言的相关信息,所以我觉得在我的问题上加上这些标签会更加合适。如果我的想法有误,请谅解。 - Trần Kim Dự
1
@TrầnKimDự 尽管这个问题基于你对C和Java的已知,但是该问题并不关乎C或Java本身。因此我已将那些标签删除了。 - code_dredd
2个回答

43

在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

多维切片 xy 的元素总数均为 2000(字节),但是 x 只存储有 2 个拥有 1000 元素的切片,而 y 则存储有 1000 个拥有 2 元素的切片。

这意味着 x 需要 2 个切片头,而 y 需要 1000 个切片头(每个元素需要一个,加上 xy 自身需要的)!

切片头由 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]

如果您还没有阅读过,建议看一下:Go博客:数组、切片(和字符串):'append'的机制

1
感谢您的专业回答。我从您的回答中学到了很多:D - Trần Kim Dự
嗨,非常棒的答案。我知道一维数组将被连续存储,但是你能解释一下多维切片支持数组在内存中如何存储吗?内部切片支持数组是否单独存储在内存中? - mohammad
1
@mohammad Slice 只是一个头部,指向一个支持数组。因此,如果您有一个包含头部的片段片段,它可能指向任何地方。您甚至不能在一步中为一个切片分配一个备份数组,只能分配头部的切片。您必须逐个分配和初始化切片,因此它们最有可能不会连续地结束在内存中。但是,您可以分配一个大的数组(或切片),并重新调整大小以初始化切片的切片,在这种情况下,您将连续分配后备数组。 - icza
很好的回答。只有一个问题:有没有办法打印出多维切片的总大小? - starriet
@starriet 总大小是多少?以字节为单位的内存大小是多少?元素数量是多少?关于前者,请参见如何在Go中获取变量的内存大小?,关于后者,请遍历切片并总结元素计数。 - icza
显示剩余3条评论

1
对我来说,Go中的slice就像C++中的vector。Vector也有三个关键数据成员:指向数据的指针、容量和长度。如果vector是二维的,子向量的内部长度也可能会动态变化。
相比之下,Go中的数组就像C中的数组。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接