编程语言---一篇文章让你搞懂各个语言的引用!以及各个语言的函数参数传递方式 值传递、指针传递还是引用传递?

基础

其他语言的概念存在模糊之处,这里用c++讲解最为清楚
而且,现代语言,大多数可能是在c和c+基础上开发的,比如php是c写的,jvm虚拟机是c++写的,所以,了解c和c++的概念后 能对后面的有更深了解

首先介绍操作符代表的含义

首先是声明

	int a;          // a是一个正常的变量
	int  *p;        // p是一个指针变量
	int &ref = a;  // ref 是 a 的引用
	void swap(int& x, int& y) //这是函数中使用引用得方式

使用

   p = &var;       // p是一个指针变量,&代表取地址,在指针变量p中存储 var 的地址
   a = *p;          //*p代表取这个地址中存的值实际是多少

此外还有多重指针与更复杂的形式,此处不展开

	//这个比较复杂 其他语言用不到 不展开
   int &c=a;    //c为a的别名,在c++11中称为左值引用
   int &&d=10;  //此时为右值引用,并且这时的d可以被左值引用,即int &p=d;

另外 上面的引用实际上是“语法糖”,这种语法糖最明显的是用在函数的定义void swap(int& x, int& y)中,让调用者无感,不然调用者就必须写成 swap(&x, &y);这种形式了

int & rodents = rats;
//实际上是下面代码的伪装表示:
int * const pr = &rats;

引用用处:如我们给小杨和小张关联的手机卡,共享话费,那么改变一个的值,另一个也要改变。

内存中的状态

变量本身有两种属性

任何一个变量都有两个属性 本身的地址 和 存的值

  • int a=1 此时a变量本身的地址是ox100(假设),存的值是1【这里出现了一个地址】
  • int*b;*b=1; 此时b变量本身的地址是ox200,存的值也是个地址,假设为ox201,而ox201这个地址存的值是1 【这里出现了两个地址 这里可以先看后面go的那个图 比较清楚】
  • 引用 int& d = c (非 &d=c 这是取地址的意思)代表的是 c和d 本身的地址相同,自然它们存的值也相同了
    而普通的d=c代表的是 c本身的地址是ox1000 d本身的地址是ox2000,我们只是让ox1000这个地址存的内容=ox2000这个地址存的内容罢了

在这里插入图片描述
这是go的图 表达了类似的意思 但是具体细节有区别
下面是c++的图

int n = 10;
int *p = &n;
int &r = n;

在这里插入图片描述

所以 引用和指针的区别在于

  • 一个指针可以被重新赋值为另一个地址,而引用始终与其初始变量关联。
  • 引用不可以被自己重新赋值为另一个变量,但指针可以指向不同的变量。

栈和堆的分配

在这里插入图片描述
比如java,main方法是个栈,对象变量实际上是个双指针,在栈中这样的形式,region变量本身有个地址OX100,它存的值还是个地址OX200指向堆内存,而OX200下存的才是实际的对象内容
但是对程序员是无感的,你直接region.id就可以访问到100,或者类似go那样,结构体本身地址.属性结构体内容.属性都能访问到内容。而不用像c++那样严格区分出来

这里讲可能比较迷惑 后面看具体对应语言的例子后就清楚了

函数传递的形式

基本类型

值传递

是指调用函数时将参数值复制一份到函数,如果对函数参数进行修改,影响不到实际参数。
形参是实参的拷贝,改变形参的值并不会影响外部实参的值。从被调用函数的角度来说,值传递是单向的(实参->形参),参数的值只能传入,不能传出。

#include <iostream>
 
void changeValue(int num) {
    num = 5;
}
 
int main() {
    int num = 10;
    changeValue(num);
    std::cout << num << std::endl; // 输出结果为10,原始值未被修改
    return 0;
}
指针传递:

形参为指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进行的操作

#include <iostream>
 
void changeValue(int* numPtr) {
    *numPtr = 5;
}
 
int main() {
    int num = 10;
    changeValue(&num);
    std::cout << num << std::endl; // 输出结果为5,原始值被修改
    return 0;
引用传递

是指调用函数时直接将参数的指针传递到函数中(我们调用的时候 没有对参数本身进行取地址操作),函数中对参数的修改,将影响到实际参数。

引用的意思是:不同的名字访问同一个变量内容,这两个变量本身的地址是相同的
形参相当于是实参的“别名”,对形参的操作其实就是对实参的操作,在引用传递过程中,被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。


#include <iostream>
 
void changeValue(int& numRef) {  //这里是在函数定义的时候操作的 不是调用的时候
    numRef = 5;
}
 
int main() {
    int num = 10;
    changeValue(num);
    std::cout << num << std::endl; // 输出结果为5,原始值被修改
    return 0;

看golang的那个例子
我们不能单纯因为函数内部的修改可以反馈到外面就认为是传递的引用!
区别在于 对函数调用者来说是无感的

引申类型

看似是引用传递,其实是值传递——涉及指针的

看go和java的例子
为了使名称指代更加明确,我们区分一下

  • 当传的值是实际内容——纯粹的值传递
  • 当传的值其实是指针——叫 指针的值传递
模拟的引用传递

就是指针那个例子
区别在于 真正的引用传递 函数调用者是无感的
模拟的引用传递 函数调用者是有感的 手动传了个地址

在开始下面的章节前,我们有必要对概念进行一次梳理

地址与所存的值
首先,变量固有两种属性,本身地址所存的值
我们说一个变量等于什么 通常是说它所存的值 而非地址,即使是复杂的变量 涉及很多指针
实际内容与指针内容
二者都是“地址与所存的值”中的后者,问题在于一些变量比较特殊,它们是指针变量,它本身有个地址,但是所存的值是另一个地址
二者的区别是:

  • 实际内容指存的是实际的形式 比如1 2 3
  • 地址内容或者指针内容 是指存的值是地址

之后是相等或者赋值的几种形式

  • 实际内容重拷贝赋值,比如int a=1,我们让int b =a,这时候a和b是不相同的地址,b复制拷贝了一份a的值
  • 指针内容重拷贝赋值,和简单内容重拷贝的区别在于,a和b是不相同的地址,b也复制拷贝了一份a的值,不过这个值是一个地址 是一个指针内容 而非实际内容 这时候你修改b下面的内容 会影响a
  • 引用赋值:a和b 本身的地址是相同的 自然其内容也想通了
  • 递归的拷贝赋值:对复杂类型,递归的让实际内容都相同,但是内部的指针都不一样

再之后是 函数参数传递的形式

  • 纯粹的值传递 当传的值是实际内容 形参变化不影响实参
  • 指针的值传递或者看似的引用传递: 当传递的值是指针 形参变化会影响实参
  • 引用传递:函数调用者传递的是实际内容,但是函数的定义部分把它变成了引用,形参变化会影响实参

分语言

首先明确,语言分两种,一种有明确引用符号&的,一种是没有明确引用符号&的
当有明确引用符号的时候,那它肯定是引用了
当没有明确引用符号的时候,它可能是引用,也可能不是

golang

对比c++ 没有引用这个操作,也就是没有int& y = x这个语法 自然而然函数参数中也不能用
其他和前面介绍的c++那一样
你最多只能模拟,比如

x := 10
var y *int = &x // y 是指向 x 的指针 等效于 y := &x
*y = 20        // 通过指针修改 x 的值

此处 y 存储的是 x 的内存地址,而非直接作为 x 的别名‌。
当然函数参数那也无法用了

所有传递都是值传递!

  • 对于基本数据类型(如 int、float32、bool 等),毫无疑问使用的是值传递,对外部不可见。
  • 对于指针,也是值传递
    • 如果你改变指针本身 对外部不可见
    • 但是你改变指针之下所保存的内容,对外部是可见的。
  • 而对于 slice、map、chan 和结构体等引用类型,看似使用的是引用传递,其实也是值传递!对外部可见情况要分类讨论

经典的值传递

func modifyInt(x int) {
    x = 10
}
 
func main() {
    a := 5
    modifyInt(a)
    fmt.Println(a) // 输出:5,a 的值没有被改变
}

看似的引用传递其实本质还是值传递

一个奇怪的例子
package main
 
import "fmt"
 
func modifySlice(s []int) {
    s[0] = 100
}
 
func main() {
    slice := []int{1, 2, 3}
    modifySlice(slice) // slice 的内容会改变
    fmt.Println(slice) // 输出:[100 2 3]
}

在这个例子中,我们感觉这肯定是引用传递呀,但是其实不然,我们看另一个例子

func modifySlice(s []int) {
    s = append(s, 10) // 修改切片
}
 
func main() {
    slice := []int{1, 2, 3}
    modifySlice(slice)
    fmt.Println(slice) // 输出:[1 2 3],切片没有被修改
}

奇怪,这是为什么呢???

首先我们要看下结构体在内存中的形态
  • 栈上分配:
    值类型(Value Types):Go语言中的基本数据类型(如int、float64、bool等)以及自定义的结构体(struct)类型通常会分配在栈上(结构体属性如果是指针 不包含指针指向的内容)。这些对象的大小在编译时是已知的,并且它们的生命周期与其作用域直接相关。

  • 堆上分配:
    引用类型(Reference Types):在Go语言中,切片(slice)、映射(map)、通道(channel)、接口(interface)等引用类型通常会在堆上分配内存。这些对象的大小在编译时不是固定的,它们的生命周期由运行时系统管理,根据需要动态分配和释放。

package main
 
import "fmt"
 
type Person struct {
    name string
    age int
}
 
func main() {
    // 初始化结构体(创建一个结构体对象)
    p1 := Person{"张三", 18}
    fmt.Println(p1.name, p1.age)
 
    // 初始化结构体指针
    p2 := &Person{"李四", 28}
    fmt.Println(p2.name, p2.age)
}

在这里插入图片描述
可以看到

  • 任何变量都有两个属性,一个是变量地址,一个是这个地址下存的东西
    • 如果我们直接声明结构体,则p1是直接在这个内存地址下存储基本数值类型的。
    • 如果我们将p2申明为结构体的指针,它绕了一层,它本身的地址下存的是另一个地址,另一个地址下才是结构体真正的东西在的位置
  • 即使是p2的情况 我们通过p2.age也能访问到18 而不用*p2.age 无论是指针变量还是数值变量的.属性都能访问到实际的值,这样设计是因为 如果age是个指针变量,你就得这么写p1.*age或者*p2.*age就太奇怪了

如果是结构体指针赋值

p1 := &Person{name: "lisi", age: 25}
p2 := p1

在这里插入图片描述

结构体的传参——都是值传递
package main

import "fmt"

type manType struct{
        age int
        high *int
    }
func changeAge(man manType){
    man.age=100
}

func changeHigh(man manType){
    *man.high=100//不能写成man.*high
}
func main() {
    xiaowang:= manType{}
    xiaowang.high = new(int)//要这样为指针变量新建内存 详情看另一篇博客
    fmt.Println(xiaowang.age,*xiaowang.high)//0,0
    changeAge(xiaowang)
    fmt.Println(xiaowang.age,*xiaowang.high)//0,0 发现age没改变
    changeHigh(xiaowang)
    fmt.Println(xiaowang.age,*xiaowang.high)//0,100 发现身高high改变了
}

由此 我们可以得出结论了,根据结构体在内存中的存储情况,如果成员变量是个正常的值,则改变外部不可见,如果是个指针变量,你改的是指针下面的值,所以外部可见。
所以,结构体是值传递

但是 有时候 结构体也会全部被分配到堆上

package main
 
import "fmt"
type manType struct{
        age int
    }
func changeAge(man manType){
    man.age=100
}
func main() {
	xiaowang:= manType{}
    fmt.Println(xiaowang.age)//0
    changeAge(xiaowang)
    fmt.Println(xiaowang.age)//0  发现age没改变
}

体会二者区别 new和&{}会导致分配到堆上

package main
 
import "fmt"
type manType struct{
        age int
    }
func changeAge(man *manType){ //这里变成了 *manType
    man.age=100
}
func main() {
	xiaowang:= new(manType) //变成了使用new
    fmt.Println(xiaowang.age)//0
    changeAge(xiaowang)
    fmt.Println(xiaowang.age)//100  发现age改变了
}

详细看另一篇博客 讲new make {} &{}区别的

回到那个奇怪的例子

之后我们首先看下slice的内部结构 发现其实是个结构体

type SliceHeader struct {
    Data uintptr
    Len int
    Cap int
}

由切片的结构定义可知,切片的结构由三个信息组成:

  • 指针Data,指向底层数组中切片指定的开始位置,这是一个指针
  • 长度Len,即切片的长度
  • 容量Cap,也就是最大长度,即切片开始位置到数组的最后位置的长度

但是我们平常直接用slice的时候 不用写成xxxslice.data xxxslice.len的方式访问,这是因为我们平常用它的时候,相当于是语法糖,xxxslice相当于是*xxx.Data
不过 初始化的时候 没有用这个语法糖 是根据其底层结构来的 new的赋值 是len cap为0 而data为nil

初步分析:
切片是作为值传递给函数的,而非引用传递,因此在函数中会创建一个拷贝切片,而拷贝切片指针也是指向原切片指针所指地址。

在函数中使用append方法,切片的底层数组进行了扩容处理,因此在拷贝切片中,指针指向了新的数组,而原切片并没有指向新的数组,因此原切片不会添加新的值。

func main() {
   arr := []int{1,2,3}
   fmt.Printf("%p\n",arr) //0xc000014150
   addNum(arr)
   fmt.Printf("%p\n",arr) //0xc000014150
}

func addNum(sli []int){
   sli = append(sli,  4)
   //可以看到 这里地址变了 原理和java的string类似 扩容 所以新的地址
   fmt.Printf("%p\n",sli) //0xc00000c360  
}

从输出结果可看出,拷贝切片指针发生改变,而原切片指针没有变化。

此时创建一个长度为3,容量为4的切片。

再次使用append方法在函数中对切片进行添加操作。

代码如下:

func main() {
   arr := make([]int, 3, 4) //创建一个长度为3,容量为4的切片
   fmt.Printf("%p\n", arr) //0xc000012200
   fmt.Println(arr)        //[0 0 0]
   addNum(arr)
   fmt.Printf("%p\n", arr) //0xc000012200
   fmt.Println(arr)        //[0 0 0]
}

func addNum(sli []int) {
   sli = append(sli, 4)
   fmt.Printf("%p\n", sli) //0xc000012200
   fmt.Println(sli)        //[0 0 0 4]
}

从结果可以看出,因为初始时,已经设置了切片的容量为4,所以拷贝切片并没有因为扩容指向新的数组。
但是,slice的内容依然没有改变!这是因为,虽然原切片的底层数组发生了变化,但长度Len没有发生变化,因此原切片的值仍然不变。
实验一下

func main() {
	arr := make([]int, 3, 4) //创建一个长度为3,容量为4的切片
	fmt.Println(arr, len(arr), cap(arr)) //[0 0 0] 3 4
	addNum(arr)
	fmt.Println(arr, len(arr), cap(arr)) //[0 0 0] 3 4
}

func addNum(sli []int) {
	sli = append(sli, 4)
	fmt.Println(sli, len(sli), cap(sli)) //[0 0 0 4] 4 4
}

更深入的验证下

func main() {
    arr := [5]int{1, 3, 5, 6, 7}
    fmt.Printf("addr:%p\n", &arr)// addr:0xc42001a1e0
    s1 := arr[:]
    fmt.Printf("addr:%p\n", &s1)// addr:0xc42000a060

    changeSlice(s1)
}

func changeSlice(s []int) {
    fmt.Printf("addr:%p\n", &s)// addr:0xc42000a080
    fmt.Printf("addr:%p\n", &s[0])// addr:0xc42001a1e0
}

代码中定义了一个数组 arr,然后用它生成了一个slice。如果go中存在引用传递,形参 s 的地址应该与实参 s1 一样(上面c++的证明),通过实际的情况我们发现它们具备完全不同的地址,也就是传参依然发生了拷贝——值传递。

但是这里有个奇怪的现象,大家看到了 arr 的地址与 s[0] 有相同的地址,这也就是为什么我们在函数内部能够修改 slice 的原因,因为当它作为参数传入函数时,虽然 slice 本身是值拷贝,但是它内部引用了对应数组的结构,因此 s[0] 就是 arr[0] 的引用,这也就是能够进行修改的原因。

结论:对结构体、slice、map、chan都是如此

就不在这里一一验证了

如果我们非要改呢?模拟的引用传递

如果需要在函数中进行append操作怎么办?
答:一个方法就是用指针。

如何模拟引用传递?
如果你希望在函数内部修改原始的切片或映射,你可以通过返回切片或映射的指针来实现:

func modifySlice(s *[]int) {
    *s = append(*s, 10) // 使用指针解引用来修改原始切片
}
 
func main() {
    slice := []int{1, 2, 3}
    modifySlice(&slice) // 传递切片的地址
    fmt.Println(slice) // 输出:[1 2 3 10],切片被正确修改
}

对于映射(map),同样的逻辑适用:

func modifyMap(m *map[string]int) {
    (*m)["new"] = 10 // 使用指针解引用来修改原始映射
}
 
func main() {
    m := make(map[string]int)
    m["key"] = 5
    modifyMap(&m) // 传递映射的地址
    fmt.Println(m) // 输出:map[key:5 new:10],映射被正确修改
}

总结

  • 切片的底层是一个结构体,平常的写法可以理解为“语法糖”
  • 我们不能单纯因为函数内部的修改可以反馈到外面就认为是传递的引用!
  • 将一个切片作为函数参数传递给函数时,其实采用的是值传递,因此传递给函数的参数其实是切片结构体的值拷贝。
  • 因为Data是一个指向数组的指针,所以对该指针进行值拷贝时,得到的指针仍指向相同的数组,所以通过拷贝的指针对底层数组进行修改时,原切片的值也会发生相应变化。
  • 但是,我们以值传递的方式传递切片结构体的时候,同时也是传递了Len和Cap的值拷贝,因为这两个成员并不是指针,因此,当函数返回时,原切片结构体的Len和Cap并没有改变。
  • 所以可以解释如下现象:当传递切片给函数时,并且在函数中通过append方法向切片中增加值,当函数返回的时候,切片的值没有发生变化。其实底层数组的值是已经改变了的(如果没有触发扩容的话),但是由于长度Len没有发生改变,所以显示的切片的值也没有发生改变。
  • 对map、结构体类似
  • 此外 注意return和非return的去区别

java

Java虚拟机(JVM)是Java程序运行的核心组件,它使用C++编写

java也是本质上只有值传递 Java中也没有真正的引用传递,尽管对对象引用的值传递看起来像是引用传递。因为传递的是对象引用的副本,而不是对象本身,所以方法内对引用的修改不会影响外部的引用。

先看go的那个例子 和go类似 但是有不同之处

java对象在内存中的模型

这里实际是说 对象在 jvm中是怎么存储的

首先看基本类型,这个很简单,变量在栈中直接存的是值
其次看对象,如下图所示 ,变量在栈中存储的是引用地址,这个地址指向堆中具体的值
在Java中,对象的实例数据存储在堆内存中,而对象的引用(或者说地址)则存储在栈内存中
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
可以看出和go的区别,java对象直接就是存了一个地址

而且,java除了几个基本类型 其他都是对象 String也是对象(此处回忆下string和stringbuilder的区别)

我们还是先以对象举例

class A {
    public int age;

    public A(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}


public class Main {
    public static void change(A a){
         a.setAge(18);
        System.out.println(a.getAge());//18
    }
    public static void main(String[] args) {
        A a = new A(22);
        change(a);
        System.out.println(a.getAge());//18 发现在外部改变了

    }
}

上面的例子发现形参可以改变实参

public class Test {
    public static void change(A a){
         a=new A(18);  //主要这里发生变化
        System.out.println(a.getAge());
    }
    public static void main(String[] args) {
        A a = new A(22);
        change(a);
        System.out.println(a.getAge());

    }
}

这次发现改不了了
更改引用本身,而不是引用所指向的对象,那么这种修改不会影响外部对象
在change方法里面a对象指向了新的实例,也就是说形参指向的是0x8888(假设),而实参指向的还是原来的0x6666,两者引用的地址不一样了,所以并没有改变实参的引用地址,也没有改变引用地址所指向的值!

下面这个例子更明确的说明了,java对象实际是值传递

class A {
    public int age;

    public A(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}


public class Main {
    public static void main(String[] args) {
        A a = new A(22);
        A b = a;
        System.out.println(b.age);//22
        A c = new A(1);
        a=c;
        System.out.println(a.age + " " + b.age + " " + c.age); //1 22 1
    }
}

如果b=a 像c语言的b=&a那样 b是a的另一个名字的话,那么后面让a=c 则b也应该变成c。但是实际上,我们让a=c 是让a指向的地址变成了c而非让a原来指向的内容变成了c的内容

再看string

public class Test{
    public static void main(String[] args){
       String str = "java";
       change(str);
        System.out.println(str);  //java
    }
    public static void change(String str){
        str = "web";
        System.out.println(str);//web
    }
}

很明显,传递的string并没有改变实参的值
在方法里面,str=“web”,是新的对象,所以不同的引用地址,形参改变的值不会影响实参,只有两者指向地址相同才能改变。

c语言

c和cpp的区别在于 没有int &ref = a; 这种形式 但是也可以模拟出来int * const pr = &rats;

php

简单来说
首先 不要去看内存结构 php的有点神奇 可能还封装了一层

  • 对于有引用操作符的 毫无疑问是引用 使用方法和c一致 但是没显性的指针
  • 对于没有引用操作符的
    • 对于数字 字符串 普通数组 关联数组 都是值传递
    • 对于对象来说 和java一样 存的内容是个指针 表现为 看似的引用传递 实际还是值传递
  • 特别的 foreach 循环中的变量也是按值传递的。这意味着你不能通过修改这个变量来改变原始数组的元素

基础

首先,我们要知道php是c写的

语法概要
  • 首先php是弱类型
  • 其次,php没有地址或者指针的概念(虽然语言语法上没有 但是不代表其底层内核没有)
  • 但是有引用的概念 也就是&操作符
内存模型

这里看看对象的就行了 string、array不知道为啥 虽然好像堆内存 但是表现完全是在栈内存
以后再研究吧

在这里插入图片描述

下面举个例子 看看内存中怎么分配的

/*
声明一个人 类 person
*/
class Person{

  var $name; //成员属性,人的姓名
  var $age;//成员属性,人的年龄
  var $sex;//成员属性,人的性别

  function eat(){    //成员方法,吃饭方式
    echo '人在吃饭';
  }

}

//通过person类,实例化三个对象$p1、$p2、$p3

$p1 = new Person();
$p2 = new Person();
$p3 = new Person();

?>

在这里插入图片描述
看内存结构,感觉和java一样,理论上应该string、数组、关联数组、对象 都是披着引用传递外皮的值传递?
但php是c写的,而且有引用符号,实际上应该类似c或者c++ 虽然没有直接的指针

区分明确有引用符号&的和没有引用符号的

首先明确,语言分两种,一种有明确引用符号&的,一种是没有明确引用符号&的

  • 当有明确引用符号的时候,那它肯定是引用了
  • 当没有明确引用符号的时候,它可能是引用,也可能不是

引用相关基础语法介绍【侧重直接带了&符号的讲解】

这里主要是介绍语法知识点 了解怎么用的 不过多展开说明和论述

使用场景

1、需要foreach修改本身的内容
2、当使用大数组、对象的时候,用引用节省内存空间

简单变量的引用

我们平时,如果直接给a=b=2 那么它们值虽然相等,但是其实是独立的两个变量,改变一个的值并不会影响另一个的值,它们是两个不同的地址。

$a = 'before';
$b = &$a;//a b 指向同一块内存,但b并非指针型 
//虽然php没有地址的概念 
//但是我们还是要明确 一个具体的值、结构体、对象等(而非变量)自然是包含两个属性的 地址和实际存的东西

$c =  &$b;//a b c指向同一块内存,或者它们是同名的
$d=$b; // 这里是把b的值复制了一份 让d等于b的值

$b='new';
print_r(['a'=>$a,'b'=>$b,'c'=>$c,'d'=>$d]); //abc都是new,但是d还是before
$a=1;
$b =&$a; //此时a b的值都是1
$b= 2; 
echo $a;//2

//进阶----------
//1 当b等于一个普通变量而非一个值的时候
$c = 3;
$b = $c;
print_r([$a,$b,$c]);//3 3 3 b新指向一个变量,会影响a的值

//2 当b等于一个新的引用的时候,此时的b重定向了!不再是a的同名了
$d=4;
$b = &$d;
print_r([$a,$b,$c,$d]); //3434  b新指向一个引用,不影响ac的值
复杂类型的引用语法

明确带引用符号&的肯定是引用了 使用类似上面简单类型 不需要展开

函数参数的显式引用传递

注意,这里我们手动加上了&操作符

如果我们要交换a和b的顺序,而不用return语句

function swap1($a,$b){
//这种方式 当函数执行完毕 就unset了 外部无法获得内部形参a b的值
$tmp = $a;
$a = $b;
$b = $tmp
}

function swap2(&$a,&$b){ 
//这种传参方式相当于 &$形参=$实参  即引用了 虽然直接这么写会报错 但是可以这么理解
$tmp = $a;
$a = $b;
$b = $tmp
}

$a = 1;$b =2;
swap1($a,$b);//分别为1 2
swap2($a,$b);//分别为2 1

注意

//1,里面必须是变量
swap2(1,2);//如果这里写 会报错
//2 call_user_func的时候 调用函数也要写&
function a(&$b){
    $b++;
}
$c=0;
call_user_func_array('a',array(&$c));
函数本身的引用(用的不多直接放弃 别看了)
function &test(){
    static $b=0;//申明一个静态变量
    $b=$b+1;
    echo $b;
    return $b;
}

$a=test();//这条语句会输出1 
$a=5;
$a=test();//这条语句会输出 $b的值 为2

$a=&test();//这条语句会输出 $b的值 为3
$a=5;
$a=test();//这条语句会输出 $b的值 为6
工具函数与其他应用
可用var_dump看变量是否是引用
引用的取消unset

当你 unset 一个引用,只是断开了变量名和变量内容之间的绑定。这并不意味着变量内容被销毁了。例如:

    $a = 1;
    $b =& $a;
    unset ($a);

不会 unset $b,只是 $a。

global 变量的实质是引用

当用 global $var 声明一个变量时实际上建立了一个到全局变量的引用。也就是说和这样做是相同的:

    $var =& $GLOBALS["var"];

这意味着,例如,unset $var 不会 unset 全局变量

在一个对象的方法中,$this 永远是调用它的对象的引用

引用详解【侧重不带&的讲解】

由于php有显性引用操作符& 下面的内容重点介绍那些 没带引用符号 但是和引用相关的内容

复杂变量

先给出结论:
虽然php内存模型中,普通数组、关联数组和对象 都是堆内存
但是:(在不加&符号的情况下)(php5.3后版本)

  • 数组的表现形式是 简单值拷贝 虽然实际不一定 但是表层是 地址:实际内容的形式 和简单变量一样
  • 对象的表现形式是 看似的引用传递 虽然实际不一定 但是表层是 地址:地址内容的地址

发现只有对象比较特殊,这和之前内存中的状态感觉相违背,因为对象、数组、普通数组都是堆内存中。
对此,可能需要进一步去查php底层 zval 、zend_object 、hashtable等的结构 和更深入的了解

比如 鸟哥博客
https://www.laruence.com/2020/02/25/3182.html
https://www.laruence.com/2009/08/23/1065.html
https://zhuanlan.zhihu.com/p/138239404

这里先不继续了,暂且当成中间还有一层做了特殊处理

以及 以后有空再去查查这个

  1. 对象默认传引用?
    在 PHP 5 及之前的版本中,对象是通过引用传递的,但这一行为在 PHP 5.3.0 中发生了变化。从 PHP 5.3.0 开始,对象也是通过值传递的,但传递的是对象的标识符(或者说是引用),这个标识符指向对象在内存中的位置。这意味着在函数内部对对象属性的修改会反映到原始对象上,但如果你尝试将函数参数设置为新的对象实例,这不会影响到原始变量。
  2. 数组和对象的“复制”
    尽管 PHP 允许你通过值传递数组和对象,但实际上这种“复制”往往是浅复制(shallow copy)。浅复制意味着 PHP 会复制数组或对象的结构,但不会递归地复制其内部的元素或属性。如果数组或对象包含对其他变量的引用(例如,数组中的另一个数组或对象),则这些引用在复制时也会被保留。因此,在某些情况下,即使是通过值传递的数组或对象,也可能出现意外的副作用。

PHP脚本运行的时候,那些变量被放到了栈内存,那些被保存到了堆内存?
在PHP5的Zend Engine的实现中,所有的值都是在堆上分配空间,并且通过引用计数和垃圾收集来管理. PHP5的Zend Engine主要使用指向zval结构的指针来操作值,在很多地方甚至通过zval的二级指针来操作.
而在PHP7的Zend Engine实现中,值是通过zval结构本身来操作(非指针). 新的zval结构直接被存放在VM[虚拟机?]的栈上,HashTable的桶里,以及属性槽里. 这样大大减少了在堆上分配和释放内存的操作,还避免了对简单值的引用计数和垃圾收集.

可能和垃圾回收的有关系?要放引用计数 以后好好看看

普通数组与关联数组
$map1 = ["name"=>"a1","age"=>1];
$map2 = $map1;
$map2["age"]=2;

var_dump($map1,$map2)//[a1,1] , [a1,2]

发现是纯粹值传递 map2的修改不会影响map1

对象

PHP4前,对象的右相等是用的递归的拷贝赋值模式
在php[4,5.3) 对对象类型的相等 默认使用的是引用赋值的形式
5.3.0以及之后 使用看似的引用传递的形式

5.3之后 其实和java的对象一样,对象变量所存的实际上是个地址

//下面都是5.3之后版本
    class test{
        var $value="A";
    }
    $a=new test;

	//让b=a,这里的相等 复制的是指针
    $b=$a;
    $b->value="B";
    echo $a->value;//B 发现b的改变会改a的值
    
    //如果让b改变指向到新地址
    $c = new test;
    $b = $c; //让b新指向到c  //ps这里如果让a=c  那么b的地址仍然是原来的a  而a的地址变成了c  
    echo $a->value;//B  没变  b指向新地址后的改变不影响a
    echo $b->value;//A  变了  b指向新的地址后 值变成c的了
    echo $c->value;//A  没变

由此我们确定了,php的对象变量,其所存的值地址
但有时你可能想建立一个对象的副本,并希望原来的对象的改变不影响到副本 . 为了这样的目的,PHP5定义了一个特殊的方法,称为__clone

下面看下对象上 =&=的区别

class A{
	public $a;
  	function __construct($parameter){
	  $this->a = $parameter;  
  	}
}
//简单的赋值模式
$obj1 = new A(1);
$obj2 = $obj1;
$obj3 = new A(2);

var_dump($obj1,$obj2);//1 1
//重新引向新地址
$obj1 = $obj3;
var_dump($obj1,$obj2,$obj3);//2 1 2

和简单变量一样,=是简单赋值

//改为引用传递
$obj1 = new A(1);
$obj3 = new A(2);
$obj2 = &$obj1;//改为引用传递
$obj1 = $obj3;
var_dump($obj1,$obj2,$obj3);//2 2 2

和简单变量一样,= &是引用赋值

证明PHP中对象不是按引用传递:

class Test{
    public $a ;
}

$test1 = new Test();
$test2 = $test1;  

$test1 = null;
var_dump($test1);   // null
var_dump($test2);   //object  如果是按引用传递,那么$test2也应该为空!
函数中参数

数组

function test($param){
  $param[1] = 1; 
}

$obj = [1,2];
test($obj); 
print_r($obj); //还是[1,2] 没变

关联数组

function test($param){
  $param["b"] = 1; 
}

$obj = ["a"=>1,"b"=>2];
test($obj); 
print_r($obj); //还是["a"=>1,"b"=>2] 没变

对象

class A{
  public $a=1; 
}

function test($param){
  $param->a = 2; //函数内修改了对象属性a=2
}

$obj = new A(); //初始化后对象属性a=1
test($obj); 
var_dump($obj); //对象属性a=2改变了

发现只有对象比较特殊,形参会影响实参

foreach中的引用

当你使用 foreach 遍历数组时,默认情况下你是在访问数组元素的值,而不是它们的引用。例如:

$array = [1, 2, 3];
 
foreach ($array as $value) {
    echo $value . "\n";
}

在这个例子中,$value 是通过值传递的。即使你修改了 $value(比如在循环内部给它重新赋值),这也不会影响原始数组中的元素。

下面再看一个例子

<?php
function test1($arr){
	foreach($arr as $item){  //这里的item是【arr形参】的【形参】
		$item = $item+1;
	}
	return $arr;//返回的是形参
	
}

function test2($arr){
	foreach($arr as &$item){  //这里的item是【arr形参】的【引用】
		$item = $item+1;
	}
	return $arr;//返回的是形参 但是形参已经改变了
	
}
function test3(&$arr){  //这里形参是实参的引用 本质就是实参
	foreach($arr as $item){//虽然上面用了引用 但是这里又是【引用实参】的【形参 】
		$item = $item+1;
	}
	return $arr;//返回的是实参 
}
function test4(&$arr){ 
	foreach($arr as &$item){//这里是【实参】的【实参】
		$item = $item+1;
	}
	return $arr;//返回的是实参 
}

$arr = [1,2,3];
$test1 = test1($arr);
$test2 = test2($arr);
$test3 = test3($arr);
$test4 = test4($arr);

print_r($test1);//【1,2,3】
print_r($test2);//【2,3,4】
print_r($test3);//【1,2,3】
print_r($test4);//【2,3,4】

可以看到 2和4是一样的 那么它的区别在哪呢?
区别在于 当你用了4的语法后,函数外面的$arr也变成了【2,3,4】!

注意事项!要即使unset
例子1

foreach中使用引用后 不会自动unset “局部变量”

<?php
$arr = [1,2,3];
foreach($arr as &$item){
	$item = $item+1;
}

print_r($item);//输出4 不会自动unset 上面item不用引用也是一样的结果

不过 大多数时候 不会出现异常 只要你不在foreach块外面使用item

//在上面的代码基础上
$arr1 = [];//即使这里是空数组
foreach($arr1 as &$item){
	$item = $item+1;
}
print_r($arr1);

但是 不总是如此

$arr = [[1,1],[2,2]];
foreach($arr as &$item){
	$item[1] = $item[1]+1;
}
print_r($arr);//[[1,2],[2,3]] 这里一切正常

$copy_arr = $arr;//我们想要记录下来现在arr的值
//对arr进一步处理
foreach($arr as &$item){
	unset($item[1]);
}
print_r($arr);//[[1],[2]] 正常

//奇怪的事情发生了!!
print_r($copy_arr);//[[1,2],[2]] ?为什么变了呢?

我们在第一个打印后面使用var_dump打印一下

array(2) {
  [0]=>
  array(1) {
    [0]=>
    int(1)
  }
  [1]=>
  &array(1) {
    [0]=>
    int(2)
  }
}

发现 最后一个数组那,多了一个引用符号!这就是问题所在,正常情况不会出现问题,但是当我们第二次需要变更arr,以及需要记录原始的值的时候,就会出问题。特别是在array长度为1的时候 这个问题就更隐蔽了。
解决方法是使用完item后加unset

$arr = [[1,1],[2,2]];
foreach($arr as &$item){
	$item[1] = $item[1]+1;
}
unset($item);//之后就正常了

上面的例子看起来脑子疼 更简单的例子如下

<?php
$arr = ['a', 'b'];
foreach ($arr as &$each){
}
//这里执行完 each没有消失 是arr[1]的引用
foreach ($arr as $each){
	//这里以为each是个新的变量 但是不是!each还存在 是arr[1]的引用
   $each =1;
}

print_r($arr);//[a,1]

所以,如果在foreach中适用引用&来改变数组或者对象的值,那么在foreach完成后一定得手动释放引用。

$arr = [];
foreach ($arr as &$each){
	unset($each);
}

这样也不会报错 可以放心用

例子2

https://blog.csdn.net/wolf23151269/article/details/145450511

假设你有如下 PHP 代码:

<?php $arr = array(1, 2, 3, 4); // 使用引用遍历并修改数组元素 foreach ($arr as &$value) { $value = $value * 2; } // 此时 $arr 变为 array(2, 4, 6, 8) // 再使用非引用方式遍历数组 foreach ($arr as $key => $value) { echo "{$key} => {$value} "; print_r($arr); } ?>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
预期输出可能只是打印每个键值对及数组的内容,但实际输出却是:

0 => 2 Array ( [0] => 2, [1] => 4, [2] => 6, [3] => 2 )
1 => 4 Array ( [0] => 2, [1] => 4, [2] => 6, [3] => 4 )
2 => 6 Array ( [0] => 2, [1] => 4, [2] => 6, [3] => 6 )
3 => 6 Array ( [0] => 2, [1] => 4, [2] => 6, [3] => 6 )
1
2
3
4
可以看到最后一个元素在循环中不断被修改,最终变成了前面某个元素的值。这到底是怎么回事呢?

深入分析问题根源

  1. 引用的特性
    在 PHP 中,使用 & 表示引用传递。引用的特性在于两个变量指向同一个内存地址。代码中的第一个 foreach 循环:

foreach (KaTeX parse error: Expected 'EOF', got '&' at position 8: arr as &̲value) {
$value = $value * 2;
}
1
2
3
在这个循环中:

每一次循环,$value 都被绑定到数组中当前元素的引用;
当你修改 $value 的值时,实际上直接修改了对应数组项的值;
循环结束后,v a l u e 仍然保留着对数组最后一个元素(即 ‘ value 仍然保留着对数组最后一个元素(即 value仍然保留着对数组最后一个元素(即‘arr[3])的引用。
这就是问题的关键:引用在循环结束后不会自动解除。

  1. 后续非引用遍历中的隐患
    接下来的代码中,我们使用了非引用的遍历:

foreach ($arr as $key => KaTeX parse error: Expected '}', got 'EOF' at end of input: …) { echo "{key} => {KaTeX parse error: Expected 'EOF', got '}' at position 6: value}̲ "; print_r…arr);
}
1
2
3
4
虽然这里看似并没有用引用,但 PHP 在执行这个 foreach 时使用的变量 $value,由于在前一个循环中已经被绑定为引用,它仍然指向 $arr[3]。因此,在第二个循环的第一次迭代时,发生了下面的情况:

第一次迭代:

循环将 $arr[0] 的值(2)赋给 $value。
由于 $value 是对 $arr[3] 的引用,这个赋值操作也同时修改了 $arr[3] 的值,变成 2。
此时数组变为 [2, 4, 6, 2]。
后续迭代:

同理,下一次迭代时 $value 被赋值为 $arr[1] 的值(4),导致 a r r [ 3 ] 变成 4 。第三次迭代时, arr[3] 变成 4。 第三次迭代时, arr[3]变成4。第三次迭代时,value 赋值为 $arr[2] 的值(6),使得 $arr[3] 也变成 6。
最后一轮时,实际没有变化,因为 $arr[3] 已经是 6。
这样,最后一个元素不断被错误赋值,导致输出的数组内容出现意外变化。

  1. 为什么会出现“残留引用”?
    PHP 中的变量引用不会因为循环结束而自动清除。循环体外的变量 $value 保持着它最后的引用关系。如果不主动解除这个绑定,那么在后续的赋值操作中,依然会对被引用的目标产生影响。这正是为什么第二个 foreach 循环看似普通的赋值操作会影响到数组最后一个元素。

如何正确处理这种情况

  1. 使用 unset() 解除引用
    最直接的方法是在引用 foreach 循环结束后,主动解除 $value 与数组元素的引用。示例如下:

foreach (KaTeX parse error: Expected 'EOF', got '&' at position 8: arr as &̲value) {
$value = KaTeX parse error: Expected 'EOF', got '}' at position 12: value * 2; }̲ unset(value); // 清除对最后一个数组元素的引用

foreach ($arr as $key => KaTeX parse error: Expected '}', got 'EOF' at end of input: …) { echo "{key} => {KaTeX parse error: Expected 'EOF', got '}' at position 6: value}̲ "; print_r…arr);
}
1
2
3
4
5
6
7
8
9
调用 unset($value) 后,变量 $value 不再保持对 $arr[3] 的引用,从而保证后续赋值不会影响数组。

  1. 避免变量名冲突
    另外一种方法是避免在后续代码中使用相同的变量名。比如,你可以在第一个循环中使用 $item,而在后续循环中使用 $value:

foreach (KaTeX parse error: Expected 'EOF', got '&' at position 8: arr as &̲item) {
$item = KaTeX parse error: Expected 'EOF', got '}' at position 11: item * 2; }̲ unset(item); // 建议也解除 $item 的引用

foreach ($arr as $key => KaTeX parse error: Expected '}', got 'EOF' at end of input: …) { echo "{key} => {KaTeX parse error: Expected 'EOF', got '}' at position 6: value}̲ "; print_r…arr);
}
1
2
3
4
5
6
7
8
9
注意:即使换了变量名,前一个循环结束后,$item 仍然引用了最后一个元素,因此最好也对其调用 unset()。

总结
引用遍历的隐患:
在使用引用遍历时,循环结束后引用变量不会自动解除,这可能导致后续代码中意外修改了引用的对象。

后续操作的误区:
当后续循环中再次使用之前的变量(例如 $value)时,即使不使用引用,赋值操作也会作用到原来引用的目标上(在本例中为数组最后一个元素)。

解决方案:

在引用 foreach 循环结束后调用 unset( v a l u e ) 或 u n s e t ( value) 或 unset( value)unset(item) 以解除引用关系。
尽量避免在同一作用域中混用引用和非引用的循环,或改变变量名后仍记得清理引用。
理解了这个问题的机制后,在实际开发中就可以避免类似的陷阱,提高代码的健壮性和可读性。希望这篇详细的讲解能帮助你深入理解 PHP 中 foreach 循环与引用相关的细节。
————————————————

                        版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/wolf23151269/article/details/145450511

例子3 另外
foreach ($group_array as $k => &$item){ //对多维关联数组 要加引号 否则排序无效  虽然 $item 之后没用到
            arsort($group_array[$k]);
        }
        unset($item);

js

对数组来说 js的数组是对象 形参会影响实参

var arr = new Array('corn', '24');
test_arr(arr);
function test_arr(arr){
    arr[0] = 'qqyumidi';
 }
 
console.log(arr);  //result:["qqyumidi", "24"] 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值