首先声明,我并没有去读PHP的源码,只是对于PHP的有时候诡异的表现感兴趣,找了一下开发人员laruence的博客结合PHP提供的函数debug_zval_dump刺探得到了本博客所阐述的工作机理。如果你想对PHP变量赋值行为有一个了解或想对PHP赋值行为(Copy on write 和 Change on Write)加深理解的话,本文是适合你的,比较深入的去看源代码吧。
首先阅读本博客请先阅读笔者关于PHP变量存储结构博文和laruence的博文深入理解PHP原理之变量分离/引用(Variables Separation),但是要指出的是Laruence关于引用赋值行为的描述和用debug_zval_dump刺探的结果有出入,下面我会指出Laruence的叙述有误的地方。
总结来说,分析PHP赋值行为遵循以下原则:
在保证常规赋值思维的前提下,保证占用尽量小的内存和做尽量少的指针指向变化。
由于PHP弱类型变量的原因,在PHP中存储一个变量会比在C等强类型语言中占用的内存大得多,如果也按照C语言中每个变量开辟一个单独的内存空间来保存,显然将占用大量的内存空间,显然这并不是最优化的设计。我们自己可以设想一下在PHP中诸如以下的赋值语句
<?php $a=1; $b=$a; ?>
对$a显然要开辟一块新的内存,但是对于$b来说要不要开辟新的内存呢?
显然,在PHP中我们是不需要像C一样,每次都去开辟变量内存的,针对上面的情况,我们只需要在变量符号表中加入名字b,同时将其对应的zval指针指向$a指向的zval内存地址即可。其内存结构变化示意图如下
这时候没有做内存开辟,那么什么时候会做内存开辟呢?答案是出现不可调和的矛盾时会产生内存开辟。
我们观察“$a赋值X”这一语句,赋值可为普通赋值(=)和引用赋值(=&),X可为常量或变量。所有的内存开辟都是在X端完成,整理列表如下。
$a | 赋值 | X | 举例 | 判断依据和结果 |
= | 常量 | $a=1; | 新建 $a>A{refcount:1,is_ref:0,value:1,type:IS_LONG} |
|
is_ref=0 refcount=1 |
$b=1; $a=$b; |
$b>A{refcount:1,is_ref:0,value:1,type:IS_LONG} \/ $a>A{refcount:2,is_ref:0,value:1,type:IS_LONG} $b>A{refcount:2,is_ref:0,value:1,type:IS_LONG} |
||
is_ref=0 refcount>1 |
$c=1; $b=$c; $a=$b; |
$c>A{refcount:1,is_ref:0,value:1,type:IS_LONG} \/ $c>A{refcount:2,is_ref:0,value:1,type:IS_LONG} $b>A{refcount:2,is_ref:0,value:1,type:IS_LONG} \/ $c>A{refcount:3,is_ref:0,value:1,type:IS_LONG} $b>A{refcount:3,is_ref:0,value:1,type:IS_LONG} $a>A{refcount:3,is_ref:0,value:1,type:IS_LONG} |
||
is_ref=1 | $c=1; $b=&$c; $a=$b; |
$c>A{refcount:1,is_ref:0,value:1,type:IS_LONG} \/这里Laruence叙述有误,is_ref=1时所有refcount清0 $c>A{refcount:0,is_ref:1,value:1,type:IS_LONG} $b>A{refcount:0,is_ref:1,value:1,type:IS_LONG} \/is_ref=1,将A复制一份给$a $a>B{refcount:1,is_ref:0,value:1,type:IS_LONG} |
||
=& | 常量 | $a=&1; | 语法错误 | |
is_ref=0 refcount=1 |
$b=1; $a=&$b; |
$b>A{refcount:1,is_ref:0,value:1,type:IS_LONG} \/ $b>A{refcount:0,is_ref:1,value:1,type:IS_LONG} $a>A{refcount:0,is_ref:1,value:1,type:IS_LONG} |
||
is_ref=0 refcount>1 |
$c=1; $b=$c; $a=&$b; |
$c>A{refcount:1,is_ref:0,value:1,type:IS_LONG} \/ $c>A{refcount:2,is_ref:0,value:1,type:IS_LONG} $b>A{refcount:2,is_ref:0,value:1,type:IS_LONG} \/refcount>1,将A复制一份给$c $c>A{refcount:1,is_ref:0,value:1,type:IS_LONG} $b>B{refcount:0,is_ref:1,value:1,type:IS_LONG} $a>B{refcount:0,is_ref:1,value:1,type:IS_LONG} |
||
is_ref=1 | $c=1; $b=&$c; $a=&$b; |
$c>A{refcount:1,is_ref:0,value:1,type:IS_LONG} \/ $c>A{refcount:0,is_ref:1,value:1,type:IS_LONG} $b>A{refcount:0,is_ref:1,value:1,type:IS_LONG} \/ $a>A{refcount:0,is_ref:1,value:1,type:IS_LONG} |
1.名字a不存在,赋给一个常量,这时候没办法必须新建
2.$b=&$c,$a=$b,最开始$b和$c捆绑在一起,除非$b重新绑定到其他引用关系上,$b、$c的绑定关系是不会解除的,这时候$a想和$b在一起又不想和$c绑定(如果$a=&$b,那就是a、b、c绑定了),那就只有分出去一个$b的复制品给$a了,希望这样解释比较好理解。在程序中判断条件为赋值行为为=且X的is_ref=1。
3.$b=$c,$a=&$b,这时候$b想和$a绑定在一起,$a又不想$c搀和进来(如果$b=&$c,那就是a、b、c绑定了),那就只有为难$b分出个复制品给$c了。在程序中判断条件为赋值行为为=&且X的refcount>1。
可以看到所有要新建内存的地方都是矛盾不可调和的地方,这也是动态类型语言设计的高明之处,也是所谓的"Copy on write"原则的意思。
在之前我们谈到的$a,$b指向同一内存结构,实际上就是$a,$b名字对应的指针指向统一内存结构,对于如下语句
<?php $a = 1; $b = $a; echo '$a='.$a."\t".'$b='.$b."\n"; $b = 2; echo '$a='.$a."\t".'$b='.$b."\n"; $c = 1; $d = &$c; echo '$c='.$c."\t".'$d='.$d."\n"; $d = 2; echo '$c='.$c."\t".'$d='.$d."\n"; ?>这里采用命令行演示,结果如下
$a=1 $b=1
$a=1 $b=2
$c=1 $d=1
$c=2 $d=2
可以看到:
这里我们用一张图来说明其内存结构和指针指向变化示意图如下这两个例子的唯一不同在于定义$d时加上了一个引用定义符&。为什么$b赋值时指向发生了变化而$d赋值时没有变化,是么时候指针指向才会发生变化呢?答案是同样是出现不可调和的矛盾时会产生指针指向改变。
同样我们观察“$a赋值X”这一语句,赋值可为普通赋值(=)和引用赋值(=&),X可为常量或变量。所有的指针指向改变都是在$a端完成。
这里我就不再列表了,可以同样按照以上方法分析,结论同样是三种情况下指针指向发生改变:
1.名字a不存在,这时候新建了名字没办法必须将指针指向一个地方
2.$b=1,$a=$b,$a=2,最开始$a和$b指向同一内存结构,当$a要指向一个新的值又不希望$c指向这个值,那么只有将$a指针从原来的指向$b内存结构改为指向新建的内存结构。在程序中判断条件为赋值行为为=且$a的refcount>1。
3.$b=1,$c=2,$a=&$b,$a=&$c,这时候$a想和$c绑定在一起,只能绑定一个内存结构,$a只有舍弃$b,将$a的指针从原来指向$b内存结构改为指向$c内存结构。在程序中判断条件为赋值行为为=&且$a的is_ref=1。
可以看到所有要改变指针指向的地方同样是矛盾不可调和的地方,这同样也是动态类型语言设计的高明之处,也是所谓的"Change on write"原则的意思。
实际上所有弱类型的语言机理大概都是如此,只有按照这样的逻辑来设计的动态语言才是最优性能同时不反人类的,下一节深入到PHP的存储结构来讲解PHP的引用,说明PHP引用特别是对象引用容易被错误理解的地方。
原创,转载请注明来自http://blog.csdn.net/wenzhou1219