指针的基本意义是存储某些值所在的内存地址。
在 Golang 中,虽然不是所有的值都可以取出地址(尽管他们也存储在内存中,比如 const),但所有的变量必然可以取出地址。
变量,就是一块内存区域中存储的值[1]。不仅我们熟悉的 var x int
中的 x
是变量,一个比较复杂的表达式也可以表示一个变量,比如sliceA[0]
,mapB["key"]
,以及structC.FieldD
。也就是说,他们都可以有自己的指针。
但这里有一个问题,如果变量的值变了,他的指针会变么?
分析一下这个问题,指针的值变不变,只会跟变量的地址有关系,如果变量的地址没有变,那么指针是不会变的。所以,这个问题转化为了,改变变量的值,会改变变量的内存地址么?答案是,没有改变[2]。
我们知道,如果一个变量是指针类型的,那么他可以存储指针类型的值,比如 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;
}
那么,这个声明出来的局部变量,是一种自动变量(automatic variable[3]),原方法,也就是 init() 方法,结束后,这些自动变量就“消失”了[4]。
对于直接声明指针的版本,我们做如下实验:
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
打印出来的指针的值都是一样的(也就是地址都是一样的),但是结构体成员的值却很奇怪。具体来说就是重复访问同一个地址上的值,得到的结果竟然是不一样的。这里的具体原因和程序的调用栈结构有关,但我们这里想说明的是:
在一个方法返回后,他的局部变量已经消失,虽然内存地址还在,但最好不要再使用这个内存地址!如果访问一个已经消失了的自动变量的地址,可能会有很严重的 bug,因为相关地址上的值可能已经被其他代码改变! —— 这类问题通常被称为 use after free。
如果在一个 C 方法内部生成一个指向某个结构体的指针,可以用 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 程序中,好像确实没见过指针运算(嗯,我知道你想写不一般的程序)。
但实际上,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))
如果你没有遇到过需要转换指针类型的需求,可以看看这个项目(端口扫描工具),其中构建 IP 协议首部的代码,就用到了指针类型转换。
微信公众号:刘思宁
-
A variable is a piece of storage containing a value. -- Donovan, Alan A. A.. The Go Programming Language (Addison-Wesley Professional Computing Series) (p. 32). Pearson Education. Kindle Edition. ↩
-
除非发生 moving GC(移动内存地址式的垃圾回收,比如内存回收的复制算法)等底层程序,不然变量的内存地址不变 ↩
-
Each local variable in a function comes into existence only when the function is called, and disappears when the function is exited. This is why such variables are usually known as automatic variables, following terminology in other languages. -- Kernighan, Brian W.. C Programming Language (p. 31). Pearson Education. Kindle Edition. ↩
-
具体消失的过程和原理待查,可能是被垃圾回收了 ↩
网友评论