C中的指针

https://www.yuque.com/ueumd/clang/su0rlc 变量的本质分析 https://www.yuque.com/ueumd/clang/tanhre 指针专题

指针的概念

指针的主要作用就是在函数内部改变传递进来变量的值

概念 说明
变量 是一种占位符,用于引用计算机的内存地址。可理解为内存地址的标签
指针 表示内存地址,表示地址的指向。指针是一个指向另一个变量内存地址的值
& 取地址符,例如:{指针}:=&{变量}
* 取值符,例如:{变量}:=*{指针}

声明一个指针:

  1. var p *int

*int表示p是一个int类型指针
p指针中存的是一个int类型变量的地址,这意味着p中不能存其他类型变量的地址。

如何获取某个变量的地址呢?使用操作符&:

  1. var a int = 250
  2. p = &a //将a的地址赋给指针p

那么如何根据指针中的地址找到对应的变量呢?使用操作符*:

  1. var b = *p //根据p中的值找到a,将其值赋给b
  2. fmt.Println(b) //250
  3. *p = 100 //根据p中的值找到a,改变a的值
  4. fmt.Println(a) //100

一定要注意指针的初始化,如果不初始化,则指针的的值是其零值—nil。对未初始化的指针赋值,则会出问题:

  1. var p *int //只声明,未初始化
  2. *p = 100 //报错:invalid memory address or nil pointer dereference

原因是指针p中没值,是个nil,自然就无法根据地址找到变量。如果你想使用指针,必须先确保你的指针中有合法的内存地址才行。应当这样写:

  1. var a int
  2. var p *int = &a //p被初始化为a的地址
  3. *p = 100 //根据p的值找到a,12赋值给a
  4. //或者
  5. var a int
  6. var p *int
  7. p = &a //a的地址赋给p
  8. *p = 100 //根据p的值找到a,12赋值给a

完整的例子

  1. package main
  2. import "fmt"
  3. func main() {
  4. var a int = 250 // 变量a
  5. var p *int = &a // 指针p指向a
  6. var b = *p // 获取p指向的变量a的值
  7. fmt.Println("a =",a, ", b =", b, ", p =", p)
  8. fmt.Println("a的地址 =", &a, ", b的地址 =", &b, ", p的地址 =", &p)
  9. *p = 100 // 改变p指向的变量a的值
  10. fmt.Println("a =",a, ", b =", b, ", p =", p)
  11. fmt.Println("a的地址 =", &a, ", b的地址 =", &b, ", p的地址 =", &p)
  12. var pp **int = &p // 指针pp指向指针p
  13. var c = *pp // 获取pp指向的p的值
  14. var d = **pp // 获取pp指向的p指向的a的值
  15. fmt.Println("pp =", pp, ", c =", c, ", d =", d)
  16. fmt.Println("pp的地址 =", &pp, ", c的地址 =", &c, ", d的地址 =", &d)
  17. }

指针地址和变量空间

Go语言保留了指针, 但是与C语言指针有所不同. 主要体现在:

  • 默认值: nil,C是NULL
  • 操作符 & 取变量地址, * 通过指针访问目标对象
  • 不支持指针运算, 不支持 -> 运算符, 直接用 . 访问目标成员

    变量存储

    写:等号 左边的变量,代表 变量所指向的内存空间。 p = 100
    读:等号 右边的变量,代表 变量内存空间存储的数据值。 num =
    p

    1. func main() {
    2. var p *int
    3. // 开辟内存空间
    4. p = new(int)
    5. *p = 250;
    6. fmt.Println(*p) // 250
    7. var num int
    8. num = *p
    9. fmt.Println(num) // 250
    10. }


    空指针 & 野指针

  • 空指针:未被初始化的指针。 var p int p —> err

  • 野指针:被一片无效的地址空间初始化。 ```c func main() { var p int //空指针 fmt.Println(p) }

func main() { var p int = 0 // 无效地址空间初始化 fmt.Println(p) }

  1. <a name="hiv3H"></a>
  2. ## new & 默认值
  3. 变量声明而没有赋值,默认为零值,不同类型零值不同,例如字符串零值为空字符串;<br />指针声明而没有赋值,默认为nil,即该指针没有任何指向。<br />当指针没有指向的时候,不能对(*point)进行操作包括读取,否则会报空指针异常。
  4. 解决方法即给该指针分配一个指向,即初始化一个内存,并把该内存地址赋予指针变量,例如
  5. ```c
  6. func main1(){
  7. var x int = 99
  8. var p *int = &x
  9. fmt.Println(p) //0xc000114000
  10. fmt.Println(*p) //99
  11. }

或者通过**new**开辟一个内存,并返回这个内存的地址

表达式 new(T)将创建一个 T类型的匿名变量,所做的是为T类型的新值分配并清零一块内存空间。
并将这块内存空间的地址作为结果返回,结果就是指向这个新的T类型值的指针值,返回的指针类型为*T

new创建的内存空间位于heap上,空间的默认值为数据类型默认值。
如:new(int) 则 p为0,new(bool) 则 p为false。

使用new()函数,无需担心其内存的生命周期或怎样将其删除,因为Go语言的内存管理系统会帮我们打理一切。

  1. func pNewInt() {
  2. var p *int
  3. p = new(int)
  4. fmt.Println(*p) // 默认类型的 默认值 0
  5. }
  6. func pNewString() {
  7. var p *string
  8. p = new(string)
  9. // q% 打印go语言格式的字符串
  10. fmt.Printf("%q\n", *p) // 默认类型的 默认值 ""
  11. }
  12. func pNewBool() {
  13. var p *bool
  14. p = new(bool)
  15. fmt.Println(*p) // 默认类型的 默认值 false
  16. }

指针的函数传参(传引用)

  • 传地址(引用):将形参的地址值作为函数参数传递。
  • 传值(数据据):将实参的 值 拷贝一份给形参。

值传递

  1. // 会在栈中开辟新内存空间 a和b,和main中的a和b没有关系
  2. func swap(a, b int) {
  3. a, b = b, a
  4. fmt.Printf("swap: a = %d, b = %d\n", a, b) // swap: a = 20, b = 10
  5. }
  6. func main() {
  7. a, b := 10, 20
  8. swap(a, b) //变量本身传递,值传递
  9. fmt.Printf("main: a = %d, b = %d\n", a, b) // main: a = 10, b = 20
  10. }

image.png

传地址

传引用: 在A栈帧内部,修改B栈帧中的变量值。

  1. // 开辟内存空间 p1和p2 并指向 main方法中的 a和b 空间
  2. func swap(p1, p2 *int) {
  3. *p1, *p2 = *p2, *p1
  4. }
  5. func main() {
  6. a, b := 10, 20
  7. swap(&a, &b) // 地址传递
  8. fmt.Printf("main: a = %d, b = %d\n", a, b)
  9. }

image.png

指针结构体

指针结构体实现的目的:

  1. 通过引用内存空间地址,来为 结构体内 定义的具体的变量实例 进行赋值 — 赋值。
  2. 通过引用内存空间地址,修改 访问结构体后的变量 的具体数值 — 修改值。

    指针的使用

    方法中的指针

    方法即为有接受者的函数,接受者可以是类型的实例变量或者是类型的实例指针变量。但两种效果不同。
    1、类型的实例变量 ```c func main(){ person := Person{“vanyar”, 21} fmt.Printf(“person<%s:%d>\n”, person.name, person.age) person.sayHi() person.ModifyAge(210) person.sayHi() } type Person struct { name string age int } func (p Person) sayHi() { fmt.Printf(“SayHi — This is %s, my age is %d\n”,p.name, p.age) } func (p Person) ModifyAge(age int) { fmt.Printf(“ModifyAge”) p.age = age }

//输出结果 person SayHi — This is vanyar, my age is 21 ModifyAgeSayHi — This is vanyar, my age is 21

  1. 尽管 ModifyAge 方法修改了其age字段,可是方法里的pperson变量的一个副本,修改的只是副本的值。下一次调用sayHi方法的时候,还是person的副本,因此修改方法并不会生效。<br />即实例变量的方式并不会改变接受者本身的值。<br />**2、类型的实例指针变量**
  2. ```c
  3. func (p *Person) ChangeAge(age int) {
  4. fmt.Printf("ModifyAge")
  5. p.age = age
  6. }

Go会根据Person的示例类型,转换成指针类型再拷贝,即 person.ChangeAge会变成 (&person).ChangeAge。
指针类型的接受者,如果实例对象是值,那么go会转换成指针,然后再拷贝,如果本身就是指针对象,那么就直接拷贝指针实例。因为指针都指向一处值,就能修改对象了。