背景
php语言的高度封装和五花八门的库使这门语言很容易上手,而且开发效率比C/C++高出许多。但是也正是由于php封装度很高,一些在c语言中很简单的概念,让php这么一封装,就变得难以琢磨。比如引用,在c语言中的概念很简答, 就是两个变量名指向同一块内存。而且引用必须要你手动操作,哪个变量引用的哪块内存在编写代码的时候心里是一清二楚的。但是在php中,有很多地方使用了隐式的引用,而写代码的时候并不知道这是引用。这就很容易造成问题,而且难以发现。就比如下面的代码我没有使用引用啊?但是name的值确实被改变了。这种类型的错误一旦发生,很可能就应验了一句话,“一个小bug,一查一下午”,还不一定能查出来。
输出:
我没有使用引用啊?但是name的值确实被改变了。这种类型的错误一旦发生,很可能就应验了一句话,“一个小bug,一查一下午”,还不一定能查出来。所以这促使我去仔细了解一下php引用的规则,进而在以后的工作中尽量减少这种没有意义的错误。
下面在梳理引用规则的时候,先不会管php的写时复制,尽可能从语义的角度来看待php的引用。下面的讨论仅适用于php5.
php内置类型的引用
当使用php内置类型时,php的引用规则和C语言没有区别。例中测试了int, string, array,是预期的结果。只要函数的形参不是引用类型,就不会被莫名其妙的改变。
输出:
php对象的引用
看下面的例子:
看输出,坑爹的地方来了:
不管函数的形参是引用类型还是非引用类型,obj的name成员都被更改了。
在某篇博客上看到了这样的解释:“PHP 5 引进了独立于变量容器的『对象存储器』。当一个对象赋值给变量时,变量不再存储整个对象(属性表和其他的『类』信息),而是存储这个对象所在 存储器的引用 —— 当我们复制一个对象变量时,我们复制的是这个『存储器的引用』”。把这句话套到例子中就是,$obj现在是一个引用,引用的就是“new Name()”生成的对象存储器。既然知道了$obj只是个引用,那么解释上面例子为什么会这样输出就很容易了:
对于fun函数,当实参赋值给形参的时候,相当于 $arg = $obj。当我们复制一个变量对象时,复制的是这个对象存储器的引用。那么现在的情况就是,$arg也引用刚才"new Name()"生成的对象存储器,这个对象存储器的引用计数是2。所以在fun函数中,我们操作$arg->name=“fun”,本质上就是让这个对象存储器中的name属性编程“fun”。当fun函数返回时,$arg被销毁,此对象存储器引用计数减一。但是$obj仍然引用着这个对象存储器,所以我们 echo $obj->name的时候,输出此对象存储器的name属性,即“fun”。
对于funReference, 当实参赋值给形参时,相当于$arg = &$obj。$arg作为$obj的引用,而$obj作为对象存储器的引用。在解引用的时候,可以简单的认为,$arg->name="funReference"就是在直接操作对象存储器。所以funReference和fun造成了同样额结果。
但是是不是$arg = $obj 和 $arg = &$obj 就完全等价了?事实证明,不是,举个例子:
输出:
可以看出,$obj2=$obj1, $obj2复制的是对象存储器的引用,所以后续就算是$obj指向了其他的结构,$obj2仍然引用的是对象存储器。但是$obj3 = &obj1, $obj3是$obj1的引用,按照C语言的引用规则,当$obj1的指向发生改变时,$obj3也应该指向也会发生改变,使之和$obj1指向相同的内存。php也确实这么去实现了这个语义。所以当obj后来指向“abc”时,$obj3也指向了"abc"。php的引用指向在初始化后,还可以随意改变,真实让人脑壳疼。
另外一个问题,php的对象没法被赋值吗?因为不管怎么弄,我拿到的都只是相当于对象存储器的引用。php开发者也想到这个问题了,我在网上找到了下面的两种办法:
1. 给类实现一个__clone()方法。然后赋值的时候这样:$obj2 = clone $obj1;
2. 使用序列化函数。$obj2 = unserialize(serialize($ojb1));
global,$GLOBALS, 引用
先给个定义:
$GLOBALS['var_name'], 是全局变量$var本身
global $val是全局变量的同名引用
这两句话让人感觉有点云里雾里,举个例子就清楚了:
输出:
可以看到,var1并没有被改变,而var2则被改变了。
先解释var1, 开头说了,global var 是 全局变量 var 的同名引用。也就是说,在fun()内部,var1只是全局变量var1的引用而已。看看fun()中做了什么?$var1=&$str, 让var1重新引用局部变量str。这个操作只是让fun内部的同名引用var1所引用的对象变了,和全局的var1一点关系都没有,所以var1不会改变。
$GLOBALS的官方定义是,由所有已定义的全局变量所组成。$GLOBALS["var_name"]就是全局变量var_name本身。套到例子中,$GLOBALS["var2"]也就是全局变量var2本身,让全局变量引用fun中的局部变量str,所以var2的值就发生了改变。
这两个概念一定要区分清楚,否则在平时编码中,就会发生想改变的全局变量没有被改变,但是不想被改变的全局变量反而被意外改变。而且这种bug还真不好找。