Go36-15-指针

指针

之前已经用到过很多次指针了,不过大多数时候是指指针类型及其对应的指针值。这里要讲更为深入的内容。

其他指针

从传统意义上说,指针是一个指向某个确切的内存地址的值。这个内存地址可以是任何数据或代码的起始地址,比如,某个变量、某个字段或某个函数。

uintptr
在Go语言中还有其他几样东西可以代表“指针”。其中最贴近传统意义的当属uintptr类型了。该类型实际上是一个数值类型,也是Go语言内建的数据类型之一。根据当前计算机的计算架构的不同,它可以存储32位或64位的无符号整数,可以代表任何指针的位(bit)模式,也就是原始的内存地址。

unsafe.Pointer
在Go语言标准库中的unsafe包。unsafe包中有一个类型叫做Pointer,也代表了“指针”。unsafe.Pointer可以表示任何指向可寻址的值的指针,同时它也是前面提到的指针值和uintptr值之间的桥梁。也就是说,通过它,我们可以在这两种值之上进行双向的转换。这里有一个很关键的词——可寻址的(addressable)。在我们继续说unsafe.Pointer之前,需要先要搞清楚这个词的确切含义。

不可寻址的值

一下的值都是不可寻址的:

  • 常量的值
  • 基本类型值的字面量
  • 算数操作的结果值
  • 对各种字面量的索引表达式和切片表达式的结果值。例外,切片字面量的索引结果值是可寻址的
  • 对字符串变量的索引表达式和切片表达式的结果值
  • 对字典变量的索引表达式的结果值
  • 函数字面量和方法字面量,以及对他们的调用表达式的结果值
  • 结构体字面量的字段值,也就是对结构体字面量的选择表达式的结果值
  • 类型转换表达式的结果值
  • 类型断言表达式的结果值
  • 接收表达式的结果值

上面一堆术语,看看在代码里具体指的是哪些类型:

package main

type Named interface {
    // Name 用于获取名字。
    Name() string
}

type Dog struct {
    name string
}

func (dog *Dog) SetName(name string) {
    dog.name = name
}

func (dog Dog) Name() string {
    return dog.name
}

func main() {
    // 示例1。
    const num = 123
    //_ = &num // 常量不可寻址。
    //_ = &(123) // 基本类型值的字面量不可寻址。

    var str = "abc"
    _ = str
    //_ = &(str[0]) // 对字符串变量的索引结果值不可寻址。
    //_ = &(str[0:2]) // 对字符串变量的切片结果值不可寻址。
    str2 := str[0]
    _ = &str2 // 但这样的寻址就是合法的。

    //_ = &(123 + 456) // 算术操作的结果值不可寻址。
    num2 := 456
    _ = num2
    //_ = &(num + num2) // 算术操作的结果值不可寻址。

    //_ = &([3]int{1, 2, 3}[0]) // 对数组字面量的索引结果值不可寻址。
    //_ = &([3]int{1, 2, 3}[0:2]) // 对数组字面量的切片结果值不可寻址。
    _ = &([]int{1, 2, 3}[0]) // 对切片字面量的索引结果值却是可寻址的。
    //_ = &([]int{1, 2, 3}[0:2]) // 对切片字面量的切片结果值不可寻址。
    //_ = &(map[int]string{1: "a"}[0]) // 对字典字面量的索引结果值不可寻址。

    var map1 = map[int]string{1: "a", 2: "b", 3: "c"}
    _ = map1
    //_ = &(map1[2]) // 对字典变量的索引结果值不可寻址。

    //_ = &(func(x, y int) int {
    //  return x + y
    //}) // 字面量代表的函数不可寻址。
    //_ = &(fmt.Sprintf) // 标识符代表的函数不可寻址。
    //_ = &(fmt.Sprintln("abc")) // 对函数的调用结果值不可寻址。

    dog := Dog{"little pig"}
    _ = dog
    //_ = &(dog.Name) // 标识符代表的函数不可寻址。
    //_ = &(dog.Name()) // 对方法的调用结果值不可寻址。

    //_ = &(Dog{"little pig"}.name) // 结构体字面量的字段不可寻址。

    //_ = &(interface{}(dog)) // 类型转换表达式的结果值不可寻址。
    dogI := interface{}(dog)
    _ = dogI
    //_ = &(dogI.(Named)) // 类型断言表达式的结果值不可寻址。
    named := dogI.(Named)
    _ = named
    //_ = &(named.(Dog)) // 类型断言表达式的结果值不可寻址。

    var chan1 = make(chan int, 1)
    chan1 <- 1
    //_ = &(<-chan1) // 接收表达式的结果值不可寻址。
}

总结一个不可寻址的值的特点:

  1. 不可变的值不可寻址。常量、基本类型的值字面量、字符串变量的值、函数以及方法的字面量都是如此。其实这样规定也有安全性方面的考虑。
  2. 绝大多数被视为临时结果的值都是不可寻址的。算术操作的结果值属于临时结果,针对值字面量的表达式结果值也属于临时结果。但有一个例外,对切片字面量的索引结果值虽然也属于临时结果,但却是可寻址的。
  3. 若拿到某值的指针可能会破坏程序的一致性,那么就是不安全的,该值就不可寻址。由于字典的内部机制,对字典的索引结果值的取址操作都是不安全的。另外,获取由字面量或标识符代表的函数或方法的地址显然也是不安全的。

最后,如果把临时结果赋值给一个变量,那么它就是可寻址的了。

不可寻址的值的限制
无法使用取址操作符&获取他们的指针。如果尝试取址会是编译器报错,所以不用太担心。这里再看个小问题:

package main

import "fmt"

type Dog struct {
    name string
}

func (d *Dog) SetName (name string) {
    d.name = name
}

func New(name string) Dog {
    return Dog{name}
}

func main() {
    obj := New("Snoopy")
    obj.SetName("Goofy")
    fmt.Println(obj.name)
    // New("Snoopy").SetName("Wishbone")  //
}

这里写了一个New函数,用于获取Dog的结构体。返回的是结构体的值类型。还有一个指针方法,这里直接对值类型调用指针方法是没有问题的。因为会被自动转译成(&dog).SetName("Goofy")。但是New函数的调用结果值是不可寻址的,所以最后一行尝试直接以链式的方法调用就会有编译问题。这个不可取址的情况应该是属于临时结果,所以把结果赋值给一个变量,再调用指针方法是没有问题的。

自增++和自减--
另外,在Go语言中++和--不属于操作符,而是自增语句或自减语句的组成部分。只要在++或--的左边添加一个表达式,就组成了一个自增语句或自减语句,但是表达式的结果值必须是可寻址的。比如值字面的表达式就是无法自增的1++
这里也有例外,字典字面量和字典变量索引表达式的结果值都是不可寻址的,但是可以自增、自减。
类似的规则还有两个:

  1. 赋值语句,赋值操作符左边的表达式的结果值必须是可寻址的。但是对字典的索引结果值也是赋值
  2. 带有range子句的for语句中,在range关键字左边的表达式的结果值也必须是可寻址的。还是对字典的索引结果值例外。

unsafe.Pointer黑科技

下面讲的方法,可以绕过Go语言的编译器和其他工具的重重检查,并达到潜入内存修改数据的目的。这不是一种正常的手段,使用它会很危险,还很可能造成安全隐患。我们总是应该优先使用常规代码包中提供的API去编写程序,当然也可以把像reflect以及go/ast这样的代码包作为备选项。
指针值、unsafe.Pointer、uintptr有如下的转换规则:

  1. 指针值和unsafe.Pointer可以互相转换
  2. uintptr和unsafe.Pointer也可以互相转换
  3. 指针值和uintptr无法直接互相转换

所以说unsafe.Pointer是指针值和uintptr值之间的桥梁。到这一步,我们现在已经可以获取到变量的uintptr类型的值了:

s := student{}
sP := &s
sPtr := uintptr(unsafe.Pointer(sP))

unsafe.Offsetof 的使用
unsafe.Offsetof函数返回变量(struct类型)指定属性的偏移量,以字节为单位。如下使用就可以获取到结构体的属性相对于结构体的偏移量了:

func main() {
    type student struct {
        name string
        age int
    }
    s1 := student{}
    p1 := unsafe.Offsetof((&s1).name)  // 结构体的第一个变量,偏移量是0
    p2 := unsafe.Offsetof((&s1).age)  // 这里就会有偏移量了
    fmt.Println(p1, p2)
}

搭配使用获取属性的地址
简单的把结构体的地址和属性的偏移量相加,就能获得属性的地址了。获取到了属性的地址后,如果再对这个地址做两次地址转换,就变回属性的指针值了:

package main

import (
    "unsafe"
    "fmt"
)

func main() {
    type student struct {
        name string
        age int
    }
    s1 := student{"Adam", 18}
    s1P := &s1
    s1Ptr := uintptr(unsafe.Pointer(s1P))  // 结构体的地址
    fmt.Println(s1Ptr)
    namePtr := s1Ptr + unsafe.Offsetof(s1P.name)  // name属性的地址
    agePtr := s1Ptr + unsafe.Offsetof(s1P.age)  // age属性的地址
    fmt.Println(namePtr, agePtr)
    nameP := (*string)(unsafe.Pointer(namePtr))  // 获取到属性的指针
    ageP := (*int)(unsafe.Pointer(agePtr))
    fmt.Println(*nameP, *ageP)  // 取值获取到属性指针的值
}

上面的方法,饶了一大圈就是为了获取到结构体里属性的地址。有了地址就可以对操作,也就可以直接修改埋藏的很深的内部数据了。比如可以直接结果别的包里的结构体内的不可导出的属性值。

修改结构体不可导出的属性值
知识点都在上面了,这里直接试着修改别的包的结构体内的不可导出的属性的值:

// article15/example06/model/s.go
package model

// 结构体属性全小写
type Student struct {
    name string
    age int
}

// article15/example06/main.go
package main

import (
    "Go36/article15/example06/model"
    "fmt"
    "unsafe"
)

func main() {
    s1 := model.Student{}
    s1P := &s1
    s1Ptr := uintptr(unsafe.Pointer(s1P))
    namePtr := s1Ptr + 0
    agePtr := s1Ptr + 16
    nameP := (*string)(unsafe.Pointer(namePtr))
    ageP := (*int)(unsafe.Pointer(agePtr))
    *nameP = "Adam"
    *ageP = 22
    fmt.Println(s1)
}

这里unsafe.Pointer类型和uintptr类型所代表指针更贴近于底层和内存,理论上可以利用它们去访问或修改一些内部数据。但是这么用会带来安全隐患,在很多时候,使用它们操纵数据是弊大于利的。总之知道就行了,别这么用。

原文地址:http://blog.51cto.com/steed/2341409

时间: 2024-11-02 23:10:22

Go36-15-指针的相关文章

Go 系列教程 —— 15. 指针

什么是指针? 指针是一种存储变量内存地址(Memory Address)的变量. 如上图所示,变量 b 的值为 156,而 b 的内存地址为 0x1040a124.变量 a 存储了 b 的地址.我们就称 a 指向了 b. 指针的声明 指针变量的类型为 *T,该指针指向一个 T 类型的变量. 接下来我们写点代码. package main import ( "fmt" ) func main() { b := 255 var a *int = &b fmt.Printf(&quo

【学习笔记】【C语言】指针

一.指针变量的定义 1. 格式:变量类型 *指针变量名; 2. 举例:int *p;   char *p2; 3. 注意:定义变量时的*仅仅是指针变量的象征 二.利用指针变量简单修改其他变量的值 1.指向某个变量 int a; int *p; p = &a; 或者 int *p = &a; 2.修改所指向变量的值 *p = 10; 3.在函数内部修改外面变量的值 int a = 10; change(&a); void change(int *n) {     *n = 20; }

三、指针

1.内存地址就是一个编号,这些编号都是连续的,称作地址.编号对应的内存以字节为单位划分. 2.内存地址的大小与数据总线的位数有关 3.内存访问分为:直接访问和间接访问 ①直接访问:直接访问内存单元中的内容 示例: int a = 20 ; 对于直接访问,a代表存放数据的内存单元,通过对a赋值或者取值,实现对内存的访问 ②间接访问:通过内存单元编号(地址)以及数据所占字节数访问内存中的数据 间接访问在程序中随处可见,通过指针实现内存的间接访问 4.指针:就是地址.内存地址,我们习惯上把内存地址叫做

Go指针

Go 语言指针 Go 语言中指针是很容易学习的,Go 语言中使用指针可以更简单的执行一些任务. 接下来让我们来一步步学习 Go 语言指针. 我们都知道,变量是一种使用方便的占位符,用于引用计算机内存地址. Go 语言的取地址符是 &,放到一个变量前使用就会返回相应变量的内存地址. 以下实例演示了变量在内存中地址: 1 package main 2 3 import ( 4 "fmt" 5 ) 6 7 func main(){ 8 var name string 9 name =

《C和指针》总结

链接属性 1.extern 关键字用于标识符第二次或以后的声明并不会改变第一次声明所指定的属性. 2.static 防止被访问 (和java完全不同) 存储类型 1.变量存储地方:普通内存,堆栈,硬件寄存器 2.代码块外声明的是静态变量,存于静态内存(普通内存),程序运行-前-存在,始终存在 3.自动变量 4.代码块内变量 + static --> 静态变量 运算符 1. i+++ ++i 这种5个加号的存在gcc中间有空格可以通过,甚至可以是6个,i+++ + ++i,一个表示正数 指针 1.

golang学习之指针、内存分配

1 func pointer_test() { 2 //空指针,输出为nil 3 var p *int 4 fmt.Printf("p: %v\n", p) 5 //指向局部变量,变量值初始为0 6 var i int 7 p = &i 8 fmt.Printf("p: %v,%v\n", p, *p) 9 //通过指针修改变量数值 10 *p = 8 11 fmt.Printf("p: %v,%v\n", p, *p) 12 //数组的

面试复习重点——数据结构、操作系统、计算机网络、数据库。

必看书籍:剑指offer.程序员面试宝典 来自:腾讯.搜狐.网易.烽火.百度.大众点评.美团.风行 1. 死锁是什么?什么情况下产生?怎么解决? 2. 设计模式(尤其是单例模式,要会写该模式的程序框架,要注意同步问题,怎么实现在要用时才创建) 3. 线程的同步?为什么要同步?线程间通信方式. 4. 进程与线程的区别,进程间通信方式. 5. 容器类:hashmap与hashtable的区别,arraylist与linkedlist的区别 6. 为什么要用多线程,实现多线程的两种方式,有什么区别?

Inode 相关

inode 磁盘存储的最小单位是扇区(sector),8个sector组成一个block,每个block大小为4k 操作系统读取磁盘时,以block为单位. 系统存储文件的数据,分为两种.元数据和数据块.同样,硬盘存储也在系统格式化的时候被分为两部分.分别存储元数据和数据块. 元数据: 元数据是存储文件元信息的地方.这些元信息包括:文件创建者,文件所属组,文件权限信息,文件时间信息,文件类型(regular file,character special file,block special fi

《C++primer》v5 第4章 表达式 读书笔记 习题答案

4.1 105 4.2 *vec.begin()=*(vec.begin())//先调用点运算符,再解引用 *vec.begin()+1=(*vec.begin())+1//先解引用,再加一 4.3略? 4.4 (12/3*4)+(5*15)+(24%4/2)=91 4.5 (a)-86(b)-16 (c)0 (d)0 4.6 n%2 4.7 溢出:计算结果超出该数据类型所能表示的范围 2147483647+1 1U-2 ... 4.8 比较低.. 4.9 首先判断cp是否为空指针,若非空指针则

C-循环,获取数组地址的几种方法

程序的调试的作用: 跟踪CPU执行代码的步骤 监视变量的值在程序执行的时候是如何变化的 do-while 和 while 在实际的开发中, do-while比较少用 因为就算循环无论如何要至少执行一次的时候,while也可以搞定 循环的情况一共就两种: 1.循环次数确定的循环 2.循环次数不确定的循环,但是确定了循环继续活着结束的条件 对于一个一位数组来说 1.获取 a[i]的地址的几种方法 1 &a[i] // 取地址符 1 a+i // 数组名就是数组首地址 1 int *p = a; 2