指针的基本意义是存储某些值所在的内存地址

在 Golang 中,虽然不是所有的值都可以取出地址(尽管他们也存储在内存中,比如 const),但所有的变量必然可以取出地址。

var x intxsliceA[0]mapB["key"]structC.FieldD

但这里有一个问题,如果变量的值变了,他的指针会变么?

分析一下这个问题,指针的值变不变,只会跟变量的地址有关系,如果变量的地址没有变,那么指针是不会变的。所以,这个问题转化为了,改变变量的值,会改变变量的内存地址么?答案是,没有改变。

var ptr *int&ptrvar a inta&a

代码举例如下:

b := 1
fmt.Printf("%p\n", &b) // 0x416028
b = 2
fmt.Printf("%p\n", &b) // 0x416028
c := &b
fmt.Printf("%p\n", c) // 0x416028
b

但这里还有一个问题,如果变量的值变了,他的指针所指向的值(或者说用指针取出的值)会变么?

答案显然是会变的。因为变量的指针还是指向同一个内存地址,但是那个地址上的值已经变了。举例说明就是:

type A struct {
    Value int
}
a := A{Value: 1}
fmt.Printf("a-ptr: %p, value-ptr: %p, value: %d\n", &a, &a.Value, (&a).Value)
// a-ptr: 0x41602c, value-ptr: 0x41602c, value: 1
a = A{Value: 2}
fmt.Printf("a-ptr: %p, value-ptr: %p, value: %d\n", &a, &a.Value, (&a).Value)
// a-ptr: 0x41602c, value-ptr: 0x41602c, value: 2
a

Golang 与 C 的不同

相比于 C,Golang 中的指针有 2 点不同(或者说,有一些优化):

1. Go 可以直接新建 struct 的指针

ptr := &A{Value: 1}
typedef struct {
    int value;
} A;
A *ptr1; // 无法给 ptr 所指的值赋值
A *ptr2 = &A{1}; // 没有这样的语法
A a = {1}; // 再通过 &a 可以得到指针

如果说这个区别只是语法上的表象,另外一个区别可能就是事关 bug 的区别了。

2. Go 中可以安全地返回局部变量的指针

在上面的 C 代码举例中,我们确实可以声明一些变量,但如果这些声明是在一个方法内完成的,比如:

A *init()
{
    A *ptr;
    return ptr;
}

或者

A *init()
{
    A a;
    return &a;
}

那么,这个声明出来的局部变量,是一种自动变量(automatic variable),原方法,也就是 init() 方法,结束后,这些自动变量就“消失”了。

对于直接声明指针的版本,我们做如下实验:

A *init(int value)
{
    A *ptr;
    printf("1. inside - ptr: %x, value: %d\n", ptr, ptr->value);
    return ptr;
}
int main()
{
    A *ptr = init(1);
    printf("2. after return: ptr: %x, value: %d\n", ptr, ptr->value);
}

得到的结果可能类似于是:

1. inside - ptr: 1ad2f248, value: 25
2. after return - ptr: 1ad2f248, value: 25

结果是不是出乎意料(在不同机器上,结果会稍有不同)?我们确实声明了一个指针类型的变量,但是这个变量的值,也就是实际存储的内存地址,指向的不一定是一个结构体A,而且很可能是完全不相干的地址。这就给程序留下了安全性的隐患,尤其是意外被访问的地址中有一些重要数据的话。

当然,这个地址也可能是无效的,如果你想要改变这个地址中的值,比如:

 ptr->value = 2;
bus error

同理,对于一个先声明结构体的值,再返回指针的方法,也会有意向不到的问题。我们做如下实验;

A *init(int value)
{
    A a = {value};
    printf("1. inside - ptr: %x, value; %d\n", &a, (&a)->value);
    return &a;
}

int main() {
    A *ptr = init(1);
    printf("2. after return - ptr: %x, value: %d\n", ptr, ptr->value);
    printf("3. after return - ptr: %x, value: %d\n", ptr, ptr->value);
    A *ptr2 = init(2)
    printf("4. after return - ptr: %x, value: %d\n", ptr, ptr->value); // Watch here!!!
}

你会发现结果类似于这样(如果是 macOS,结果会更相近):

1. inside - ptr: e43de2d8, value: 1
2. after return - ptr: e43de2d8, value: 1
3. after return - ptr: e43de2d8, value: 0
1. inside - ptr: e43de2d8, value: 2
4. after return - ptr: e43de2d8, value: 2

打印出来的指针的值都是一样的(也就是地址都是一样的),但是结构体成员的值却很奇怪。具体来说就是重复访问同一个地址上的值,得到的结果竟然是不一样的。这里的具体原因和程序的调用栈结构有关,但我们这里想说明的是:

在一个方法返回后,他的局部变量已经消失,虽然内存地址还在,但最好不要再使用这个内存地址!如果访问一个已经消失了的自动变量的地址,可能会有很严重的 bug,因为相关地址上的值可能已经被其他代码改变! —— 这类问题通常被称为 use after free。

malloc
A *ptr = (A *)malloc(sizeof(A));

然后,可以安全的返回这个指针。

相比之下,Golang 中的处理就简单多了,那部分内存并不会被回收:

func init(value int) *A {
    return &A{Value: 1}
}

所以,这段 go 代码是安全的。

指针运算

在很多 golang 程序中,虽然用到了指针,但是并不会对指针进行加减运算,这和 C 程序是很不一样的。Golang 的官方入门学习工具(go tour) 甚至说 Go 不支持指针算术。虽然实际上并不是这样的,但我在一般的 go 程序中,好像确实没见过指针运算(嗯,我知道你想写不一般的程序)。

unsafe.Pointeruintptruintptr

比如:

uintptr(unsafe.Pointer(&p)) + 1
&p
unsafe.Pointer(uintptr(unsafe.Pointer(&p) + 1))

因为 go 中是有垃圾回收机制的,如果某种 GC 挪动了目标值的内存地址,以整型来存储的指针数值,就成了无效的值。

+ 1++unsafe.Sizeof
unsafe.Pointer(uintptr(unsafe.Pointer(&p) + unsafe.Sizeof(p)))

最后,另外一种常用的指针操作是转换指针类型。这也可以利用 unsafe 包来实现:

var a int64 = 1
(*int8)(unsafe.Pointer(&a))

如果你没有遇到过需要转换指针类型的需求,可以看看这个项目(端口扫描工具),其中构建 IP 协议首部的代码,就用到了指针类型转换。

微信公众号:刘思宁