先画饼
面试官 : 请你说一下
值类型引用类型值传递引用传递四个概念吧你 : 小伙子你不讲武德,这好吗 ? 这不好!
面试官 : 你还有什么想问我的吗?
之前我也是偶然看到这几个概念,以为自己挺了解,然后发现把这四个词放在一起就蒙了….
所以抽空理一理
值类型和引用类型
- 值类型的变量直接持有数据 数据存放在栈上 引用类型的变量持有的数据的引用, 数据本身会放在堆上, 而在栈上会放一个指向这个数据 (储存在堆上) 的指针
- 定义值类型和引用类型时变量时, 值类型会默认分配内存(当然值为
0, 或者false之流), 而声明引用类型则不一样,也就是说声明引用类型之后只在堆上建立了一个引用,但是并没有分配内存, 此时给引用类型赋值就会 panic - 那么有小朋友就要问了, 有人说引用类型的初始值是
nil那和你这个说的不一样啊 ,其实nil也没有错 ,nil的原因呢就是 ,声明了一个引用类型变量 ,这个引用类型变量指向栈上的某个地址,而这个地址上的值应该指向堆上的某个地方,但是没有分配空间, 所以为nil
|
|
-
当你把一个变量赋给新变量的时候呢, 值类型是创建一个新的副本, 并把它给新变量;引用类型呢 就是吧对于数据本身的指向赋值给新变量, 两个变量还是指向的同一块数据, 以代码为例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50func main() { var m int m = 1 fmt.Println("m初始值", m) fmt.Println("m地址", &m) a := m fmt.Println("a初始值", a) fmt.Println("a地址", &a) a = 2 fmt.Println("m最终值", m) fmt.Println("m地址", &m) fmt.Println("a最终值", a) fmt.Println("a地址", &a) return } //➜ go run "/home/guo/code/c/main.go" //m初始值 1 //m地址 0xc000014110 //a初始值 1 //a地址 0xc000014118 //m最终值 1 //m地址 0xc000014110 //a最终值 2 //a地址 0xc000014118 func main() { m := map[int]int{} m[1] = 1 fmt.Println("m[1]初始值", m[1]) fmt.Println("m地址", &m) a := m fmt.Println("a初始值", a[1]) fmt.Println("a地址", &a) a[1] = 2 fmt.Println("m最终值", m[1]) fmt.Println("m地址", &m) fmt.Println("a最终值", a[1]) fmt.Println("a地址", &a) return } //➜go run "/home/guo/code/c/main.go" //m[1]初始值 1 //m地址 &map[1:1] //a初始值 1 //a地址 &map[1:1] //m最终值 2 //m地址 &map[1:2] //a最终值 2 //a地址 &map[1:2] -
引用类型可以使用
makenew分配堆上的内存 -
如果你要问我说
指针变量是一种什么类型 我的回答是无可奉告

引用传递和值传递
值类型传递一定是值传递, 引用类型传递不一定是引用传递
Golang 中只用值传递一种传递类型
小伙子,只要记住上面这两句话,就错不了
迷惑人心的Map
-
按照上面来说, 按照 **值传递 **的特性,我们毫无疑问的猜想:函数外两次输出的结果应该是相同的,同时地址应该不同, 然而…….
-
事实真的是这样吗?
-
看
demo
|
|
- 事实却正是相反
- 两个示例代码的结果竟然截然相反,如果上述的内容让你产生了疑惑,并且你希望彻底的了解这过程中发生了什么~
|
|
如源码所示, make() 函数调用会最终调用到 makemap() 函数, 上面源码返回的是一个指针 ,也就是说, 变量 m 底层的话实际上也是由指针实现的 ,所以虽然 是按值传递, 但是拷贝的不是 map 本身, 而是 map的指针, 指针拷贝一份还是指向那个 map 呀~
Channel 也是这样吗
- 直接先看代码
|
|
- 看源码
|
|
- 和
map一样 返回的也是一个指针, 这里就不多说了
切片永远的神
-
踩过坑的小伙伴都知道 ,
slice确确实实是按值传递的 也不是像上面两个兄弟一样传递指针 -
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29package main import "fmt" func main() { sl := []string{ "a", "b", "c", } fmt.Printf("%v, %p\n",sl, sl) test_slice(sl) fmt.Printf("%v, %p\n",sl, sl) } func test_slice(sl []string){ fmt.Printf("%v, %p\n",sl, sl) sl[0] = "aa" //sl = append(sl, "d") fmt.Printf("%v, %p\n",sl, sl) } //output //[a b c], 0x442260 //[a b c], 0x442260 //[aa b c], 0x442260 //[aa b c], 0x442260 -
看 源 码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17func makeslice(et *_type, len, cap int) unsafe.Pointer { mem, overflow := math.MulUintptr(et.size, uintptr(cap)) if overflow || mem > maxAlloc || len < 0 || len > cap { // NOTE: Produce a 'len out of range' error instead of a // 'cap out of range' error when someone does make([]T, bignumber). // 'cap out of range' is true too, but since the cap is only being // supplied implicitly, saying len is clearer. // See golang.org/issue/4085. mem, overflow := math.MulUintptr(et.size, uintptr(len)) if overflow || mem > maxAlloc || len < 0 { panicmakeslicelen() } panicmakeslicecap() } return mallocgc(mem, et, true) } -
这里为什么返回的是一个指针呢?
目前的
runtime.makeslice会返回指向底层数组的指针,之前版本的 Go 语言中,数组指针、长度和容量会被合成一个slice结构并返回,但是从 cmd/compile: move slice construction to callers of makeslice 这次提交之后,构建结构体SliceHeader的工作就都交给runtime.makeslice的调用方处理了,这些调用方会在编译期间构建切片结构体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17func typecheck1(n *Node, top int) (res *Node) { switch n.Op { ... case OSLICEHEADER: switch t := n.Type n.Left = typecheck(n.Left, ctxExpr) l := typecheck(n.List.First(), ctxExpr) c := typecheck(n.List.Second(), ctxExpr) l = defaultlit(l, types.Types[TINT]) c = defaultlit(c, types.Types[TINT]) n.List.SetFirst(l) n.List.SetSecond(c) ... } }
OSLICEHEADER操作会创建我们在上面介绍过的结构体SliceHeader,其中包含数组指针、切片长度和容量,它也是切片在运行时的表示:
1 2 3 4 5type SliceHeader struct { Data uintptr Len int Cap int }
- 所以这就是为什么传入切片能够修改外层数据的原因 , 因为数据存在底层的数组里 , 而
slice中则是存储指向数组的指针 - 当 切片不发生扩容的时候, 拷贝的切片 和 函数外的切片指向同一个数组, 所以保持相同
- 当发生扩容时, 会指向新的数组地址, 所以这时候两个切片的值就会不同啦~
- 还有一件事: 既然是按值传递的, 为什么函数内和函数外切片地址是一样的呢?
Pointer()函数中,对于Slice类型的数据,返回的一直是指向第一个元素的地址,所以我们通过
fmt.Printf()中%p来打印Slice的地址,其实打印的结果是内部存储数组元素的首地址
以上内容指针对 Go 语言, 其他语言或许也有相同的地方,但是也会有很大的不同, 特别是C++
参考:
golang的参数引用: https://juejin.im/post/6844903762079776775
go语言设计与实现: https://draveness.me/golang/docs/part2-foundation/ch03-datastructure/golang-array-and-slice/