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

文章讨论了Golang中指针的基本概念,指出变量的值改变不会改变其内存地址,但指针所指向的值会变。对比C语言,Golang可以直接新建struct指针且能安全返回局部变量的指针,而C中则可能导致useafterfree问题。此外,文章还提到了Go中不常见指针运算,但可通过unsafe包进行转换和计算。
摘要由CSDN通过智能技术生成

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

在 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 协议首部的代码,就用到了指针类型转换。


1.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. 2.除非发生 moving GC(移动内存地址式的垃圾回收,比如内存回收的复制算法)等底层程序,不然变量的内存地址不变 3.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. 4.具体消失的过程和原理待查,可能是被垃圾回收了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值