12 值类型与引用类型

[TOC]

值类型与引用类型

一 介绍

值类型:变量内存空间中存放的是值

引用类型:变量内存空间中存放的是地址,地址再指向具体的值

二者的区别就在两点上:

​ 1、赋值操作前,是否需要手动申请内存空间

​ 2、原值赋值给新值,改原值,新值是否受影响,反过来一个道理

二 值类型与引用类型分类

  • 1、值类型:
    • 1、基本数据类型都是值类型:整型、浮点、复数、布尔、字符串
    • 2、复合数据类型之数组(array)
    • 3、复合数据类型之结构体(struct)
  • 2、引用类型:
    • 2.1 复合数据类型之切片(slice)
    • 2.2 复合数据类型之字典(map)
    • 2.3 其他类型之通道(chan)
    • 2.4 其他类型之指针(pointer)
    • ps:函数也属于引用类型​

三 值类型的特点

1、赋值操作前,无需手动申请内存,因为在声明时编译器会依据它们的零值为它们申请好内存空间

// 例1
var a int   //int类型默认值为 0
fmt.Printf("%p\n", &a) // 打印变量本身的值,输出地址为0xc000014060

b := 3
a = 3 // 可以直接赋值

// 例2
var m [2]int           // 数组默认值为[0 0]
fmt.Printf("%p\n",&m)  // 0xc00001c060,因为默认就开辟好了内存空间,所以可以直接&取地址

var n [2]int = [2]int{11, 22}
m = n // 可以直接赋值 
fmt.Println(m) // [11 22]

2、 原值拷贝为一个新值,改原值,新值不受影响

// 例1
var a =10
fmt.Println(&a)  // 0xc00001c060

b := a
fmt.Println(&b)  // 0xc00001c068

b = 101     //修改b的值,此时不会影响a
fmt.Println(a) // 10
fmt.Println(b) // 101

fmt.Println(&a)  // 0xc00001c060
fmt.Println(&b)  // 0xc00001c068

例2:
var c = [3]int{1, 2, 3}
fmt.Printf("%p\n", &c) // 0xc42000a180

d := c
fmt.Printf("%p\n", &d) // 0xc42000a1a0

d[1] = 100
fmt.Println(c) // [1 2 3]
fmt.Println(d) // [1 100 3]

fmt.Printf("%p\n", &c) // 0xc42000a180
fmt.Printf("%p\n", &d) // 0xc42000a1a0

四 引用类的特点

1、赋值操作前,需要手动申请好内存,如果不,那么初始值为nil,nil代表空、没有申请内存,无法被直接赋值

var a []int

fmt.Printf("%p\n", &a) // 打印变量本身的值,输出地址为0xc00000c060
fmt.Printf("%p\n", a) // 打印首元素的地址,输出地址为0x0,代表为空
fmt.Printf("%v\n", a) // []

fmt.Println(a == nil) // true
a[0] = 666  // 错

// ps: 把一个完整的切片赋值给未开辟空间的切片当然是可以的
var b []int  // 未开辟空间的切片
var v = []int{11, 22, 33}
b = v
fmt.Println(b) // [11 22 33]

引用类型存在的意义在于引用了一个已经存在的内存空间,所以对于引用类型我们必须为其申请好内存才可以使用,有两种方式:

  • (1)调用make()函开辟空间,举例如下
// 例1:make用在切片类型
var a []int = make([]int, 3, 3) // 开辟内存空间,指向一个底层数组
fmt.Printf("%p\n", &a) // 打印变量本身的值,输出地址为0xc00000c060
fmt.Printf("%p\n", a) // 打印首元素的地址,输出地址为0xc000018140,代表不为空、有值了
fmt.Printf("%v\n", a) // [0 0 0]

fmt.Println(a == nil)           // false
a[0] = 666  // 可以赋值

// 例2:make用在map类型
var b map[string]int = make(map[string]int)
//var b map[string]int
fmt.Println(b == nil)
b["aaa"] = 666

fmt.Println(b["aaa"])
  • (2)一步到位式,即赋一个完整的值,我们之前用的都是这种方式,该方式等同于make完毕之后再塞值,如下所示
// 1、声明并开辟空间初始化
var names []string = make([]string,3,3)
// 2、往空间里塞进值
names[0] = "egon"
names[1] = "bigegon"
names[2] = "smallegon"

// 一步到位式,等同于上面步骤1和2
var names []string = []string{"egon", "bigegon", "smallegon"}

2、原值拷贝为一个新值,改原值,新值受影响

详解:
引用类型的变量赋值操作,如 j = i ,当然也是在内存中将 i 的值拷贝给了j,但是因为引用类型的变量直接存放的就是一个内存地址(这个地址指向的空间存的才是值),即i与j都是同一个地址。所以通过i或j修改对应内存地址空间中的值,另外一个也会修改。

例1
var a = []int{1,2,3,4,5}
b := a

// 打印两个变量的地址,肯定是不一样的
fmt.Printf("%p\n",&a)  // 0xc00000c060
fmt.Printf("%p\n",&b)  // 0xc00000c080

// 打印首元素地址,都一样,指向的就是同一个底层数组
fmt.Printf("%p\n",a)  // 0xc0000160c0
fmt.Printf("%p\n",b)  // 0xc0000160c0

// 改b,会影响a,反之亦然,因为改的都是同一个底层数组
b[1] = 666
fmt.Println(a)  // [1 666 3 4 5]

ps:a,b底层数组是一样的,即操作的都是同一个底层数组,但是上层切片不同,所以内存地址不一样。

copy操作示例

var a = []int{1, 2, 3, 4, 5}
c := make([]int, 5, 5)
copy(c, a) //将切片a拷贝到c,底层发生的事情是:把a底层数组拷贝一份,是创建了新的底层数组,给c指向的是这个新的数组

// 打印两个变量的地址,肯定是不一样的
fmt.Printf("%p\n", &a) // 0xc0000a6020
fmt.Printf("%p\n", &c) // 0xc0000a6040

// 打印首元素地址,也不一样,copy机制,让c与a指向了不同的底层数组
fmt.Printf("%p\n", a) // 0xc0000aa030
fmt.Printf("%p\n", c) // 0xc0000aa060

// 改c,不会影响a
c[1] = 20
fmt.Println(a) // [1 2 3 4 5]

指针操作示例

var a = []int{1, 2, 3, 4, 5}
d := &a     //将a的内存地址赋值给d,取值用*d

// 打印两个变量的地址,肯定是不一样的
fmt.Printf("%p\n", &a) // 0xc00000c060
fmt.Printf("%p\n", &d) // 0xc00000e028

// 变量d内存空间中存放的就是变量a的内存地址
fmt.Printf("%p\n", d) // 0xc00000c060

// 改d,影响a,反之亦然
(*d)[1] = 666
fmt.Println(a)  // [1 666 3 4 5]

a[1] = 777
fmt.Println(*d)  // [1 777 3 4 5]

关于函数

函数的参数传递机制是将实参的值拷贝一份给形参.只不过这个实参的值有可能是地址, 有可能是数据.

所以, 函数传参的传递其实本质全都是值传递,只不过该值有可能是数据(通常被简称为”值传递“),也有可能是地址(通常被简称为”引用传递“).

上一篇
下一篇
Copyright © 2022 Egon的技术星球 egonlin.com 版权所有 帮助IT小伙伴学到真正的技术