搜索
您的当前位置:首页正文

Golang 指针:使用方法、特点 和 运算

来源:二三娱乐
题图来自 @unsplash

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

我们知道,如果一个变量是指针类型的,那么他可以存储指针类型的值,比如 var ptr *int 中的 ptr 可以存储指针类型的值。这个变量的值可以改变,从而只想不同的内存空间,但变化的只是这个变量的值。ptr 本身的内存空间是没有变的,也就是说 &ptr 一直是一个值(除非发生 moving GC)。同理,我们上面提到的问题就类似于问 var a int,如果改变了 a 的值,&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

可以看到,指针都是没有变的(因为 Value 字段是 struct A 的第一个字段,所以内存地址一样),虽然我们给变量 a 重新赋了值。

Golang 与 C 的不同

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

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

在 golang 中,我们可以通过ptr := &A{Value: 1},就得到了一个结构体 A 值的指针;但在 C 中就无法通过单独的赋值语句得到:

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;
}

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

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;

很可能会得到一些错误,比如在 macOS 上会得到 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

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

如果在一个 C 方法内部生成一个指向某个结构体的指针,可以用 malloc:

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

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

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

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

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

指针运算

但实际上,go 可以通过 unsafe.Pointer 来把指针转换为 uintptr 类型的数字,来实现指针运算。这里请注意,uintptr 是一种整数类型,而不是指针类型。

比如:

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

就得到了 &p 的下一个字节的位置。然而,根据 《Go Programming Language》 的提示,我们最好直接把这个计算得到的内存地址转换为指针类型:

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

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

同时也要注意,go 中对指针的 + 1,真的就只是指向了下一个字节,而 C 中 + 1 或者 ++ 考虑了数据类型的长度,会自动指向当前值结尾后的下一个字节(或者说,有可能就是下一个值的开始)。如果 go 中要想实现同样的效果,可以使用 unsafe.Sizeof 方法:

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

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

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

微信公众号:刘思宁


Top