一、PHP 中引用变量的概念
在 PHP 中引用意味着用不同的名字访问同一个变量内容。通过在变量前使用 & 符号进行定义。
二、PHP 的 COW 机制
COW(Copy on Write)是内存优化的常见手段,在 PHP 中也采用了这种方式来优化内存。COW 即“写时复制”,只有当对其中一个或多个变量进行写操作的时候,才会复制一份内存,对其内容进行修改。
示例1:
<?php
$a=range(0,1000);
var_dump(memory_get_usage()); //int(204416)
$b=$a;
var_dump(memory_get_usage()); //int(204464)
$a=range(0,1000);
var_dump(memory_get_usage()); //int(288744)
?>
解析:
1. 对 $a 赋值时,开辟出了一块内存空间;
2. 当 $b=$a 操作时,并没有立即开辟出另一块新的内存空间,而是 $a 与 $b 指向了同一块内存(如图1所示),因而获取此时的内存使用情况几乎与第1步没有差别;
图 1
3. 当对 $a 再进行赋值操作时,即便赋值没有变化,但此时 $a 开辟出了一块新的内存空间(图2),获取内存使用情况与前两步有了明显的差别。
图 2
三、PHP 引用变量的内存空间变化
将示例1中的 $b 改为对 $a 的引用变量,观察其内存空间变化情况。
示例2:
<?php
$a=range(0,1000);
var_dump(memory_get_usage()); //int(204416)
$b=&$a;
var_dump(memory_get_usage()); //int(204464)
$a=range(0,1000);
var_dump(memory_get_usage()); //int(204464)
?>
解析(图3):
1. 对 $a 赋值时,开辟出了一块内存空间;
2. 当 $b=&$a 操作时,$b 指向了与 $a 同一块的内存空间,获取此时的内存使用情况几乎与第1步没有差别;
3. 当对 $a 再进行赋值操作时,只更改了空间内存储的值,而不会开辟出一块新的内存空间,获取此时的内存使用情况几乎与前两步没有差别。
图 3
四、PHP 引用变量的 zval 变化
通过观察 zval 结构体的变化理解引用变量的底层原理。此处需用到 PHP 调试工具 Xdebug 。
示例3:
<?php
$a=range(0,3);
xdebug_debug_zval('a'); //refcount=1,is_ref=0
$b=$a;
xdebug_debug_zval('a'); //refcount=2,is_ref=0
$a=range(0,3);
xdebug_debug_zval('a'); //refcount=1,is_ref=0
?>
解析(图4):
1. 对 $a 赋值时,zval 显示有一个变量指向该变量容器(refcount=1),且非引用变量(is_ref=0);
2. 当 $b=$a 操作时,有两个变量指向该变量容器(refcount=2),且非引用变量(is_ref=0);
3. 当对 $a 再进行赋值操作时,由于 $a 开辟出了一块新的内存空间,此时又只有一个变量指向 $a 的变量容器(refcount=1),且非引用变量(is_ref=0)。即印证了前面第二部分里提到的 PHP 的 COW 机制。
图 4
示例4:
<?php
$a=range(0,3);
xdebug_debug_zval('a'); //refcount=1,is_ref=0
$b=&$a;
xdebug_debug_zval('a'); //refcount=2,is_ref=1
$a=range(0,3);
xdebug_debug_zval('a'); //refcount=2,is_ref=1
?>
解析(图5):
1. 对 $a 赋值时,zval 显示有一个变量指向该变量容器(refcount=1),且非引用变量(is_ref=0);
2. 当 $b=&$a 操作时,有两个变量指向该变量容器(refcount=2),且为引用变量(is_ref=1);
3. 当对 $a 再进行赋值操作时,由于引用变量不会开辟新的内存空间,此时还是有两个变量指向 $a 的变量容器(refcount=2),且为引用变量(is_ref=1)。即印证了前面第三部分里提到的 PHP 引用变量的内存空间变化情况。
图 5
五、PHP 中引用变量的其它几个注意点
1. unset 只会取消引用,不会销毁空间。
示例5:
<?php
$a=1;
$b=&$a;
unset($b);
echo $a; //1
?>
解析(图6):
图 6
2. 对象本身就是引用传递。
示例6:
<?php
class Person
{
public $name='zhangsan';
}
$p1=new Person;
xdebug_debug_zval('p1'); //refcount=1,is_ref=0
$p2=$p1;
xdebug_debug_zval('p1'); //refcount=2,is_ref=0
$p2->name='lisi';
xdebug_debug_zval('p1'); //refcount=2,is_ref=0,name='lisi'
?>
解析(图7):
1. 对 $p1 实例化,zval 显示有一个变量指向该变量容器(refcount=1),且非引用变量(is_ref=0);
2. 当 $p2=$p1 操作时,有两个变量指向该变量容器(refcount=2),且非引用变量(is_ref=0);
3. 当对 $p2->name 进行修改时,还是有两个变量指向 $p1 的变量容器(refcount=2),且 $p1->name 的值也被修改了,由此可知 $p2 并没有开辟新的内存空间,而依然与 $p1 指向同一个空间。is_ref=0 说明 $p1 不是引用变量,但是却进行了引用传递。
图 7
六、考察 PHP 引用变量的经典面试真题
写出如下程序的输出结果:
<?php
$data=['a','b','c'];
foreach($data as $key=>$val){
$val=&$data[$key];
}
?>
1. 程序运行时,每一次循环结束后变量 $data 的值时什么?请解释。
2. 程序执行完成后,变量 $data 的值时什么?请解释。
解析(图8):
1. 第一次循环时,$key=0,$val=’a’ ;之后 $val=&$data[0],$val 与 $data[0] 指向了同一个空间,此时 $data=[‘a’,’b’,’c’] ;
2. 第二次循环时,$key=1,$val=’b’,由于指向同一空间 $data[0]=’b’ ;之后 $val=&$data[1],$val 与 $data[1] 指向了同一个空间,此时 $data=[‘b’,’b’,’c’] ;
3. 第三次循环时,$key=2,$val=’c’,由于指向同一空间 $data[1]=’c’ ;之后 $val=&$data[2],$val 与 $data[2] 指向了同一个空间,此时 $data=[‘b’,’c’,’c’] ;
4. 循环结束,最终 $data=[‘b’,’c’,’c’] 。
图 8