指针的含义和相关运算符
变量是存储值的地方,而指针的值就是变量的地址。不是所有值都有地址,但是所有变量都有地址。通过使用指针,就可以在无需知道变量名字的情况下,间接读取/更新变量的值。
在C语言和Go中,有两个特别重要又非常容易搞混的相关运算符,一个是&,一个是*
取址运算符&
- 格式:&变量名
- 含义:取出存放变量的地址
间接运算符*
- 格式:*指针名
- 含义:取出存放在此地址中的值
举个例子:1
2
3
4
5x := 1
p := &x // p是整形指针,指向x
fmt.Println(*p) // "1"
*p = 2 // 等价于 x = 2
fmt.Println(x) // 结果 "2"
在这里,C/C++和Go有一点差异,C/C++是可以对指针变量进行运算的,而Go是不支持这种操作的。例如:1
2
3
4
5
6
7
8
9
10
11
12
13
14int main()
{
int a = 10, *pa = &a;
char c = '@', *pc = &c;
pa++; // 地址值+4,因为int占4个字节
pc++; // 地址值+1,因为char占一个字节
}
其实,也正是因为C++的指针计算功能过于强大,导致在C++中支持GC变成一个很困难的工作,假如C++支持垃圾收集,下面的代码在运行时将会变成一个严峻的考验:
```c++
int* p = new int;
p += 10; // 指针发生偏移,因此那块内存不再被引用
// 这里可能发生针对那块内存的垃圾收集
p -= 10; // 又偏移回原来位置
*p = 2; // 如果有垃圾收集,这里就无法保证正常运行1
2
3
4
5
6## 关于空指针
在Go语言里,指针类型的零值是nil,相当于C语言里的NULL和C++11里的nullptr,说到这里,不妨顺带谈一谈C/C++里的空指针。
在C语言里,我们使用NULL来表示空指针,如下:
```c
int *i = NULL;
foo_t *f = NULL;
其实在C语言中,NULL通常被定义为如下:1
也就是说,C语言里的NULL是一个void*类型的指针,然后将void *赋值给int*和foo_t*类型指针时,隐式转换成了相应类型(注意:GO无隐式转换)。而如果换一个C++编译器来编译这是要出错的,因为在C++里,void*是不能隐式转换成其他类型指针的,所以通常情况下,编译器头文件会这样定义NULL:1
2
3
4
5
也就是说,假如是C++编译环境,就将NULL定义为0,这就带来了一个问题,“二义性”,代码如下:1
2
3
4
5
6
7
8
9
10
11
12void test(void* p)
{
cout << "pointer" << endl;
}
void test(int num)
{
cout << "number" << endl;
}
int main()
{
test(NULL);
}
编译会报错,为什么呢,因为NULL=0,test(NULL)可以匹配上面两个函数,所以有二义性。解决的方法,一是尽量用0代替NULL,这样写的时候自己就会发现问题,二就是C++11带来的解决方案nullptr,例如上面的代码,改成test(nullptr)则不会有问题。
Go中指针与函数/方法结合
Go语言中没有类这一概念,但是可以给结构体定义方法。
在Go中,方法是一种带有接收者参数的特殊的函数。方法接收者在它的参数列表内,位于func关键字和方法名之间,例如下面这个代码,Abs方法拥有一个类型为Vertex的接收者:1
2
3func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
注意:接收者的类型定义和方法声明必须在同一包内,不能以其他包里定义的类型为接收者声明方法,比如下面这个就是非法的:1
2
3
4
5
6func (f int) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
方法接收者可以是值,也可以是指针。当指针作为接收者的时候,该方法就可以修改指针指向的值。当值作为接收者的时候,方法会对原始值的副本进行操作而不修改原始值,取副本是需要每次调用方法的时候进行复制的,如果值的类型是大型结构体,那么这样做的效率比较低。由是观之,使用指针作为接收者有两点好处:
- 方法能够修改其接收者指向的值
- 避免在每次调用方法时复制该值,较为高效
最后,关于指针和方法在使用上还有一点要注意的。
- 参数是指针的函数必须接受一个指针
1
2
3
4
5
6
7
8func ScaleFunc(v *Vertex, f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
var v Vertex
ScaleFunc(v, 5) // 编译错误!
ScaleFunc(&v, 5) // OK - 以指针为接收者的方法被调用时,接收者既能为值也能为指针,此时用值用指针效果一样
1
2
3
4
5
6
7
8func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
var v Vertex
v.Scale(5) // OK,v改变,因为Go会将语句 v.Scale(5) 解释为 (&v).Scale(5)
p := &v
p.Scale(10) // OK,v改变 - 参数是值的函数必须接受一个值
1
2
3
4
5
6
7func AbsFunc(v Vertex) float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
var v Vertex
fmt.Println(AbsFunc(v)) // OK
fmt.Println(AbsFunc(&v)) // 编译错误 - 以值为接收者的方法被调用时,接收者既能为值又能为指针,此时用值用指针效果一样
1
2
3
4
5
6
7
8func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
var v Vertex
fmt.Println(v.Abs()) // OK
p := &v
fmt.Println(p.Abs()) // OK,这种情况下,方法调用 p.Abs() 会被解释为 (*p).Abs()