1.数组指针
(1)数组指针与指针数组
这俩概念原本在c语言中就是一个绕口令般的存在,尽管从类型角度来看两者并没什么相似的地方。
但是在go语言中对这两个类型的设定做出了一些不同的规定。首先交代一下基本概念:
数组指针:指的是一个指针,只不过这个指针指向了一个数组
eg:
var arr [5]int = [5]int{1,2,3,4,5};
var p *[5]int = &arr;
fmt.Println(p);//0xc00006a030
//此时p称为是数组arr的指针
//此时如果通过p访问数组元素是 (*p)[2],结果是3
指针数组:指的是一个数组,这个数组里面装满了指针。
eg:
var arr1 [5]int = [5]int{1,2,3,4,5};
var arr2 [5]int = [5]int{6,7,8,9,0};
var p1 *[5]int = &arr1;
var p2 *[5]int = &arr2;
var pArr [2]*[5]int = [2]*[5]int{p1,p2};
fmt.Println(pArr);//[0xc00006a030, 0xc00006a060]
//此时pArr称为是指针数组
//此时如果通过pArr访问数组元素是 (*pArr[0])[2],结果是3
(2)地址相同、类型不同问题
在go语言中,内存地址相同并不意味着其表示的数据或者数据类型就完全相同。
eg:
var arr [5]int = [5]int{1,2,3,4,5};
p1,p2 := &arr,&arr[0];
fmt.Println(p1); //&[1,2,3,4,5] 整个数组的内存地址
fmt.Println(p2); //0xc0000180c0 数组第一个元素的内存地址
fmt.Printf("%T",p1); //*[5]int 数组指针
fmt.Printf("%T",p2); //*int 指针
(3)*号可以不写问题
在go语言中通过指针去访问指针所对应的地址处的值时,*允许不写。
而这个规定又会催生一个由于go运算符优先级问题而催生的指针访问问题。
eg:
var arr [5]int = [5]int{1,2,3,4,5};
p := &arr;
fmt.Println(*p[0]);
乍一看这段代码并无任何问题。
因为p是数组指针,*p就是数组本身,而*p[0]自然就是数组的第一个元素
但是事与愿违,代码的运行结果是一个错误!
这是因为在go语言中*寻址运算符和[]中括号运算符的优先级是不同的!
[]中括号是初等运算符
*寻址运算符是单目运算符
初等运算符的优先级是大于单目运算符的,因此先参与计算的是p[0];
p[0]其实就是数组的第一个元素,就是数字1
数字1必然是int类型,而不是一个地址,因此针对数字1使用*寻址运算符自然也就发生了错误。
解决问题的办法很简单,就是添加一个小括号就可以了。
即:
(*p)[0]
不过因为*在go语言中,建立了 p:=&arr 这种类似地址关系后,*允许不写。
所以,访问时候可以直接携程p[0]。事实上在工作开发过程中,这种写法反而更加常见。
ps:
仅对访问下标时,*寻址运算符允许不写!
2.切片指针
(1)切片名
前文中提过,go语言中的切片与传统js语言中的数组结构类似。
因此在go语言中切片名本身就是一个地址。
因此通过切片名加下标的方式访问切片元素原本就是指针访问的一种表现。
(因为go语言中*寻址运算符可以不写)
eg:
var slice []int = []int{1,2,3,4,5};
fmt.Printf("%p\n", slice);//0xc000012346;
fmt.Println(slice[0]);
(2)二重指针
因为切片名本身已经是一个指针了,如果再对切片名取地址,
那么得到的就是一个地址的地址、指针的指针,即二重地址(二级地址、二重指针、二级指针)
eg:
p := &slice;
fmt.Printf("%p\n", p);//0xc000012446;
面对一重指针时*寻址运算符在go语言中是省略不写的,
但是在二重与二重以上的指针参与运算的时候,*寻址运算符则是一个必不可少的角色。
eg:
fmt.Println(p); //0xc0000aa362 相当于传统c中的*p
fmt.Println(*p); //0xc0000ac141 相当于传统c中的**p
fmt.Println(p[0]); //违法 相当于传统c中的(*p)[0]
fmt.Println((*p)[0]); //1 相当于传统c中的(**p)[0]
(3)切片指针作为函数参数
切片指针作为函数参数传入函数内部时,不论是修改还是追加都能保证函数内的操作影响到函数外部
而不像切片作为函数参数传入函数内部,只有修改会影响外部,而追加则无法对外部造成影响。
eg:
func sliceParam(tempSlice []int){
tempSlice = append(tempSlice, 4,5,6);
}
func sliceParamPointer(tempSlicePointer *[]int){
*tempSlicePointer = append(*tempSlicePointer,4,5,6);
}
func main() {
slice := []int{1,2,3};
sliceParam(slice);
fmt.Println(slice);//[1,2,3]
sliceParamPointer(&slice);
fmt.Println(slice);//[1,2,3,4,5,6]
}
究其原因也很明朗,其实就是函数传参永远都是值传递!
1)当切片作为参数的时候,一旦对tempSlice追加数据。那么tempSlice变量的值,
即所保存的内存地址就会变化。
换句话说此时追加数据操作是针对tempSlice变量变化之后的值对应的内存地址
但是由于值传递,slice只是把之前的切片地址作为值传到了函数内的tempSlice上
因此tempSlice的值做出变更不会对slice变量有任何影响,
因此切片作为参数的时候,一旦追加,就无法对外部造成影响。
2)当切片指针作为参数的时候,必须清楚的是,仍然是值传递!!!
只不过传到tempSlicePointer上的是slice的指针
此时通过*tempSlicePointer进行追加操作时,同样*tempSlicePointer的值也会发生变化
关键的来了
那就是tempSlicePointer因为是二重指针,
所以*tempSlicePointer表示的是原slice切片的地址
所以即便*tempSlicePointer的值发生怎样的变化,都相当于原slice切片的地址发生变化
而既然函数内通过*tempSlicePointer对切片的追加,都相当于对原slice切片进行追加
所以,通过切片指针在函数内对切片的追加操作就能够对函数外的切片产生影响。
ps:
所以到目前为止,
数组传参只能改不能加且无法影响外部,
切片传参改能影响外部,加无法影响外部
字典传参能改能加且均会影响外部
切片指针能改能加且均会影响外部
(4)new为切片指针分配内存空间
众所周知,对切片进行内容追加的时候切片必须被分配内存才能执行。
eg:
var s []int;
s = make([]int,0);//这一句写不写无所谓
s = append(s, 1,2,3);
fmt.Println(s);
不过go语言总切片比较特殊,是可以在内容追加后进行“内存变更”的
换句话说,哪怕不使用make对s切片进行长度的分配也无所谓
但是对于切片指针来说则是完全不同。
eg:
var p *[]int;
p = new([]int);//这一句必须写
*p = append(*p,1,2,3);
fmt.Println(*p);
当使用切片指针的时候,go语言规定指针必须被分配内存才能使用,
否则空指针是没办法进行寻址赋值操作的。go语言使用new来对指针进行分配内存空间,
p=new([]int)表示让p指向一个int类型切片数据的地址。
如果没有这一句而直接进行append,那就表示对一个空指针所指向的内容进行append
这是一定没有办法执行的。
3.结构体指针
结构体本身是使用type定义的一种数据类型,存放在内存的数据区。
而结构体指针是指向了结构体内存地址的指针。
可以通过结构体指针间接访问结构体中的成员。
eg:
type UserInfo struct {
userName string
userAge int
}
func main() {
var xiaoMingInfo UserInfo = UserInfo{"小明",18};
fmt.Println(xiaoMingInfo.userName, xiaoMingInfo.userAge);//小明 18
}
ps:结构体定义为了何种数据,数据就存放在哪个位置。
例如结构体中存了数组,就存放在栈区
如果结构体重存放了切片,就存放在堆区
ps:通过结构体指针间接访问结构体成员的时候,允许不写*寻址运算符。
eg:
var xmp *UserInfo = &xiaoMingInfo;
fmt.Println(xmp.userName, xmp.userAge);//小明 18
ps:结合数组和切片,还能够推论出【结构体数组指针】和【结构体切片指针】的内容
只要明确
【结构体数组指针】是指向结构体数组的指针,*指针代表了结构体数组
【结构体切片指针】是指向结构体切片的指针,*指针代表了结构体切片。
再注意符号和名称就没什么问题。