从PHP的引用BUG谈开去
大蛇写这篇文章是因为TIPI上关于PHP写时复制(Copy-On-Write)问题被同事发到群里,引起了我的兴趣。下面就把这个问题说出来,大家想想为什么: ?php$foo['love']= 1;$bar= $tipi= $foo;$tipi['love']= '2';echo $foo['love']; // 输出 2 相信很多人会认为这
大蛇写这篇文章是因为TIPI上关于PHP写时复制(Copy-On-Write)问题被同事发到群里,引起了我的兴趣。下面就把这个问题说出来,大家想想为什么:
<?php $foo['love'] = 1; $bar = &$foo['love']; $tipi = $foo; $tipi['love'] = '2'; echo $foo['love']; // 输出 2
相信很多人会认为这是一个BUG,为什么$foo['love']的值会被改变?在官方的邮件列表中,这个问题也被讨论烂了,与其说是特性,我更愿意说它是个BUG。因为一切有可能挖坑的动作都应该被规避。
为什么会有这样的差异?我们从这里谈开去。
变量类型:
PHP的变量类型有8种,其中NULL和resource是特殊类型,我们常用的有简单类型:int, float, string, boolean,和复合类型:array, object。
何谓简单类型?何谓复合类型?我们来看一看PHP是怎样实现变量的。
typedef union _zvalue_value { long lval; /* long value */ double dval; /* double value */ struct { char *val; int len; } str; HashTable *ht; /* hash table value */ zend_object_value obj; } zvalue_value; typedef struct _zval_struct zval; struct _zval_struct { zvalue_value value; zend_uint refcount__gc; zend_uchar type; zend_uchar is_ref__gc; };
如上,定义了联合体zvalue_value和结构体zval。
zval是变量的结构,而zval_value就是zval的值。
在zval中type表示数据的类型,他们是:
#define IS_NULL 0 #define IS_LONG 1 #define IS_DOUBLE 2 #define IS_BOOL 3 #define IS_ARRAY 4 #define IS_OBJECT 5 #define IS_STRING 6 #define IS_RESOURCE 7
3(IS_BOOL)及以下类型可以通过联合体 zvalue_value其中的一项来表述,布尔和整型保存在lval中,浮点和双精度浮点保存在double中,NULL是无需保存的,只要type设为IS_NULL就行了。剩下的则麻烦点,比如字符串型的存在struct str中,分别保存字符串和长度,也就是说我们使用 strlen会直接返回长度而无需重新计算字符串长度。数组保存在哈希表ht中,而对象则保存在 obj中。
复合型的变量,如$array['foo']=888;,$array的类型是 IS_ARRAY,而$array['foo']的类型则是IS_LONG,在$array中实际保存的并非888,而是指向$array['foo']的指针。也就是说array的值实际上是一个指针的集合。
我们再看zval中的refcount__gc和 is_ref__gc。refcount__gc是一个计数器,而is_ref__gc则表示该变量是否为引用。那么
$a = &$b; $c=1;
的情况下,$a和$b的is_ref__gc值均为1;$c的is_ref__gc的值为0。
那么refcount__gc在什么时候用呢?那我们接下来说说变量的回收机制。
变量在unset的时候会被注销,那么他的值占用的内存是否马上释放呢?实际上不是,refcount__gc这个计数器就是做这个用途的。
PHP有个特性叫做写时复制(Copy-On-Write),例如:
$a = 1; $b = $a;
这个时候PHP并不会为$b申请一块新的内存,而是将$a的refcount__gc这个计数器加1,再将$a赋值给$b。当我们echo $b时,实际上读到的是同$a指向同一个内存地址的值。当我们执行:
$b=2;
的时候,PHP会先检查$b是否为引用(这里不是),然后再将$b与$a共同的refcount__gc减1并判断是否为0(这里不是,而是1),那么PHP会重新为$b申请一块新内存,复制$a的值,再修改为2,这个时候$b的refcount__gc发生一次自增,变为1。
那么PHP在unset($b)时所做的就是判断它的refcount__gc在减1后是否为0,如果是,那么则回收(实际上也并没有释放内存,只是放到缓冲区,等满了再释放);如果不是,则只把$b从符号表删除。
=============== 休息,休息一下 ======== 一休割 ===============
那么我们来分析以下为什么会出现文章开头的那个问题,下面一行行分析:
$foo ['love'] = 1; // $foo: refcount=1; isref=0; // ->love: refcount=1; isref=0; $bar = &$foo['love']; // $bar: refcount=2; isref=1; // $foo->love: refcount=2; isref=1; // $foo: refcount=1; isref=0; $tipi = $foo; // $foo: refcount=2; isref=0; // $foo->love: refcount=2; isref=1; // $tipi: refcount=2; isref=0; // $tipi->love: refcount=2; isref=1; // 注意,这一步复合类型(array)$foo的refcount自增到2,而$foo['love']还是数组的hashtable指向的另一块内存地址,它并不会被复制 $tipi['love'] = '2'; // 这里的$tipi['love']是一个引用,如同$foo['love']一样 echo $foo['love']; // 所以当$tipi['love']改变以后,这里自然会输出 2
理解了吗?
相信你看完这个分析后也会认为这个是PHP的特色,我也是这么想的。知道真相后似乎要推翻之前的结论——这是个BUG。但是仔细想想,这种坑实际上是不应该出现的,所以我还是坚持最开始的想法——这就是个BUG!当然,见仁见智。
引用不要滥用,因为PHP本身已经对变量做了很好的优化。但是有些时候还是该用,比如你实际上想传址而不是传值。
另外,大蛇要提醒一句,在5.4.0中,动态引用已经被取消了,例如:
function myfunc($var){ $var = 1; } myfunc(& $foo)
这里这种写法是会导致错误出现的,正确的用法应该是:
function myfunc(& $var){ $var = 1; } myfunc($foo)
好吧,到这里,就到这里,再见吧!
原文地址:从PHP的引用BUG谈开去, 感谢原作者分享。

热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

AI Hentai Generator
免费生成ai无尽的。

热门文章

热工具

记事本++7.3.1
好用且免费的代码编辑器

SublimeText3汉化版
中文版,非常好用

禅工作室 13.0.1
功能强大的PHP集成开发环境

Dreamweaver CS6
视觉化网页开发工具

SublimeText3 Mac版
神级代码编辑软件(SublimeText3)

PHP 8.4 带来了多项新功能、安全性改进和性能改进,同时弃用和删除了大量功能。 本指南介绍了如何在 Ubuntu、Debian 或其衍生版本上安装 PHP 8.4 或升级到 PHP 8.4

CakePHP 是 PHP 的开源框架。它的目的是使应用程序的开发、部署和维护变得更加容易。 CakePHP 基于类似 MVC 的架构,功能强大且易于掌握。模型、视图和控制器 gu
