基础技能树-21 数组

小说:火锅浏览器能挣多少钱作者:杜海更新时间:2019-03-21字数:28602

基础技能树-21 数组


本节内容

  • 数组值类型和指针差异
  • 数组指针和指针数组的差别
  • 切片为什么不是动态数组或数组指针
  • 用new或make创建引用类型的差别[付费阅读]
  • 切片和数组的性能差异[付费阅读]

数组值类型和指针差异

数组很简单,因为数组是所有类型的基础类型。一个整数也是一个字节数组,比如说64位整数在内存中是由8个字节组成的字节数组。所有的地址空间都可以看成一个字节数组,地址就是数组的序号,数组是所有类型里面最基础的类型,而且它还有个特点是数组的访问效率是高的,因为我们只需要给出它的下标就可以进行访问某一个元素,我们知道给出一个下标的情况下正常情况下编译器会优化成很简单的偏移寻址操作,因为我们知道对于寻址操作的效率非常高。

数组是最基础的数据结构,数组和其他数据结构基本差别是什么呢?数组通常是一块完整的内存,基于序号的访问模式。其实整个栈的管理就像一个数组,可以看作都是字节,SP+偏移量就是访问内存,我们看上去所有的数据结构都是由数组的方式构成,因为我们的内存空间可以看成数组。

很多语言里对于数组处理有两种做法,比如在c语言里对于数组的处理是这样的,比如数组A,A代表的是起始地址,所以你可以对A做加法运算,每加上一个序号偏移实际上就是根据元素的长度来做偏移寻址。

go语言里数组也是一个连续内存,但是数组本身并不代表指针了,你可以获得A[0]、A[1]的地址,但是A本身不再代表A的起始地址。

所以在不同语言里面数组符号名字究竟代表什么是有很大的差别的,起码在go语言里A代表数组一整块内存,而在c语言里A代表起始位置。

正是因为这个差别会带来什么问题呢?比如c语言里向函数传递A的时候传递过后的是个指针,而go语言传递A的时候代表是一连串的数据。对于汇编层面来说,你传递数据它会复制数据,如果你传递的是指针它就复制指针,你传递一连串数据它就会复制数据。那么这地方就有个差别,在c语言里传递指针你只是复制指针,传递过去以后两边还共享同一份数组,因为两个指针指向同一个数组,我们管这种方式叫做按引用传递。那么在go语言里A代表的是整个数据,传递的时候会发生把整个数据复制一遍,它们会各自持有不同的复制体。

package main

func test(x [3]int) {
    println("test:", &x)
    x[1] += 100
}

func main() {
    x := [3]int{1, 2, 3}
    test(x)
    println("main:", &x, x[1])
}

通过指针来判断是不是同一个对象,还有个做法是对数组的修改是否会影响外面的数据。

$ go run test.go

除此之外还可以用反汇编来看

$ go build -gcflags "-l" -o test
$ go tool objdump -s "main.main" test

看到首先把1,2,3保存了一份MOVQ $0x1, 0x20(SP),接下来把这些数据拷贝到传参的区域MOVQ 0x20(SP), BX,最后调用test。所以从这一块我们可以看出数组被复制的过程。

我们改成指针看看复制的是什么

package main

func test(x *[3]int) {
    println("test:", x)
    x[1] += 100
}

func main() {
    x := [3]int{1, 2, 3}
    test(&x)
    println("main:", &x, x[1])
}
$ go build -gcflags "-l" -o test
$ go tool objdump -s "main.main" test

看到首先把1,2,3保存了一份MOVQ $0x1, 0x18(SP),接下来把数组起始地址保存起来LEAQ 0x18(SP), BX,最后把地址放到SP调用test。这时候我们注意到复制的仅仅是指针,没有复制元素。

我们一定要搞清楚当我们传递数组过去的时候究竟复制了什么东西?你复制的仅仅是指针还是复制的是所有的元素,这地方很大差别的。

数组指针和指针数组的差别

数组指针指的是数组的起始位置,我们通常用一个对象保存这个地址,我们管这个对象叫做数组指针。

指针和数组不是一回事。指针指的是独立的变量,这个变量里面存了某一个地址,我们管这个叫指针,指针是个对象,它是要分配内存的,而地址只是抽象的序号,所以指针和地址并不是一回事。

我们把一个变量保存了数组的起始位置,我们管这个东西叫做数组指针。指针数组说白了有个数组,数组里面保存的全是指针,就是说一个数组的元素类型是指针的情况下,我们管这种东西叫指针数组,就等于整数数组整数换成指针而已。

数组在不同的语言里不同的实现,因为有些语言支持动态数组有些语言不支持,这里的动态数组和链表并不是一回事。大多数情况下像Python很少去使用数组,虽然类库支持。但我们通常使用链表的构造方式,链表的底层有可能是数组实现有可能是链表实现,所以当我们选择一个结构使用时需要搞清楚到底是什么东西。数组在不同的语言表现方式也不一样,可能是引用传递,就是传递的是指针,也可能是值传递,把整个数组拷贝过去。数组有两种,一种引用整个数组的指针,或者数组里面存的全是指针,这两者是有差别的。

切片为什么不是动态数组或数组指针

在go语言里面不支持动态数组的,一般情况下什么称之为动态数组呢?像c语言里面我们可以这样做:

x = 100
var N[x] int

N[x]就是这个x是在运行期决定的,但是go语言不支持,go语言数组下标必须是个常量,也就是在编译的时候必须要确定的。这样一来就可能有个问题,我们不可能在编译的时候提前把这个长度确定下来。所以我们需要有个机制怎么样在运行期动态创建指定长度的数组。这个就会设计一些简单的指针操作。

c语言最简单的做法分配一段内存,分配完了以后,把指向这段内存的指针转换为比如长度为3的[3] int的类型就可以了。

go语言简单的做法是用了一种称之为切片类型的东西动态指定,切片类型就是一种复合类型,动态分配一段内存,然后有个指针指向内存位置,然后有个长度信息和容量信息。分配内存的时候指定的长度,可以在运行期确定的,但是把这段内存用起来有两种做法,一种像c语言那样做指针类型转换,第二种你得有种方式来管理这段内存。go语言因为默认不支持指针类型转换,必须用种方式来管理内存,怎么管理呢?它用固定长度的头来管理,第一个用指针指向开始位置,接下来长度信息和容量信息。为什么会有后面两个东西?

对于数组的管理不同语言不同做法,理论上数组就是静态,因为数组一次性分配好之后通常情况下不会对数组进行扩容。系统调用有对数组扩容的做法,但是有个限制。比如我们声明一个数组,假设有4个元素,我们扩容的前提是如果后面的地址空间没有被分配过,那么可以在后面进行扩容,比如C语言支持这样去做,但如果后面的空间被占用了,在其他的地方去分配你需要的容量,然后把原来的数据拷贝过来,最后把指针指向新的数组,原来的地方放弃,很显然知道这样的扩容方式付出一定的代价的,因为数组本身很小的话,你拷贝数据无所谓,但如果数组本身很大的话,拷贝所消耗的代价可能会很大。

另外对数组进行管理,我们通常会提供一个额外的数据结构来管理数组,比如像最常见的方式是切片,首先说下什么是切片,切片从字面含义上说它引用数组一个片段,比如A引用数组位置1-4的片段,也可以B引用数组位置3-5的片段,很显然引用这样片段的话需要什么样的数据结构呢?

第一个肯定需要提供一个指针pointer,需要知道从哪地方开始,比如A从数组位置1开始,B从数组位置3开始。那么为什么用指针不用index呢?因为你用index,前提你需要保存index为0的位置,否则index不知道从哪里开始,index为0的位置需要保存,要么是指针要么是原来数组的引用,意思就是要么引用数组,要么引用数组的指针,这样的话还是需要一个指针,那么数据结构就需要数组的指针加上index,这样反而把事情搞复杂了,所以我们直接需要从哪地方开始就可以简化了。

第二个究竟需要引用多长,片段需要有长度cap,那么很显然你想引用数组片段起码用两个属性才可以。go语言对这个做了进一步扩充,它在这基础之上做了这样一个操作,比如一个底层数组,数据结构首先是你从哪边开始的指针pointer,然后引用多大的容量cap,还有个属性叫长度len,长度的意思是我引用了那么大的容量现在对它完整读写,假如当动态内存管理的话,我依次拿回四个位置,但是我没必要对四个位置进行读写,那么我们可以用一个指针来表示哪个位置可以读写,直到可以读写的指针和容量相等,防止越界。所以额外提供一个当前可操作的范围,所以len表示当前可读写操作的范围,cap表示完整的容量防止越界,所以完整的切片提供了三个属性,其中可读写的范围表示这里面可能有数据可能没有数据,如果一开始len等于cap表示所有地方都可以读写,那么就还原成数组的操作方式,但是如果len是动态的话,好处在于只要移动len的位置实际上类似于扩容的机制,这就是切片简单的数据结构,用切片模拟动态数组,说白了就是预分配,你预分配足够大的内存,这个内存本来就是一个数组,然后提供两个额外的属性对这个数组读写范围的限定以及总容量的限定,而修改len来实现类似于扩容的机制。例如使用缓存,可以先申请1MB的内存,一开始有个len通过自增,for循环只要0到len的范围就可以了,修改len实现扩容。切片可以帮助我们更好的操作数组,切片不等于数组。

切片严格意义来说不是动态数组,切片是一个很简单的对数组进行管理的数据结构,但是它本身并不是数组,它通过一个指针引用一个数组,本身显然不是数组,是一个结构体。切片是管理数组中的片段,但切片本身并不是数组,虽然我们操作数据看上去和数组类似,但这仅是一种二级代理机制。所以我们返回切片类型大小的时候,sizeof(type)实际上是三个字段相加的结果而不是它引用数组的长度。

我们一定要搞清楚每种数据类型本身的数据结构是什么,至于这种类型引用其他的东西,比如切片引用数组,那个对象并不属于当前数据结构的一部分,只是它引用了另外一块内存,但这个内存并不属于当前类型的一部分。我们可以创建一个切片,然后编译器替我们自动去分配底层数组,看上去好像这两块内存是一次性完成的,但是从底层布局来说这两个完全是独立的。

当前文章:http://ayzwzx.cn/content/2019-01/26/content_97603.html

发布时间:2019-03-21 00:33:29

微店卖家的钱 手机刷单app苹果 带融字的高炮口子 淘宝新手能赚钱吗 赚钱棋牌游戏大全 促促返利助手 招商加盟代理 赚钱宝和支付宝 肯德基员工餐价格表 京东电话卡佣金

23289 48880 25532 90883 16807 6764795130 33037 94319

我要说两句: (0人参与)

发布