关于C和Go的指针问题

指针的含义和相关运算符

变量是存储值的地方,而指针的值就是变量的地址。不是所有值都有地址,但是所有变量都有地址。通过使用指针,就可以在无需知道变量名字的情况下,间接读取/更新变量的值。

在C语言和Go中,有两个特别重要又非常容易搞混的相关运算符,一个是&,一个是*

取址运算符&

  • 格式:&变量名
  • 含义:取出存放变量的地址

间接运算符*

  • 格式:*指针名
  • 含义:取出存放在此地址中的值

举个例子:

1
2
3
4
5
x := 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
14
int 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
#define NULL ((void *)0)

也就是说,C语言里的NULL是一个void*类型的指针,然后将void *赋值给int*和foo_t*类型指针时,隐式转换成了相应类型(注意:GO无隐式转换)。而如果换一个C++编译器来编译这是要出错的,因为在C++里,void*是不能隐式转换成其他类型指针的,所以通常情况下,编译器头文件会这样定义NULL:
1
2
3
4
5
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif

也就是说,假如是C++编译环境,就将NULL定义为0,这就带来了一个问题,“二义性”,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
void 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
3
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

注意:接收者的类型定义和方法声明必须在同一包内,不能以其他包里定义的类型为接收者声明方法,比如下面这个就是非法的:
1
2
3
4
5
6
func (f int) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}

方法接收者可以是,也可以是指针。当指针作为接收者的时候,该方法就可以修改指针指向的值。当值作为接收者的时候,方法会对原始值的副本进行操作而不修改原始值,取副本是需要每次调用方法的时候进行复制的,如果值的类型是大型结构体,那么这样做的效率比较低。由是观之,使用指针作为接收者有两点好处

  • 方法能够修改其接收者指向的值
  • 避免在每次调用方法时复制该值,较为高效

最后,关于指针和方法在使用上还有一点要注意的。

  • 参数是指针的函数必须接受一个指针
    1
    2
    3
    4
    5
    6
    7
    8
    func 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
    8
    func (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
    7
    func 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
    8
    func (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()