Go语言实践[回顾]教程17--详解Go语言复合数据类型之指针 Pointer

Go语言实践[回顾]教程17--详解Go语言复合数据类型之指针 Pointer

  • 指针相关的概念
  • 指针变量的声明
  • 获取和修改指针指向的值
  • Go语言的指针禁止偏移运算
  • 利用指针在函数内修改函数外的变量值

  Go 语言也提供了类似 C/C++ 语言中的指针,只是不允许对指针偏移运算。指针常被誉为 C/C++ 性能卓越的根本,但也是很多程序员对 C/C++ 编程中最难把握的部分,饱受诟病的也是指针的运算和内存释放。Go语言禁止对指针偏移运算,规避了因指针运算引起的数据修改风险和内存溢出现象,且又使垃圾回收效率变得更高。

指针相关的概念

  指针(pointer):就是指向内存中某一个值的地址。例如我们有个 int 变量 a 的值是 68,但这个 68 存放在内存的具体位置是哪,有了地址软件系统才会访问得到这个数据(就如同通过门牌号找到房间)。那么这个地址(门牌号)就叫指针,它指向了数据的保存地址,而不是数据本身。

  指针类型:一个指针指向的地址存储的数据是什么类型的,就该按照什么类型来读写操作,这个数据类型就称作指针类型。

  指针变量:保存指针(指向的内存地址)的变量就叫指针变量,其本身属于无符号整型,保存的就是数字,这个数字就是指向内存区块的索引地址。通过这个值就可以访问到该内存区块保存的实际内容。

  引用:通常我们给一个变量赋值或调用函数传递参数时,都是赋予的变量或字面量的值。如果这个过程中我们我们不使用变量的值,而使用该变量在内存中的地址,那通常将这个获取地址的过程称为“引用”。

  空指针:没有指向任何地址但是已经声明类型的指针变量,其值为 nil。

指针变量的声明

  声明指针变量基本语法是在基本数据类型前加个 * 符号,或在某个变量前加 & 符号引用这个变量的地址赋值给新声明的变量。下面举例说明指针变量声明相关的语法格式:

var p1 *int       // 声明一个引用 int 类型数据的指针变量,默认值为空指针 nil
var p2 *uint16    // 声明一个引用 uint16 类型数据的指针变量,默认值为空指针 nil
var p3 *float32   // 声明一个引用 float32 类型数据的指针变量,默认值为空指针 nil
var p4 *string    // 声明一个引用 string 类型数据的指针变量,默认值为空指针 nil

p5 := new(uint8)  // 使用 new函数 声明一个引用 uint8 类型数据的指针变量 p5,同时分配内存并初始化该内存区块为 0 值
p6 := new(string) // 使用 new函数 声明一个引用 string 类型数据的指针变量 p6,同时分配内存并初始化该内存区块为 空字符串

a := 45           // 声明一个 int 类型的变量,并初始化值为 45
p7 := &a          // 声明变量 p7,初始化值为引用变量 a 的地址(&a 表示获取变量 a 内存地址),编译器推导类型为 int 类型指针

获取和修改指针指向的值

  使用 & 代表获取一个变量的地址,而 * 除了声明定义指针,还代表获取指针指向的数据值,看如下代码示例:

// test01 项目的 main 包,文件名 main.go
package main

import (
	"fmt"
)

// 主函数,程序入口
func main() {
	a := 45

	p := &a
	fmt.Println("指针p引用a后:", *p)

	*p = 100
	b := *p

	fmt.Println("用*p修改值后:", *p)
	fmt.Println("b用*p赋值后值:", b)
	fmt.Println("再看变量a的值:", a)
	fmt.Println("a与b是否相等:", a == b)
}

  第10行,声明一个 int 类型变量 a 并初始化值为 45。

  第12行,声明一个 int 类型的指针变量 p,初始化值为变量 a 的内存地址。这实际就等于 p 指向的地址内的数据值与a代表的值是同一个,且物理存储位置也是同一个(所以常称为引用)。

  第15行,*p 出现在了等号的左侧,同样表示 p 指向的内存区块的实际内容值,但是这表示要去修改该内容值。这里修改为 100。那既然 *p 的值与 a 的值在同一个物理内存区块上,a 的值是不是也被修改了呢?答案是肯定的,这也是指针的重要特点。这是实际 a 就等于 100 了。

  第16行,使用 *p 的值(也就是 a 的值)简式声明初始化了一个变量 b,这样 b 的类型和值都应该与 a 相同。*p 表示获取指针 p 指向的内存区块的实际内容值,并按照指针类型来读取(这里按照 int 类型读取)。

  第18~21行,使用打印输出语句查看结果,验证我们的分析是否正确。

  下面是编译运行结果:

指针p引用a后: 45
用*p修改值后: 100
b用*p赋值后值: 100
再看变量a的值: 100
a与b是否相等: true

  输出的结果充分证明了我们上面的分析是正确的。*p 只是将操作的地址指向了变量 a 的地址,然后最 *p 的修改,就等同于在修改变量 a,因为它们实际就是一块物理内存区块。注意,是 *p 而不是 p,因为 p 是地址 *p 才是值。

Go语言的指针禁止偏移运算

  为了避免编程过程中控制不好内存越界溢出现象,出现难以查找的BUG,以及提高垃圾回收的性能,Go语言禁止了对指针的偏移运算,都是基于一个已有的数据对象进行引用赋值。因此下面的代码是会出现错误的:

var p1 *int = 123  // 错误,不能给指针变量直接赋数值字面量,因为不知道这个地址是个啥

var p2 *int        // 正确行
p2 = 123           // 错误,不能给指针变量直接赋数值字面量,因为不知道这个地址是个啥

a := 45            // 正确行
p3 := &a           // 正确行
p3++               // 错误,不能将指针地址偏移,因为不知道偏移后的地址是个啥
p3--               // 错误,不能将指针地址偏移,因为不知道偏移后的地址是个啥

利用指针在函数内修改函数外的变量值

  函数的参数如果声明为指针,那么在函数内就可以修改调用时传递给这个参数的变量本身的值,这里简单说一下指针具有这个应用场景,更深入的后面章节在实践。看下面两段代码运行结果的对比:

// test01 项目的 main 包,文件名 main.go
package main

import (
	"fmt"
)

func abc(a int) {
	a = 100
}

// 主函数,程序入口
func main() {
	b := 45

	abc(b)
	fmt.Println(b)
}

  这段代码编译运行后输出的结果是 45,说明在 16 号调用函数 abc 时,传递的参数仅仅是把值传过去了,到函数内部给局部变量赋值,并没有影响外面的变量,这就是参数的值传递。

  再看修改后的代码:

// test01 项目的 main 包,文件名 main.go
package main

import (
	"fmt"
)

func abc(a *int) {
	*a = 100
}

// 主函数,程序入口
func main() {
	b := 45

	abc(&b)
	fmt.Println(b)
}

  这段代码编译运行后输出的结果是 100,说明变量 b 被函数 abc 内部的赋值修改了。

  这段代码与上段代码仅有三处差别,分别是:
  ● 第8行,将函数声明的参数 a 由 int 类型改为了 *int,也就是变成指针类型。
  ● 第9行,将给参数局部变量赋值改为给参数所指的地址实际内容赋值。这就改变了外面传进来的地址指向的内存区块的实际内容。
  ● 第16行,调用 abc 函数时,参数由原来的直接给变量 b 的值改成了给变量 b 的地址。这两函数内修改这个地址指向的值就等于修改了变量 b 的值。这就在函数内影响了函数外面的变量,这就是参数的地址传递或叫引用传递。
.
.
上一节:Go/Golang语言学习实践[回顾]教程16–详解Go语言的各种引号及整数进制

下一节:Go/Golang语言学习实践[回顾]教程18–详解Go语言复合数据类型之数组 […]
.

你可能感兴趣的:(Go语言,golang,Go语言教程,指针,Go,数据类型)