[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]
关于函数
函数的参数传递机制是将实参的值拷贝一份给形参.只不过这个实参的值有可能是地址, 有可能是数据.
所以, 函数传参的传递其实本质全都是值传递,只不过该值有可能是数据(通常被简称为”值传递“),也有可能是地址(通常被简称为”引用传递“).