Posts /

深入PHP中的引用

Twitter Facebook Google+
26 Oct 2015

虽然常说做C/C++编程的程序员转做PHP编程很快可以上手,但是对于PHP中的引用和C++的差别比较大,这种差别更多是由于C++和PHP的变量存储结构不同造成的,本文试图详解一下PHP中的引用,对C++中的引用只是作对比时提及,如果要了解C++的引用请参考《C++ Primer》一书。理解本文最好先看一下笔者PHP变量存储结构的博文变量赋值行为的博文,本文说明PHP引用特别是对象引用和函数返回引用容易被错误使用和错误理解的地方。

1.简单变量引用

对于PHP中的引用,我们先从基本的变量谈起,用之前的程序说话,运行如下PHP程序

  1. <?php  
  2. $a = 1;  
  3. $b = $a;  
  4. echo '$a='.$a."\t".'$b='.$b."\n";  
  5. $b = 2;  
  6. echo '$a='.$a."\t".'$b='.$b."\n";  
  7.   
  8. $c = 1;  
  9. $d = &$c;  
  10. echo '$c='.$c."\t".'$d='.$d."\n";  
  11. $d = 2;  
  12. echo '$c='.$c."\t".'$d='.$d."\n";  
  13. ?>  
这里采用命令行演示,结果如下

  1. $a=1 $b=1  
  2. $a=1 $b=2  
  3. $c=1 $d=1  
  4. $c=2 $d=2  

可以看到:

开始定义$a时初始化为1,定义$b时初始化为$a的值,这时候两个值均为1,下面改变$b的值为2,再次输出,可以看到$b的值发生变化为2而$a的值仍然为1,这时候$a,$b的值不一样;然后,开始定义$c时初始化为1,定义$d时初始化为$c的引用,这时候两个值均为1,下面改变$d的值为2,再次输出,可以看到$d的值发生变化为2而$c的值也为2,这时候$c,$d的值一样

这两个例子的唯一不同在于定义$d时加上了一个引用定义符&。

这里我们用一张图来说明


最开始将$a=1时,$a变量指向内存结构1,$a(内存结构1)的引用计数refcount为1,传值方式赋值给$b后,$a(内存结构1)的引用计数refcount+1为2,由于不是引用故is_ref值为0,$a、$b指向同一内存结构,故此时$b的引用计数refcount也为2而is_ref为0,当$b赋值为2时,检查是否为引用is_ref为0且引用计数refcount>1故新建一个内存结构2其值为2,$b指向这个内存结构,$b的的引用计数refcount为1而is_ref为0,$a的引用计数refcount-1为1而is_ref为0;最开始将$c=1时,$c变量指向内存结构1,$c(内存结构1)的引用计数refcount为1,引用方式赋值为$d后,由于是引用故is_ref值为1,$c(内存结构1)的引用计数refcount设为0,$c、$d指向同一内存结构,故此时$d的引用计数refcount也为0而is_ref为1,当$d赋值为2时,检查是否为引用is_ref为1,不会新建一个内存结构,只是将原来$a指向的内存结构的值改为2,其他不变。

2.对象引用

PHP的对象引用是人们最容易错误理解的地方,由于PHP赋值的"Copy on Write"原则,将一个对象赋值给另外一个对象后两者指向同一个内存地址,这时候去操纵其中一个对象的成员变量实际上就是操作另外一个对象的成员变量,这样由于PHP存储和赋值行为决定的表现和C++中的引用即别名表现是一样的,人们看到赋值后对象这样的表现就说PHP中对象赋值默认传的是引用,显然按照PHP中的引用概念来说,这样的理解完全是错误的,测试代码如下

  1. <?php  
  2. class class1  
  3. {  
  4.     public $a;  
  5.       
  6.     function __construct()  
  7.     {  
  8.         $this->a = 1;  
  9.     }  
  10. }  
  11.   
  12. class class2  
  13. {  
  14.     public $a;  
  15.       
  16.     function __construct()  
  17.     {  
  18.         $this->a = 2;  
  19.     }  
  20. }  
  21.   
  22. $object1 = new class1;  
  23. $object2 = new class2;  
  24.   
  25. $a = $object1;  
  26. $b = $a;  
  27. echo '$a->a='.$a->a."\t".'$b->a='.$b->a."\t".'$a属于类'.get_class($a)."\t".'$b属于类'.get_class($b)."\n";  
  28. $b->a = 3;  
  29. echo '$a->a='.$a->a."\t".'$b->a='.$b->a."\t".'$a属于类'.get_class($a)."\t".'$b属于类'.get_class($b)."\n";  
  30. $b = $object2;  
  31. echo '$a->a='.$a->a."\t".'$b->a='.$b->a."\t".'$a属于类'.get_class($a)."\t".'$b属于类'.get_class($b)."\n";  
  32. echo "\n";  
  33. $c = $object1;  
  34. $d = &$c;  
  35. echo '$c->a='.$c->a."\t".'$d->a='.$d->a."\t".'$c属于类'.get_class($c)."\t".'$d属于类'.get_class($d)."\n";  
  36. $d->a = 4;  
  37. echo '$c->a='.$c->a."\t".'$d->a='.$d->a."\t".'$c属于类'.get_class($c)."\t".'$d属于类'.get_class($d)."\n";  
  38. $d = $object2;  
  39. echo '$c->a='.$c->a."\t".'$d->a='.$d->a."\t".'$c属于类'.get_class($c)."\t".'$d属于类'.get_class($d)."\n";  
  40. ?>  

执行结果如下

  1. $a->a=1 $b->a=1 $a属于类class1  $b属于类class1  
  2. $a->a=3 $b->a=3 $a属于类class1  $b属于类class1  
  3. $a->a=3 $b->a=2 $a属于类class1  $b属于类class2  
  4.   
  5.   
  6. $c->a=3 $d->a=3 $c属于类class1  $d属于类class1  
  7. $c->a=4 $d->a=4 $c属于类class1  $d属于类class1  
  8. $c->a=2 $d->a=2 $c属于类class2  $d属于类class2  

可以看到,$b=$a赋值后改变$b->a的值$a->a也发生了变化,人们一般这时候就认为PHP对象赋值传的是引用,但是我们在PHP中的引用意思是对变量赋值后其关联的引用对象也发生变化,所以,$b=$object2发现$a没有改变,显然这时候并不是引用;$d=&$c才是真正的引用,$d=$object2后$a也发生改变等于$object2。

我们引用PHP官网上的一段话:

“在php5 的对象编程经常提到的一个关键点是“默认情况下对象是通过引用传递的”。但其实这不是完全正确的。下面通过一些例子来说明。

php的引用是别名,就是两个不同的变量名字指向相同的内容。在php5,一个对象变量已经不再保存整个对象的值。只是保存一个标识符来访问真正的对象内容。 当对象作为参数传递,作为结果返回,或者赋值给另外一个变量,另外一个变量跟原来的不是引用的关系,只是他们都保存着同一个标识符的拷贝,这个标识符指向同一个对象的真正内容。”

所以说,其实对象赋值行为和普通变量是一样的,两者的引用表现也是完全一样的。但是一般来说我们更多使用的是对象直接赋值,这样改变其中一个对象属性另一个对象属性也会发生变化,表现的像C++中的引用一样,这样就够了。

3.函数参数传递

对于函数参数传递,有了前面对变量和对象的理解讲解,这里只给出测试代码,请自行分析,不懂请留言

  1. <?php  
  2. //测试变量传递  
  3. function changeByValue($a)  
  4. {  
  5.     $a = 1;  
  6. }  
  7.   
  8. function changeByRef(&$a)  
  9. {  
  10.     $a = 1;  
  11. }  
  12.   
  13. $a = 2;  
  14. echo '$a before pass to changeByValue='.$a."\n";  
  15. changeByValue($a);  
  16. echo '$a after pass to changeByValue='.$a."\n";  
  17. $a = 2;  
  18. echo '$a before pass to changeByRef='.$a."\n";  
  19. changeByRef($a);  
  20. echo '$a after pass to changeByRef='.$a."\n";  
  21.   
  22. //测试对象传递  
  23. class class1  
  24. {  
  25.     public $a;  
  26.       
  27.     function __construct()  
  28.     {  
  29.         $this->a = 1;  
  30.     }  
  31. }  
  32.   
  33. class class2  
  34. {  
  35.     public $a;  
  36.       
  37.     function __construct()  
  38.     {  
  39.         $this->a = 2;  
  40.     }  
  41. }  
  42.   
  43. function changeByValueClass(class1 $a)  
  44. {  
  45.     $a->a = 3;  
  46.       
  47.     global $object1,$object2;  
  48.     echo '此时还没有对对象重新赋值,只是操作对象属性'."\n";  
  49.     echo '$a->a='.$a->a."\t".'$object1->a='.$object1->a."\t".'$a属于类'.get_class($a)."\t".'$obejct1属于类'.get_class($object1)."\n";  
  50.     echo '此时对对象重新赋值'."\n";  
  51.     $a = $object2;  
  52.     echo '$a->a='.$a->a."\t".'$object1->a='.$object1->a."\t".'$a属于类'.get_class($a)."\t".'$obejct1属于类'.get_class($object1)."\n";  
  53. }  
  54.   
  55. function changeByRefClass(class1 &$a)  
  56. {  
  57.     $a->a = 4;  
  58.       
  59.     global $object1,$object2;  
  60.     echo '此时还没有对对象重新赋值,只是操作对象属性'."\n";  
  61.     echo '$a->a='.$a->a."\t".'$object1->a='.$object1->a."\t".'$a属于类'.get_class($a)."\t".'$obejct1属于类'.get_class($object1)."\n";  
  62.     echo '此时对对象重新赋值'."\n";  
  63.     $a = $object2;  
  64.     echo '$a->a='.$a->a."\t".'$object1->a='.$object1->a."\t".'$a属于类'.get_class($a)."\t".'$obejct1属于类'.get_class($object1)."\n";  
  65. }  
  66.   
  67. $object1 = new class1;  
  68. $object2 = new class2;  
  69. changeByValueClass($object1);  
  70. changeByRefClass($object1);  
  71. ?>  

执行结果如下

  1. $a before pass to changeByValue=2  
  2. $a after pass to changeByValue=2  
  3. $a before pass to changeByRef=2  
  4. $a after pass to changeByRef=1  
  5. 此时还没有对对象重新赋值,只是操作对象属性  
  6. $a->a=3 $object1->a=3   $a属于类class1  $obejct1属于类class1  
  7. 此时对对象重新赋值  
  8. $a->a=2 $object1->a=3   $a属于类class2  $obejct1属于类class1  
  9. 此时还没有对对象重新赋值,只是操作对象属性  
  10. $a->a=4 $object1->a=4   $a属于类class1  $obejct1属于类class1  
  11. 此时对对象重新赋值  
  12. $a->a=2 $object1->a=2   $a属于类class2  $obejct1属于类class2  

简单说明一下,当传入简单变量时必须明确指明是传值方式还是传引用方式调用参数,如果要求在函数中形参的重新赋值影响到实参就需要引用方式传值,反之就直接传值就行,特别提一下,一般对象传值就行了因为一般要求改变也只是对对象成员属性进行改变很少需要改变对象赋值

4.函数返回引用

PHP的函数返回引用也是很多人有疑惑的地方,PHP的函数返回引用不仅在函数定义的时候要说明返回的是引用,在调用的时候也要说明以引用的方式调用,缺一不可。这里以函数返回值为简单变量的情况来测试代码,前面已经说过了对象和普通变量引用没什么差别,不过和函数参数调用一样,一般情况下也不需要返回对象引用。

  1. <?php  
  2. $a = 1;  
  3. function returnValue()  
  4. {  
  5.     global $a;   
  6.     return $a;  
  7. }  
  8.   
  9. echo "函数返回值,值方式调用函数\n";  
  10. $b = returnValue();  
  11. debug_zval_dump($a);  
  12. debug_zval_dump($b);  
  13. echo '$a='.$a."\t".'$b='.$b."\n";  
  14. $b=2;  
  15. debug_zval_dump($a);  
  16. debug_zval_dump($b);  
  17. echo '$a='.$a."\t".'$b='.$b."\n";  
  18. echo "函数返回值,引用方式调用函数\n";  
  19. $b = &returnValue();  
  20. debug_zval_dump($a);  
  21. debug_zval_dump($b);  
  22. echo '$a='.$a."\t".'$b='.$b."\n";  
  23. $b=2;  
  24. debug_zval_dump($a);  
  25. debug_zval_dump($b);  
  26. echo '$a='.$a."\t".'$b='.$b."\n";  
  27.   
  28. $a = 1;  
  29. function &returnRef()  
  30. {  
  31.     global $a;   
  32.     return $a;  
  33. }  
  34.   
  35. echo "函数返回引用,值方式调用函数\n";  
  36. $b = returnRef();  
  37. debug_zval_dump($a);  
  38. debug_zval_dump($b);  
  39. echo '$a='.$a."\t".'$b='.$b."\n";  
  40. $b=2;  
  41. debug_zval_dump($a);  
  42. debug_zval_dump($b);  
  43. echo '$a='.$a."\t".'$b='.$b."\n";  
  44. echo "函数返回引用,引用方式调用函数\n";  
  45. $b = &returnRef();  
  46. debug_zval_dump($a);  
  47. debug_zval_dump($b);  
  48. echo '$a='.$a."\t".'$b='.$b."\n";  
  49. $b=2;  
  50. debug_zval_dump($a);  
  51. debug_zval_dump($b);  
  52. echo '$a='.$a."\t".'$b='.$b."\n";  
  53. ?>  

执行结果如下

  1. 函数返回值,值方式调用函数  
  2. long(1) refcount(2)  
  3. long(1) refcount(2)  
  4. $a=1    $b=1  
  5. long(1) refcount(2)  
  6. long(2) refcount(2)  
  7. $a=1    $b=2  
  8. 函数返回值,引用方式调用函数  
  9. long(1) refcount(2)  
  10. long(1) refcount(2)  
  11. $a=1    $b=1  
  12. long(1) refcount(2)  
  13. long(2) refcount(2)  
  14. $a=1    $b=2  
  15. 函数返回引用,值方式调用函数  
  16. long(1) refcount(3)  
  17. long(1) refcount(3)  
  18. $a=1    $b=1  
  19. long(1) refcount(2)  
  20. long(2) refcount(2)  
  21. $a=1    $b=2  
  22. 函数返回引用,引用方式调用函数  
  23. long(1) refcount(1)  
  24. long(1) refcount(1)  
  25. $a=1    $b=1  
  26. long(2) refcount(1)  
  27. long(2) refcount(1)  

可以看到对于函数返回简单变类型的情况,只有函数返回的是引用,同时以引用方式调用函数时才能真正起到引用的效果。

那么为什么PHP要设计成只有函数返回的是引用同时以引用方式调用函数时才能真正起到引用的效果,这不是很麻烦吗?一切都得从根源说起--变量存储结构。
根据以上刺探结果分析绘制内存变化过程示意图如下:


从图上看,当函数直接返回时会将要返回的变量内存结构复制一份成匿名结构体(注意此时引用计数refcount为0),然后这里无论使用=还是=&调用函数结果都是将$b指向这个结构体;当函数引用返回时会将要返回的变量内存结构的指针,这时候使用=调用函数结果和直接赋值$a=$b一样,当使用=&调用函数时和引用赋值$a=&$b表现一样。所以说,这里要弄清楚的是,函数返回方式决定了对要返回的变量的处理方式(复制内存还是直接返回内存指针),函数调用方式决定了待赋值量和返回量的结合方式(指向同一内存结构还是绑定)。


好了,总结一下本博文最重要的两点:对象引用表现和普通变量一样,函数返回方式和调用方式在函数返回引用中各施其职,弄懂这两点对你加深PHP的理解和写出更好的PHP程序相信都是有帮助的。


原创,转载请注明来自http://blog.csdn.net/wenzhou1219


Twitter Facebook Google+