PHP中引用的详解(引用计数、写时拷贝)
《PHP5中文手册》内容中"引用的解释"一文的摘要:
1. PHP中引用的特性
PHP中引用意味着用不同的名字访问同一个变量内容,引用不是C的指针(C语言中的指针里面存储的是变量的内容,在内存中存放的地址),是变量的另外一个别名或者映射。注意在 PHP 中,变量名和变量内容是不一样的,因此同样的内容可以有不同的名字。最接近的比喻是 Unix 的文件名和文件本身――变量名是目录条目,而变量内容则是文件本身。引用可以被看作是 Unix 文件系统中的紧密连接或者wins的快捷方式。2.变量的引用
PHP 的引用允许用两个变量来指向同一个内容。
<?php $a = " ABC " ; $b =& $a ; echo $a ; // 这里输出:ABC echo $b ; // 这里输出:ABC $b = " EFG " ; echo $a ; // 这里$a的值变为EFG 所以输出EFG echo $b ; // 这里输出EFG ?>这意味着 $a 和 $b 指向了同一个变量。
3 对象的引用(对象的赋值传递)
<?php class fooclass{ public $att ; } $a = new fooclass(); $b = $a; $a->att = 1; $b->att = 2; echo 'a obj:',($a->att),'<br>'; echo 'b obj:',($b->att); /** * 结果: * a obj:2 b obj:2 */
在PHP5中 对象的赋值是个引用的过程。上列中
$a = new fooclass(); $b = $a;
其实等效于$b=new fooclass(); $c=&$b;
PHP5中默认就是通过引用来调用对象, 但有时你可能想建立一个对象的副本,并希望原来的对象的改变不影响到副本 . 为了这样的目的,PHP5定义了一个特殊的方法,称为__clone。
自 PHP 5 起,new 自动返回引用,因此在此使用 =& 已经过时了并且会产生 E_STRICT 级别的消息。
在php4中,对象的赋值是个拷贝过程,
$a = new foo fooclass(), 实际上$a和new fooclass()的映射到不同对象实例,其中new fooclass产生的是一个匿名的fooclass对象实例 所以需要显式地使用$a = & new fooclass()来进行引用赋值传递。
所以在php4中,为了节省内存空间,$b=new fooclass()一般会改成引用的模式,即 $b=& new fooclass()。
4.函数的引用传递(传址调用)
<?php function foo( & $a ){ $a = $a + 100 ; } $b = 1 ; echo $b ; // 输出1 foo( $b );// 这里$b传递给函数的其实是$b的变量内容所处的内存地址,通过在函数里改变$a的值 就可以改变$b的值了 echo " <br> " ; echo $b ; // 输出101
function test( & $b ){ $b ++ ; } $c = 0 ; call_user_func_array ( 'test' , array ( & $c )); echo $c ;
5 函数的引用返回
<? php function & test() { static $b = 0 ; // 申明一个静态变量 $b = $b + 1 ; echo $b ; return $b ; } $a = test(); // 这条语句会输出 $b的值 为1 $a = 5 ; $a = test(); // 这条语句会输出 $b的值 为2 $a =& test(); // 这条语句会输出 $b的值 为3 $a = 5 ; $a = test(); // 这条语句会输出 $b的值 为6 ?>
通过这种方式$a=test();得到的其实不是函数的引用返回,这跟普通的函数调用没有区别,只是将函数的值赋给$a而已, 而$a做任何改变 都不会影响到函数中的$b。
而通过$a=&test()方式调用函数呢, 他的作用是 将return $b中的 $b变量的内存地址与$a变量的内存地址 指向了同一个地方
即产生了相当于这样的效果($a=&$b;) 所以改变$a的值 也同时改变了$b的值 所以在执行了
$a=&test();
$a=5;
注意:
和参数传递不同,这里必须在两个地方都用 & 符号:定义和使用时都需要加&符号。
& test()指出返回的是一个引用,而不是通常的一个拷贝。
$a =& test()同样也指出 $a 是作为引用的绑定,而不是通常的赋值。
这里是为了让大家理解函数的引用返回才使用静态变量的,其实函数的引用返回多用在对象中:
<?php /** * This is the way how we use pointer to access variable inside the class . */ class talker{ private $data = ' Hi ' ; public function & get(){ return $this -> data; } public function out(){ echo $this -> data; } } $aa = new talker(); $d = & $aa -> get(); $aa -> out(); $d = ' How ' ; $aa -> out(); $d = ' Are ' ; $aa -> out(); $d = ' You ' ; $aa -> out(); //the output is " Hi How Are You "
6 global 引用
当用 global $var 声明一个变量时实际上建立了一个到全局变量$_GLOBAL的引用。具体可以写成如下语句:
<?php $var =& $GLOBALS [ "var" ]; ?>
把 global $var; 当成是 $var =& $GLOBALS['var']; 的简写。从而将其它引用赋给 $var 只改变了本地变量的引用。
如果全局变量赋值给另外另外一个变量$var_test,删除这个$var_test,也不会 unset 全局变量
<?php global $var; $var = "Example variable"; $var_test = & $GLOBALS [ 'var' ]; echo '$var_test is set to ',$var_test,'<br>'; // $var_test = 11; unset($var_test); echo '$GLOBALS[var] is set to ',$GLOBALS ['var'],'<br>'; /** * 输出结果: $var_test is set to Example variable $GLOBALS[var] is set to 11 */
$var1 = "Example variable"; $var2 = ""; function global_references($use_globals) { global $var1, $var2; if (!$use_globals) { $var2 =& $var1; // visible only inside the function } else { $GLOBALS["var2"] =& $var1; // visible also in global context } } global_references(false); echo "var2 is set to '$var2'", '<br>'; // var2 is set to '' global_references(true); echo "var2 is set to '$var2'"; // var2 is set to 'Example variable'
$var2 =& $var1; // visible only inside the function
另外,需要注意在函数中unset($var2)不会真正释放变量内容,可以看做只是断开了函数内部$var2的映射而已。
7 . 取消引用 unset与=null
当你 unset 一个引用,只是断开了变量名和变量内容之间的绑定。这并不意味着变量内容被销毁了。例如:
<?php $a = 1 ; $b =& $a ; unset ( $a ); echo $b; //输出:1:
使用unset($a)与$a=null的结果是不一样的。如果该块内存只有$a一个映射,那么unset($a)与$a=null等价,该内存的引用计数变为0,被自动回收;如果该块内存有$a和$b两个映射,那么unset($a)将导致$a=null且$b不变的情况,而$a=null会导致$a=$b=null的情况。
原因:某变量赋值为null,将导致该变量对应的内存块的引用计数直接置为0,被自动回收。
8 . $this
在一个对象的方法中,$this 永远是调用它的对象的引用。
9. PHP引用使用注意事项
很多人误解Php中的引用跟C当中的指针一样,事实上并非如此,而且很大差别。C语言中的指针除了在数组传递过程中不用显式申明外,其他都需要使用*进行定义,而php中对于地址的指向(类似指针)功能不是由用户自己来实现的,是由Zend核心实现的,php中引用采用的是“引用计数、写时拷贝”的原理,(写时复制(Copy-on-Write,也缩写为COW),顾名思义,就是在写入时才真正复制一份内存进行修改。)
就是除非发生写操作,指向同一个地址的变量或者对象是不会被拷贝的,比如下面的代码:
$a = array('a','c'...'n');
$b = $a;
如果程序仅执行到这里,$b和$b是相同的,但是并没有像C那样,$a和$b占用不同的内存空间,而是指向了同一块内存,这就是php和c的差别,并不需要写成$b=&$a才表示$b指向$a的内存,zend就已经帮你实现了引用,并且zend会非常智能的帮你去判断什么时候该这样处理,什么时候不该这样处理。
如果在后面继续写如下代码,增加一个函数,通过引用的方式传递参数,并打印输出数组大小。
function printArray(&$arr) //引用传递 { print(count($arr)); } printArray($a);
上面的代码中,我们通过引用把$a数组传入printArray()函数,zend引擎会认为printArray()可能会导致对$a的改变,此时就会自动为$b生产一个$a的数据拷贝,重新申请一块内存进行存储。这就是前面提到的“引用计数、写时拷贝”概念。
直观的理解:$a将使用自己原始的内存空间,而$b,则会使用新开辟的内存空间,而这个空间将使用$a的原始($a或者$b改变之前)内容空间的内容的拷贝,然后做对应的改变。
如果我们把上面的代码改成下面这样:
function printArray($arr) //值传递 { print(count($arr)); } printArray($a);
上面的代码直接传递$a值到printArray()中,此时并不存在引用传递,所以没有出现写时拷贝。
测试:
$a = array(1,2,3); $b =$a; function printArray($arr) //值传递 { print(count($arr)); } $s = microtime(true); for($i=1;$i<10000;$i++){ printArray($a); } $e = microtime(true); echo '=----------------------------'; echo $e-$s;//0.031868934631348 s
$a = array(1,2,3); $b = $a; function printArray(&$arr) //引用传递 { print(count($arr)); } $s = microtime(true); for($i=1;$i<10000;$i++){ printArray($a); } $e = microtime(true); echo '=----------------------------'; echo $e-$s;//0.047234058380127
测试结果
值传递:0.031868934631348 s
引用传递: 0.047234058380127
结果引用传递性能下降:50%左右。
所以不正确使用引用,性能反而下降。