Go语言基本语法(三)指针

什么是指针

在Go语言中,"指针是一种存储变量内存地址的数据类型",意味着指针本身是一个特殊的变量,它的值不是数据本身,而是另一个变量在计算机内存中的位置(地址)。形象地说,就像存放了一个数据项在内存中的“门牌号”。

"它允许程序直接操纵内存中的数据",这句话意味着通过指针,程序能够绕过变量本身,直接到达变量在内存中的存储位置,并对那里的数据进行读取或修改。这种能力非常重要,因为:

1. **效率**:当数据结构很大时(如大型数组或结构体),直接操作其内存地址可以避免复制整个数据结构,从而节省时间和空间。
2. **灵活性**:在函数调用时,传递数据的指针而非数据本身的副本,可以使函数有能力修改调用者的数据,这在很多场景下是必要的,比如更新共享状态或配置。
3. **控制权**:指针提供了底层的内存访问能力,这对于系统编程、性能优化和一些高级数据结构的实现至关重要。

例如,如果你有一个很大的数组,想要修改其中的一个元素,直接通过指针定位到那个元素的内存位置并修改它,比起先复制整个数组或结构到函数内部再修改,显然更高效。此外,通过指针,你还可以创建动态数据结构,如链表、树等,因为每个节点可以指向下一个节点的位置。

相比之下,C/C++的指针以其高度灵活性闻名,允许*的偏移和运算,这为系统级编程和大数据操作提供了强大工具,也是其高性能的来源。然而,这种灵活性也带来了风险,如内存泄漏、指针悬挂、缓冲区溢出等问题,这些安全漏洞常常成为黑客攻击的入口,也是操作系统频繁更新修复的原因之一。

指针的概念

指针地址和指针类型

在Go语言中,理解和操作指针时,"指针地址"和"指针类型"是两个核心概念:

指针地址

指针地址指的是一个变量在内存中的实际存储位置。在Go语言中,你可以使用`&`操作符来获取一个变量的地址。这个地址是一个无符号整数,但它通常以十六进制形式显示,代表了变量在内存中的确切位置。例如:

var num int = 10
var ptr *int = &num

在这个例子中,`&num`就是获取变量`num`的内存地址,并将其赋值给指针变量`ptr`,`ptr`的类型就是指向`int`类型的指针,即`*int`。

指针类型

指针类型定义了指针所指向的数据类型。每个指针都有一个明确的类型,它决定了该指针可以指向哪种类型的变量。在Go语言中,指针类型的声明语法是在数据类型前加上星号`*`。例如,`*int`表示一个指向整型变量的指针,`*string`表示一个指向字符串变量的指针。

指针类型的重要性在于,它确保了类型安全,意味着你不能错误地将一个类型的指针赋值给另一个不匹配类型的指针变量,除非通过类型断言或类型转换(在类型兼容的情况下)。

指针的使用

package main
import (
    "fmt"
)
func main() {
    var money int = 156
    var str string = "ppp"
    fmt.Printf("%p %p", &money, &str)
}

运行结果:

指针取值

当使用`&`操作符对普通变量进行取地址操作并得到变量的指针后,可以对指针使用`*`操作符,也就是指针取值,代码如下:

package main
import (
    "fmt"
)
func main() {
    // 准备一个字符串类型
    var day01 = "Hello World"
    // 对字符串取地址, tem类型为*string
    tem := &day01
    // 打印tem的类型
    fmt.Printf("tem type: %T\n", tem)
    // 打印ptr的指针地址
    fmt.Printf("address: %p\n", tem)
    // 对指针进行取值操作
    value := *tem
    // 取值后的类型
    fmt.Printf("value type: %T\n", value)
    // 指针取值后就是指向变量的值
    fmt.Printf("value: %s\n", value)
}

运行结果:


取地址操作符&和取值操作符*是一对互补操作符,&取出地址,*根据地址取出地址指向的值。
变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:

  • 对变量进行取地址操作使用&操作符,可以获得这个变量的指针变量。
  • 指针变量的值是指针地址。
  • 对指针变量进行取值操作使用*操作符,可以获得指针变量指向的原变量的值。

使用指针修改值

通过指针不仅可以取值,也可以修改值:

示例1:

package main

import "fmt"

// 交换函数
func swap(a, b *int) {

    // 取a指针的值, 赋给临时变量t
    t := *a

    // 取b指针的值, 赋给a指针指向的变量,
    *a = *b

    // 将a指针的值赋给b指针指向的变量
    *b = t
}

func main() {

// 准备两个变量, 赋值1和2
    x, y := 1, 2

    // 交换变量值
    swap(&x, &y)

    // 输出变量值
    fmt.Println(x, y)
}

运行结果:

*操作符作为右值时,意义是取指针的值,作为左值时,也就是放在赋值操作符的左边时,表示 a 指针指向的变量。其实归纳起来,*操作符的根本意义就是操作指针指向的变量。当操作在右值时,就是取指向变量的值,当操作在左值时,就是将值设置给指向的变量。

如果在 swap() 函数中交换操作的是指针值,会发生什么情况?可以参考下面代码:

package main

import "fmt"

func swap(a, b *int) {
    b, a = a, b
}

func main() {
    x, y := 1, 2
    swap(&x, &y)
    fmt.Println(x, y)
}

运行结果:

结果表明,交换是不成功的。上面代码中的 swap() 函数交换的是 a 和 b 的地址,在交换完毕后,a 和 b 的变量值确实被交换。但和 a、b 关联的两个变量并没有实际关联。这就像写有两座房子的卡片放在桌上一字摊开,交换两座房子的卡片后并不会对两座房子有任何影响。

示例2:

package main

import "fmt"

func main() {
	// 声明一个整型变量
	var num int = 10

	// 声明一个指向整型的指针
	var ptr = &num

	//将50赋给ptr指针指向的变量
	*ptr = 50

	fmt.Println(num) //输出50
	fmt.Println(*ptr) //输出50
}

指针的指针

指针的指针,顾名思义,就是一个指针变量,它的值是另一个指针的地址。在Go语言中,正如你可以声明指向任何基本类型或复合类型的指针一样,你也可以声明指向指针的指针。这种多级指针可以用来表示更加复杂的内存关系,或者在某些情况下,为了通过函数传递指针并修改指针本身(而不仅仅是指针指向的值)时使用。

声明与初始化

假设你有一个整型指针*int,那么一个指向这个整型指针的指针就会是**int。声明和初始化一个指针的指针的方式如下:

package main
import "fmt"

func main() {
    // 声明一个整型变量
    var num int = 10
    
    // 声明一个指向整型的指针
    var ptr *int = &num
    
    // 声明一个指向指针的指针(即指针的指针)
    var ptrToPtr **int = &ptr
    
    // 修改通过指针的指针访问的值
    **ptrToPtr = 20
    
    fmt.Println(num)       // 输出: 20
    fmt.Println(*ptr)      // 输出: 20
    fmt.Println(*ptrToPtr) // 输出: 地址,显示ptr的地址
}

使用场景

指针的指针在实际编程中的使用相对较少,但在某些特定场景下非常有用,例如:

  • 当你需要通过函数修改一个指针变量本身(比如让指针指向不同的内存地址)时。
  • 在配置或设置结构体的指针成员时,特别是这些成员也是指针类型。
  • 在某些高级的数据结构或底层系统编程中,用于复杂的数据操作和内存管理。

new() 函数

Go语言还提供了另外一种方法来创建指针变量,格式如下:

new(类型)

一般这样写:

str := new(string)
*str = "Go语言教程"
fmt.Println(*str)

new() 函数可以创建一个对应类型的指针,创建过程会分配内存,被创建的指针指向默认值。

参考文章:(6 封私信 / 42 条消息) Go 语言怎么定义和使用指针? - 知乎 (zhihu.com)

上一篇:【LeetCode】---15.最小栈-三、代码实现:


下一篇:用 LM Studio 1 分钟搭建可在本地运行大型语言模型平台替代 ChatGPT