Golang 学习笔记五 指针

一、用处

参考
为什么要有指针?
指针究竟是什么?是地址?还是类型?
指针究竟有什么用?

1.变量
一个东西在内存里面,而你想用语言去表示那个东西,就必须找到一个表示它。于是我们用常量或变量去表示内存里的值。比如, int a = 2;就是把2这个值,放在了内存中。(但是你不知道它的位置,如果你有看到整个内存的能力,你有可能发现有一个2在No.300处)并且你想要在程序中调用它,就必须有一个东西代表它,于是用变量a代表了这块内存中的内容.有了变量,你就可以用他表示一个值。从而你可以使用这个变量。

2.指针
如果你只有这一行程序的话,那指针就没有太大的存在必要了。但是如果你有好几个函数需要读写这个值,那么这时候问题就来了。
int myMoney = 1000;如果你的账上有1000元,有好几个函数要操作这个值,这时候就会产生两种需求。在函数里修改这个值的时候是应该修改原值呢? 还是不修改原值?如果使用foo(myMoney)这种形式的话,就会把myMoney代表的内存中的内容“复制”一份到函数栈里,这样你在函数里修改这个值不会对外界有任何影响。但是,如果你想在函数中对原值进行操作,这时候就不能只传进来内容,而需要传进来一个myMoney的地址,这样,在函数里面就能再程序找到那块地址,把内容修改掉。

所以有了传递地址的需求。为了方便传递地址,所以有了指针,指针也是一个变量,只不过里面存的内容是一个地址。总结地来说, 变量为了表示数据而生, 指针为了传递数据为生。

3.图示


2354823-b1d0581545f4f9a4.png
(1)C语言声明一个变量时,编译器在内存中留出一个唯一的地址单元来存储变量,如下图,变量var初始化为100,编译器将地址为1004的内存单元留给变量,并将地址1004和该变量的名称关联起来。

2354823-27c6b4872d618518.png
(2)创建指针:变量var的地址是1004,是一个数字,地址的这个数字可以用另一个变量来保存它,假设这个变量为p,此时变量p未被初始化,系统为它分配了空间,但值还不确定,如图

2354823-2691d5000a3e59ac.png
(3)初始化指针,将变量var的地址存储到变量p中,初始化后(p=&var),p指向var,称为一个指向var的指针。指针是一个变量,它存储了另一个变量的地址。

(4)声明指针:typename *p 其中typename指的是var的变量类型,可以是 short ,char ,float,因为每个类型占用的内存字节不同,short占2个字节,char占1个字节,float占4个字节,指针的值等于它指向变量的第一个字节的地址 。*是间接运算符,说明p是指针变量,与非指针变量区别开来。

2354823-59d3b24c9c15063d.png
(5)*p和var指的是var的内容;p和&var指的是var的地址

4.既然指针是用来保存地址的,也就是保存一串数字而已,为什么还要有类型?
以 int *a = &b; 为例,并假设这行代码的运行环境下,int 为 4 字节长。等价于int *a;a=&b;
类型系统有两个重要作用,一个是用作编译时类型检查,让编译器帮助我们避免犯错,比如上面的例子,我声明的是一个 int 型指针,但如果赋值时使用的 b 不是 int 型(可能酒后写代码不小心写错了),如果没有指针类型检查的话,这个错误可能只有在程序运行的时候才能发现。

类型的另外一个作用是,指明一个内存地址所保存的二进制数据,应该怎样被解释。想想看在没有指针类型的情况下,虽然我知道了 a 中保存的地址,但当我想解引用指针(*a),读出这块地址中的一个 int 型数据时就有问题了,编译器怎样知道你从这个位置开始要读出一个字节,还是 4 个字节?而在有 int* 这个类型的指导下,编译器知道在解引用的时候读出 4 个字节(int* 对应的指针偏移量是 4 字节),甚至,你也可以通过强制类型转换读出一个 char 类型数据来玩。

比如我们的调用过程希望给子过程传递一个数组,假如这个数组有5000个数吧。那么在如果我们将这个数组作为参数传递会怎样?
在传值的方式下,调用过程会将这个数组在栈上全部复制一遍。栈当然不可能是无限大的,总是有一个到头的地方,如果到头了还放不下这5000个数,那么,恩,栈就直接爆掉。然后操作系统提示你栈溢出,程序崩溃了。这时候我们就可以在其它地方开辟一块连续的内存,然后使用指针指向这段内存的起始位置,将指针传递给子过程。这样传递过程中,只需要在栈上传递一个数,不仅省空间,而且速度快。

二、C/C++ 里指针声明为什么通常不写成 int* ptr 而通常写成 int *ptr ?

C 语言设计的本意并不是把「int*」作为类型声明。它的设计本意是解一个方程,int ....,让 .... 的类型是 int。也就是 *ptr 的类型是 int。从而反推出 ptr 是 int 指针。比如int *var0, var1;这种声明下 var0 是 int 型指针,var1 是 int 类型。

2354823-51f49af193ac395b.png
《C++ primer》(第五版)

2354823-33ca3e464cecb507.png
《C程序设计语言》

2354823-9dc6d4154226ee69.png
《C程序设计语言》

因此,int表示的是【*ip】这个表达式的类型。从表达式【*ip】反向推出未使用*操作的ip是个地址。

三、C++ 引用

参考C++中 引用&与取地址&的区别
引用是给已定义的变量起别名
引用:在声明的时候一定要初始化

#include <iostream>
 
using namespace std;
 
int main()
{
    int a = 88;
    //声明变量a的一个引用c,c是变量a的一个别名
    //引用,声明的时候一定要初始化
    int &c = a;  
    //一个变量可以有多个引用
    int &d = a;  
    
    cout<<"a="<<a<<endl;
    cout<<"c="<<c<<endl;
    cout<<"====================="<<endl;
    c=99;
    cout<<"a="<<a<<endl;
 
    return 0;
}

指针和引用的区别

1.首先,引用不可以为空,但指针可以为空。前面也说过了引用是对象的别名,引用为空——对象都不存在,怎么可能有别名!故定义一个引用的时候,必须初始化。因此如果你有一个变量是用于指向另一个对象,但是它可能为空,这时你应该使用指针;如果变量总是指向一个对象,i.e.,你的设计不允许变量为空,这时你应该使用引用。而声明指针是可以不指向任何对象,也正是因为这个原因,使用指针之前必须做判空操作,而引用就不必。

2.其次,引用不可以改变指向,对一个对象"至死不渝";但是指针可以改变指向,而指向其它对象。说明:虽然引用不可以改变指向,但是可以改变初始化对象的内容。例如就++操作而言,对引用的操作直接反应到所指向的对象,而不是改变指向;而对指针的操作,会使指针指向下一个对象,而不是改变所指对象的内容。

3.再次,引用的大小是所指向的变量的大小,因为引用只是一个别名而已;指针是指针本身的大小,4个字节

4.最后,引用比指针更安全。由于不存在空引用,并且引用一旦被初始化为指向一个对象,它就不能被改变为另一个对象的引用,因此引用很安全。对于指针来说,它可以随时指向别的对象,并且可以不被初始化,或为NULL,所以不安全。const 指针虽然不能改变指向,但仍然存在空指针,并且有可能产生野指针(即多个指针指向一块内存,free掉一个指针之后,别的指针就成了野指针)

总之,用一句话归纳为就是:指针指向一块内存,它的内容是所指内存的地址;而引用则是某块内存的别名,引用不改变指向。

节选自为什么 C++ 有指针了还要引用?
一般来说指针和引用基本可以互换使用,这也是为什么 java 只有引用就够了。c++ 指针的概念完全是因为 c 语言的历史遗留,为了兼容 c 而设计的。好的,那你会问,那为什么不直接用指针,还要搞个引用这种玩意?这是因为 c++ 里有个叫运算符重载的东西,所以 pointer 这个表达式很有可能并不是直接取值的意思,因为被重载了。

四、C++ 值传递、指针传递、引用传递详解
#include<iostream>
using namespace std;
//值传递
 void change1(int n){
    //显示的是拷贝的地址而不是源地址 
    cout<<"值传递--函数操作地址"<<&n<<endl;
    n++;
}

//引用传递
void change2(int & n){
    cout<<"引用传递--函数操作地址"<<&n<<endl; 
    n++;
}
 //指针传递
void change3(int *n){
     cout<<"指针传递--函数操作地址 "<<n<<endl; 
    *n=*n+1;
 } 
int     main(){
    int n=10;
    cout<<"实参的地址"<<&n<<endl;
    change1(n);
    cout<<"after change1() n="<<n<<endl;
    change2(n);
    cout<<"after change2() n="<<n<<endl;
    change3(&n);
    cout<<"after change3() n="<<n<<endl;
    return true;
}

2354823-b509bb86495fc7a6.png
image.png

关于指针传递和引用传递,参考 C++ 中的引用传值和用指针传值有什么区别,哪种方式更优?

五、JAVA中值传递、引用传递

参考
为什么 Java 只有值传递,但 C# 既有值传递,又有引用传递,这种语言设计有哪些好处?
Java 到底是值传递还是引用传递?
1.基本类型和引用类型

int num = 10;
String str = "hello";

如图所示,num是基本类型,值就直接保存在变量中。而str是引用类型,变量中保存的只是实际对象的地址。一般称这种变量为"引用",引用指向实际对象,实际对象中保存着内容。
2.赋值运算符=

num = 20;
str = "java";

对于基本类型 num ,赋值运算符会直接改变变量的值,原来的值被覆盖掉。对于引用类型 str,赋值运算符会改变引用中所保存的地址,原来的地址被覆盖掉。但是原来的对象不会被改变(重要)。如上图所示,"hello" 字符串对象没有被改变。(没有被任何引用所指向的对象是垃圾,会被垃圾回收器回收)

3.简要结论:
java中方法参数传递方式是按值传递。
如果参数是基本类型,传递的是基本类型的字面量值的拷贝。
如果参数是引用类型,传递的是该参量所引用的对象在堆中地址值的拷贝。

六、JavaScript中函数都是值传递吗?
function setName(obj) {
// 这里 obj 和 person 指向内存中的同一块地址,a 地址
obj.name = "Nicholas";
// 这里 obj 指向了新对象所在的地址( b 地址),切断了和 a 地址的联系
obj = new Object();
obj.name = "Greg";
}
var person = new Object();
setName(person);
alert(person.name); //output: "Nicholas"
七、GO中的指针

参考Go语言参数传递是传值还是传引用
1.指针
变量是一种使用方便的占位符,用于引用计算机内存地址。如果用“var x int”声明语句声明一个x变量,那么&x表达式(取x变量的内存地址)将产生一个指向该整数变量的指针,指针对应的数据类型是 *int ,指针被称之为“指向int类型的指针”。如果指针名字为p,那么可以说“p指针指向变量x”,或者说“p指针保存了x变量的内存地址”。同时 *p 表达式对应p指针指向的变量的值。一般 *p 表达式读取指针指向的变量的值,这里为int类型的值,同时因为 *p 对应一个变量,所以该表达式也可以出现在赋值语句的左边,表示更新指针所指向的变量的值。

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"

var a int= 20/* 声明实际变量 */
var ip *int  /* 声明指针变量 */

ip = &a  /* 指针变量的存储地址 */

fmt.Printf("a 变量的地址是: %x\n", &a  )

/* 指针变量的存储地址 */
fmt.Printf("ip 变量储存的指针地址: %x\n", ip )

/* 使用指针访问值 */
fmt.Printf("*ip 变量的值: %d\n", *ip )

a 变量的地址是: 20818a220
ip 变量储存的指针地址: 20818a220
*ip 变量的值: 20

在其它语言中,比如C语言,指针操作是完全不受约束的。在另外一些语言中,指针一般被处理为“引用”,除了到处传递这些指针之外,并不能对这些指针做太多事情。Go语言在这两种范围中取了一种平衡。指针是可见的内存地址,&操作符可以返回一个变量的内存地址,并且*操作符可以获取指针指向的变量内容,但是在Go语言里没有指针运算,也就是不能像c语言里可以对指针进行加或减操作。

任何类型的指针的零值都是nil。如果 p != nil 测试为真,那么p是指向某个有效变量。指针之间也是可以进行相等测试的,只有当它们指向同一个变量或全部是nil时才相等。

var x, y int
fmt.Println(&x == &x, &x == &y, &x == nil) 
// "true false false"

下面是多级指针使用例子

func main() {
    var value int = 42
    var p1 *int = &value
    var p2 **int = &p1
    var p3 ***int = &p2
    fmt.Println(p1, p2, p3)
    fmt.Println(*p1, **p2, ***p3)
}

----------
0xc4200160a0 0xc42000c028 0xc42000c030
42 42 42

2.值传递
函数传递的总是原来这个东西的一个副本,一副拷贝。比如我们传递一个int类型的参数,传递的其实是这个参数的一个副本;传递一个指针类型的参数,其实传递的是这个该指针的一份拷贝,而不是这个指针指向的值。

func main() {
    i:=10
    ip:=&i
    fmt.Printf("原始指针的内存地址是:%p\n",&ip)
    modify(ip)
    fmt.Println("int值被修改了,新值为:",i)
}

 func modify(ip *int){
     fmt.Printf("函数里接收到的指针的内存地址是:%p\n",&ip)
    *ip=1
 }
------------------
原始指针的内存地址是:0xc42000c028
函数里接收到的指针的内存地址是:0xc42000c038
int值被修改了,新值为: 1

首先我们要知道,任何存放在内存里的东西都有自己的地址,指针也不例外,它虽然指向别的数据,但是也有存放该指针的内存。

所以通过输出我们可以看到,这是一个指针的拷贝,因为存放这两个指针的内存地址是不同的,虽然指针的值相同,但是是两个不同的指针。

首先我们看到,我们声明了一个变量i,值为10,它的内存存放地址是0xc420018070,通过这个内存地址,我们可以找到变量i,这个内存地址也就是变量i的指针ip。

指针ip也是一个指针类型的变量,它也需要内存存放它,它的内存地址是多少呢?是0xc42000c028。 在我们传递指针变量ip给modify函数的时候,是该指针变量的拷贝,所以新拷贝的指针变量ip,它的内存地址已经变了,是新的0xc42000c038。

不管是0xc42000c028还是0xc42000c038,我们都可以称之为指针的指针,他们指向同一个指针0xc420018070,这个0xc420018070又指向变量i,这也就是为什么我们可以修改变量i的值。

3.传结构体

func main() {
    p:=Person{"张三"}
    fmt.Printf("原始Person的内存地址是:%p\n",&p)
    modify(p)
    fmt.Println(p)
}

type Person struct {
    Name string
}

 func modify(p Person) {
     fmt.Printf("函数里接收到Person的内存地址是:%p\n",&p)
     p.Name = "李四"
 }
----------
原始Person的内存地址是:0xc4200721b0
函数里接收到Person的内存地址是:0xc4200721c0
{张三}

我们发现,我们自己定义的Person类型,在函数传参的时候也是值传递,但是它的值(Name字段)并没有被修改,我们想改成李四,发现最后的结果还是张三。

我们尝试把modify函数的接收参数改为Person的指针。

func main() {
    p:=Person{"张三"}
    modify(&p)
    fmt.Println(p)
}

type Person struct {
    Name string
}

 func modify(p *Person) {
     p.Name = "李四"
 }

在运行查看输出,我们发现,这次被修改了。

4.迷惑Map

func main() {
    persons:=make(map[string]int)
    persons["张三"]=19

    mp:=&persons

    fmt.Printf("原始map的内存地址是:%p\n",mp)
    modify(persons)
    fmt.Println("map值被修改了,新值为:",persons)
}

 func modify(p map[string]int){
     fmt.Printf("函数里接收到map的内存地址是:%p\n",&p)
     p["张三"]=20
 }
------------
原始map的内存地址是:0xc42000c028
函数里接收到map的内存地址是:0xc42000c038
map值被修改了,新值为: map[张三:20]

两个内存地址是不一样的,所以这又是一个值传递(值的拷贝),那么为什么我们可以修改Map的内容呢?我们可以大胆的猜测,我们使用make函数创建的map是不是一个指针类型呢?看一下源代码:

// makemap implements a Go map creation make(map[k]v, hint)
// If the compiler has determined that the map or the first bucket
// can be created on the stack, h and/or bucket may be non-nil.
// If h != nil, the map can be created directly in h.
// If bucket != nil, bucket can be used as the first bucket.
func makemap(t *maptype, hint int64, h *hmap, bucket unsafe.Pointer) *hmap {
    //省略无关代码
}

现在看func modify(p map)这样的函数,其实就等于func modify(p *hmap)。所以在这里,Go语言通过make函数,字面量的包装,为我们省去了指针的操作,让我们可以更容易的使用map。类似map的,还有chan类型:

func makechan(t *chantype, size int64) *hchan {
    //省略无关代码
}

5.不太一样的slice

func main() {
    ages:=[]int{6,6,6}
    fmt.Printf("原始slice的内存地址是%p\n",ages)
    modify(ages)
    fmt.Println(ages)
}

func modify(ages []int){
    fmt.Printf("函数里接收到slice的内存地址是%p\n",ages)
    ages[0]=1
}

运行打印结果,发现的确是被修改了,而且我们这里打印slice的内存地址是可以直接通过%p打印的,不用使用&取地址符转换。这就可以证明make的slice也是一个指针了吗?不一定,也可能fmt.Printf把slice特殊处理了。

6.小结
最终我们可以确认的是Go语言中所有的传参都是值传递(传值),都是一个副本,一个拷贝。因为拷贝的内容有时候是非引用类型(int、string、struct等这些),这样就在函数中就无法修改原内容数据;有的是引用类型(指针、map、slice、chan等这些),这样就可以修改原内容数据。

是否可以修改原内容数据,和传值、传引用没有必然的关系。在C++中,传引用肯定是可以修改原内容数据的,在Go语言里,虽然只有传值,但是我们也可以修改原内容数据,因为参数是引用类型。

这里也要记住,引用类型和传引用是两个概念。

再记住,Go里只有传值(值传递)。

7.关于字符串的传递,可以参考golang的官方字符串包里为什么都用string类型传入参数而不是*string?
string的底层是一个结构体,包括一个指针和一个长度。传参的时候是值传递,把string的描述结构体复制了一次,所以两个结构体的指针不一样,同时把底层字节数组的指针也复制了,两个字节数组的指针指向同一段区域,也就是字符串字节数组存放的区域,没有发生字符串的整体复制

8.golang语法自动处理
参考<<go语言圣经>>P143

type Employee struct {
ID int
Name string
Address string
DoB time.Time
Position string
Salary int
ManagerID int
}
var dilbert Employee
var employeeOfTheMonth *Employee = &dilbert
employeeOfTheMonth.Position += " (proactive team player)"
//相当于下面语句
(*employeeOfTheMonth).Position += " (proactive team player)"

9.new函数
参考<<go语言圣经>>P62
表达式new(T)将创建一个T类型的匿名变量,初始化为T类型的零值,然后返回变量地址,返回的指针类型为 *T 。

p := new(int) // p, *int 类型, 指向匿名的 int 变量
fmt.Println(*p) // "0"
*p = 2 // 设置 int 匿名变量的值为 2
fmt.Println(*p) // "2"

用new创建变量和普通变量声明语句方式创建变量没有什么区别,除了不需要声明一个临时变量的名字外,我们还可以在表达式中使用new(T)。换言之,new类似是一种语法糖,而不是一个新的基础概念。下面的两个newInt函数有着相同的行为:

func newInt() *int {
return new(int)
}
func newInt() *int {
var dummy int
return &dummy
}

每次调用new函数都是返回一个新的变量的地址,因此下面两个地址是不同的:

p := new(int)
q := new(int)
fmt.Println(p == q) // "false"

猜你喜欢

转载自blog.csdn.net/weixin_33730836/article/details/87321206