但这里有一个问题,如果变量的值变了,他的指针会变么?
我们知道,如果一个变量是指针类型的,那么他可以存储指针类型的值,比如 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))
微信公众号:刘思宁