2016-02-15 03:06:34 +00:00
### 2.3.2. 指针
2015-12-09 07:45:11 +00:00
2016-02-15 03:06:34 +00:00
一个变量对应一个保存了变量对应类型值的内存空间。普通变量在声明语句创建时被绑定到一个变量名, 比如叫x的变量, 但是还有很多变量始终以表达式方式引入, 例如x[i]或x.f变量。所有这些表达式一般都是读取一个变量的值, 除非它们是出现在赋值语句的左边, 这种时候是给对应变量赋予一个新的值。
2015-12-09 07:45:11 +00:00
2016-02-15 03:06:34 +00:00
一个指针的值是另一个变量的地址。一个指针对应变量在内存中的存储位置。并不是每一个值都会有一个内存地址,但是对于每一个变量必然有对应的内存地址。通过指针,我们可以直接读或更新对应变量的值,而不需要知道该变量的名字(如果变量有名字的话)。
2015-12-09 07:45:11 +00:00
2016-02-15 03:06:34 +00:00
如果用“var x int”声明语句声明一个x变量, 那么& x表达式( 取x变量的内存地址) 将产生一个指向该整数变量的指针, 指针对应的数据类型是`*int`, 指针被称之为“指向int类型的指针”。如果指针名字为p, 那么可以说“p指针指向变量x”, 或者说“p指针保存了x变量的内存地址”。同时`*p`表达式对应p指针指向的变量的值。一般`*p`表达式读取指针指向的变量的值, 这里为int类型的值, 同时因为`*p`对应一个变量,所以该表达式也可以出现在赋值语句的左边,表示更新指针所指向的变量的值。
2015-12-09 07:45:11 +00:00
```Go
x := 1
p := & x // p, of type *int, points to x
fmt.Println(*p) // "1"
*p = 2 // equivalent to x = 2
fmt.Println(x) // "2"
```
2016-02-15 03:06:34 +00:00
对于聚合类型每个成员——比如结构体的每个字段、或者是数组的每个元素——也都是对应一个变量,因此可以被取地址。
2015-12-09 07:45:11 +00:00
2016-02-15 03:06:34 +00:00
变量有时候被称为可寻址的值。即使变量由表达式临时生成,那么表达式也必须能接受`& `取地址操作。
2015-12-09 07:45:11 +00:00
2016-08-07 13:59:29 +00:00
任何类型的指针的零值都是nil。如果p指向某个有效变量, 那么`p != nil`测试为真。指针之间也是可以进行相等测试的, 只有当它们指向同一个变量或全部是nil时才相等。
2015-12-09 07:45:11 +00:00
```Go
var x, y int
fmt.Println(& x == & x, & x == & y, & x == nil) // "true false false"
```
2016-02-15 03:06:34 +00:00
在Go语言中, 返回函数中局部变量的地址也是安全的。例如下面的代码, 调用f函数时创建局部变量v, 在局部变量地址被返回之后依然有效, 因为指针p依然引用这个变量。
2015-12-09 07:45:11 +00:00
```Go
var p = f()
func f() *int {
v := 1
return & v
}
```
2016-02-15 03:06:34 +00:00
每次调用f函数都将返回不同的结果:
2015-12-09 07:45:11 +00:00
```Go
fmt.Println(f() == f()) // "false"
```
2016-02-15 03:06:34 +00:00
因为指针包含了一个变量的地址, 因此如果将指针作为参数调用函数, 那将可以在函数中通过该指针来更新变量的值。例如下面这个例子就是通过指针来更新变量的值, 然后返回更新后的值, 可用在一个表达式中( 译注: 这是对C语言中`++v`操作的模拟, 这里只是为了说明指针的用法, incr函数模拟的做法并不推荐) :
2015-12-09 07:45:11 +00:00
```Go
func incr(p *int) int {
2016-02-15 03:06:34 +00:00
*p++ // 非常重要: 只是增加p指向的变量的值, 并不改变p指针! ! !
2015-12-09 07:45:11 +00:00
return *p
}
v := 1
incr(& v) // side effect: v is now 2
fmt.Println(incr(& v)) // "3" (and v is 3)
```
2017-08-24 14:25:47 +00:00
每次我们对一个变量取地址,或者复制指针,我们都是为原变量创建了新的别名。例如,`*p`就是变量v的别名。指针特别有价值的地方在于我们可以不用名字而访问一个变量, 但是这是一把双刃剑: 要找到一个变量的所有访问者并不容易, 我们必须知道变量全部的别名( 译注: 这是Go语言的垃圾回收器所做的工作) 。不仅仅是指针会创建别名, 很多其他引用类型也会创建别名, 例如slice、map和chan, 甚至结构体、数组和接口都会创建所引用变量的别名。
2015-12-09 07:45:11 +00:00
2016-02-15 03:06:34 +00:00
指针是实现标准库中flag包的关键技术, 它使用命令行参数来设置对应变量的值, 而这些对应命令行标志参数的变量可能会零散分布在整个程序中。为了说明这一点, 在早些的echo版本中, 就包含了两个可选的命令行参数: `-n`用于忽略行尾的换行符,`-s sep`用于指定分隔字符( 默认是空格) 。下面这是第四个版本, 对应包路径为gopl.io/ch2/echo4。
2015-12-09 07:45:11 +00:00
2016-01-20 15:16:19 +00:00
< u > < i > gopl.io/ch2/echo4< / i > < / u >
2015-12-09 07:45:11 +00:00
```Go
// Echo4 prints its command-line arguments.
package main
import (
"flag"
"fmt"
"strings"
)
var n = flag.Bool("n", false, "omit trailing newline")
var sep = flag.String("s", " ", "separator")
func main() {
flag.Parse()
fmt.Print(strings.Join(flag.Args(), *sep))
if !*n {
fmt.Println()
}
}
```
2017-08-24 14:25:47 +00:00
调用flag.Bool函数会创建一个新的对应布尔型标志参数的变量。它有三个属性: 第一个是命令行标志参数的名字“n”, 然后是该标志参数的默认值( 这里是false) , 最后是该标志参数对应的描述信息。如果用户在命令行输入了一个无效的标志参数, 或者输入`-h`或`-help`参数, 那么将打印所有标志参数的名字、默认值和描述信息。类似的, 调用flag.String函数将创建一个对应字符串类型的标志参数变量, 同样包含命令行标志参数对应的参数名、默认值、和描述信息。程序中的`sep`和`n`变量分别是指向对应命令行标志参数变量的指针,因此必须用`*sep`和`*n`形式的指针语法间接引用它们。
2015-12-09 07:45:11 +00:00
2017-08-24 14:25:47 +00:00
当程序运行时, 必须在使用标志参数对应的变量之前先调用flag.Parse函数, 用于更新每个标志参数对应变量的值( 之前是默认值) 。对于非标志参数的普通命令行参数可以通过调用flag.Args()函数来访问, 返回值对应一个字符串类型的slice。如果在flag.Parse函数解析命令行参数时遇到错误, 默认将打印相关的提示信息, 然后调用os.Exit(2)终止程序。
2015-12-09 07:45:11 +00:00
2016-02-15 03:06:34 +00:00
让我们运行一些echo测试用例:
2015-12-09 07:45:11 +00:00
```
$ go build gopl.io/ch2/echo4
$ ./echo4 a bc def
a bc def
$ ./echo4 -s / a bc def
a/bc/def
$ ./echo4 -n a bc def
a bc def$
$ ./echo4 -help
Usage of ./echo4:
-n omit trailing newline
-s string
separator (default " ")
```