一文带你速通编程语言中的指针
# 写在文章开头
关于go语言的系列文章更新了有一段时间了,从阅读量来看大部分接触go语言的读者都是Java开发,因为Java这门语言没有指针这一说法,所以笔者专门整理了一篇文章带读者快速了解一下指针的概念。

我是 SharkChili ,Java 开发者,Java Guide 开源项目维护者。欢迎关注我的公众号:写代码的SharkChili,也欢迎您了解我的开源项目 mini-redis:https://github.com/shark-ctrl/mini-redis (opens new window)。
为方便与读者交流,现已创建读者群。关注上方公众号获取我的联系方式,添加时备注加群即可加入。
# go语言指针详解
# 简介
指针即指向变量的地址,在计算机宝贵的内存中存在成千上万的变量,对于某些可复用的变量,我们可以通过指针进行操作,避免拷贝的开销,从而提升系统的执行效率。

# 指针的基本概念
这里笔者以C语言的概念简单科普一下指针的概念:
- 声明一个指针:需要指定其指向的数据类型。例如,
int *p;表示p是一个指向整型数据的指针。 取地址运算符 (&):取地址运算符用于获取某个变量的内存地址。例如,如果有一个整型变量int a = 10;,那么&a就是 a 的内存地址。解引用运算符 (*):解引用运算符用于访问指针所指向的内存中的值。如果int *p = &a;,那么 在非语法赋值时使用*p本质就是解p引用,即获取p指向地址的值。
对应的我们也给出上述说明的内存图解,如下所示,本质上就是内存为0x1000的内存空间存储了一个整数4,然后0x1004这块内存空间存储了0x1000这个地址:

这里我们也给出go语言对应的示例,语法有些区别但是整体理念是一致的:
//由go语言类型推导出整形
num := 24
//通过*int 声明一个整形指针,指向4字节的地址
var px *int
px = &num
//输出num数值
fmt.Println("num:", num)
//输出num地址
fmt.Println("px:", px)
//解px引用获取px指向的值,即打印num的值
fmt.Println("*px:", *px)
2
3
4
5
6
7
8
9
10
11
# 声明和解引用取值
我们通过短变量声明了一个num变量,通过Println打印可知:
- 加上
取地址运算符&的输出结果就是num的内存地址,而这种标识地址的变量在编程语言中我们都称之为指针:
package main
import "fmt"
func main() {
num := 6
//打印变量的值
fmt.Println(num)
//打印变量的地址
fmt.Println(&num)
}
2
3
4
5
6
7
8
9
10
11
输出结果:
6
0xc0000a6058
2
3
我们继续说说指针类型,我们基于上述例子手动创建一个指针,语言很简单,即* type,例如下面这段代码,即声明一个整型的指针变量,通过取地址运算符将num的地址赋值给p,最后我们通过reflect的TypeOf查看输出结果。
import (
"fmt"
"reflect"
)
func main() {
num := 6
//打印变量的值
fmt.Println(num)
//打印变量的地址(指向内存地址的值称为指针)
fmt.Println(&num)
//声明一个指向int的指针
var p *int
//分配num的内存地址
p = &num
fmt.Println("&num的类型:", reflect.TypeOf(&num))
fmt.Println("p的类型:", reflect.TypeOf(p))
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
可以看到取地址运算符和p指针得到的类型都是整型指针类型:
6
0xc0000a6058
&num的类型: *int
p的类型: *int
2
3
4
既然我们拿到变量的地址空间,那么我们就可以打印指针所指向的值,如下所示,通过指针访问元素值的语法如下,即* variable意味获取p处的值(*在使用的场景下意为解引用运算符):
fmt.Println("p1处的值", *p1)
2
输出结果:
p1处的值 6
同理我们若要修改指针的值,也可以通过*号定位到p处的值,如上文所说,*符号在这里可以理解为解引用运算符,可以直接访问到指针所指向的内存的值,然后进行修改通过赋值运算值直接完成修改:
//通过解引用获取p1地址,修改p1指向的地址的值为18,仅改变值,地址不变
*p1 = 18
fmt.Println("p1处的值", *p1)
fmt.Println("p1处的地址", p1)
fmt.Println("num的地址", &num)
2
3
4
5
输出结果:
p1处的值 18
p1处的地址 0xc0000a6058
num的地址 0xc0000a6058
2
3
4
可以看到无论是num还是p的地址都是不变的,我们改变的仅仅是地址空间的值:

# 函数指针
函数指针就是返回指针的函数,通过对这个函数的调用我们可以得到一个变量的指针,这意味着每个调用该函数得到的变量就是当前函数栈帧上的变量的指针。
var num = 6
func main() {
pointer := createPointer()
fmt.Println("pointer值", pointer)
fmt.Println("pointer处的值", *pointer)
}
// createPointer 返回int类型的指针
func createPointer() *int {
fmt.Println("num的地址:", &num)
return &num
}
2
3
4
5
6
7
8
9
10
11
12
13
输出结果如下:
num的地址: 0x91e340
pointer值 0x91e340
pointer处的值 6
2
3
4
createPointer输出的num地址和main方法调用createPointer得到的变量的地址是一致的,这意味我们对于某些场景我们可以通过函数调用的方式或者公用变量的指针从而全局进行操作:

# 基于指针优化普通函数实践
我们现在有这样一段代码,我们希望给定一个布尔参数,通过函数negate设置为取反后的值,例如我们变量a为false,希望调用negate后a的值会变为true,原始代码如下:
func main() {
truth := true
negate(truth)
fmt.Println(truth)
lies := false
negate(lies)
fmt.Println(lies)
}
func negate(b bool) bool {
return !b
}
2
3
4
5
6
7
8
9
10
11
12
13
14
输出结果可以看到,结果是不变的:
true
false
2
3
上述方法传入的是变量的值,即我们传入的参数都会直接拷贝到形参变量上,这使得形参无论如何修改都不会影响实参。

所以我改造时要按照如下步骤执行:
- 让函数得到变量指针。
- 通过指针访问实参地址。
- 取反并赋值。

最终代码如下:
import "fmt"
func main() {
truth := true
negate(&truth)
fmt.Println(truth)
lies := false
negate(&lies)
fmt.Println(lies)
}
func negate(b *bool) {
*b = !(*b)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
通过将指针传入函数,通过地址定位到变量的地址从而完成变量修改,最终我们完成了变量修改操作:
false
true
2
# 关于指针的更进一步的理解
# 为什么需要指针
关于指针的的用途我们可以从编程语言的本质来说,我们都知道本着复用的原则,编程语言常会将通过的操作封装为函数,例如修改结构体person的姓名。与之对应我们就要将person结构体传入函数作用域中,这其中就涉及到一个拷贝的开销,因为person是一个较大的结构体:
type person struct {
name string
age int
sex string
//......
}
2
3
4
5
6
所以将实参传入函数时就涉及到一个大结构体拷贝到实参中的开销,对此我们就可以通过指针将person地址传入(也就4个字节),让函数内部通过指针定位到结构体完成修改:
func updatePersonName(p *person) {
(*p).name = "Tom"
}
2
3
4
对应另外一个原因是编程语言中的内存分配,再进行内存分配时涉及静态分配和动态分配:
- 静态分配:即在栈中创建的变量,此类变量在编译时就可以确定大小,例如上述函数内的
num:=24 - 动态分配:也就是我们上述的结构体之类需要通过new等操作创建的变量,因为申请越界内存的指针,由此就需要通过指针进行管理。
# 小结
以上便是笔者对于go语言指针的扫盲实践,本文笔者从指针的基本声明、实用、最佳实践多个角度的案例演示了指针的常见操作,希望对你有帮助。
我是 SharkChili ,Java 开发者,Java Guide 开源项目维护者。欢迎关注我的公众号:写代码的SharkChili,也欢迎您了解我的开源项目 mini-redis:https://github.com/shark-ctrl/mini-redis (opens new window)。
为方便与读者交流,现已创建读者群。关注上方公众号获取我的联系方式,添加时备注加群即可加入。
# 参考
Head First Go语言程序设计:https://book.douban.com/subject/35237045/ (opens new window)
《go语言高级编程》