PHP學習之變數:理解引用。
php的引用(就是在變數或函數、物件等前面加上&符號) //最重要就是刪除引用的變量,只是引用的變數存取不了,但是內容並沒有銷毀在PHP 中引用的意思是:不同的名字訪問同一個變數內容.
本文主要內容:
一、引論
很久之前寫了一篇關於引用的文章,當時寫的寥寥草草,很多原理都沒有說清楚。最近在翻閱Derick Rethans(home: http://derickrethans.nl/ Github: https://github.com/derickr)大牛之前所做的報告時,發現了一篇講解PHP引用機制的文章,也就是這個PDF.文中從zval和符號表的角度講解了引用計數、引用傳參、引用返回、全局參數等的原理,洋洋灑灑,圖文並茂,甚是精彩,建議童鞋們有時間都讀讀原版,相信會有不少的收穫。
廢話不多說,接著說今天的正題。
我們知道,很多語言都提供了引用的機制,引用可以讓我們使用不同的名字(或符號)來存取相同的內容。 PHP手冊中對引用的定義是:"在PHP中引用意味著用不同的名字存取同一個變數內容。這並不像C的指針,替代的是,引用是符號表別名。",換句話說,引用實現了某種形式的"綁定"。例如我們常碰到的這類面試題,便是引用的典範:
$a = array(1,2,3,4); foreach($a as &$v){ $v *= $v; } foreach($a as $v){ echo $v; }
拋開本題的輸出不談,我們今天就跟隨Derick Rethans前輩的腳步,一步一步去揭開引用的神秘面紗。
二、 符號表和zval
在開始引用的原理之前,我們有必要對於文中反覆出現的術語做個簡單的說明,其中最主要也最重要的便是: 1.符號表 2.zval.
1. 符號表
電腦語言是人與機器溝通的工具,但不幸的是,我們賴以生存和引以為傲的高階語言卻無法直接在電腦上執行,因為電腦只能理解某種形式的機器語言。這意味著,高階語言必須經過編譯(或解釋)過程才能被電腦理解和執行。在這其間,要經過詞法分析、語法分析、語義分析、中間代碼生成和優化等很多複雜的過程,而這些過程中,編譯程序可能要反復用到源程序中出現的標識符等信息(例如變量的類型檢查、語意分析階段的語意檢查),這些資訊便是保存在不同的符號表中的。符號表保存了來源程式中識別符的名字和屬性訊息,這些資訊可能包括:類型、儲存類型、作用域、儲存分配資訊和其他一些額外資訊等。為了有效率的插入和查詢符號表項,許多編譯器的符號表都使用Hashtable來實作。我們可以簡單的理解為:符號表就是一個保存了符號名稱和該符號的各類別屬性的hashtable或者map。例如,對於程式:
$str = 'this is a test'; function foo( $a, $b ){ $tmp = 12; return $tmp + $a + $b; } function to(){ }
一個可能的符號表(並非實際的符號表)是類似這樣的結構:
我們不去關注符號表的特定結構,只需要知道:每個函數、類別、命名空間等都有自己的獨立的符號表(與全域的符號表分開)。說到這裡,突然想起來一件事情,最開始使用PHP編程的時候,在讀extract()函數的手冊時,對於"從數組中將變數導入到當前的符號表"這句話的意義百思不得其解,更是對前輩們所說的"不建議使用extract($_POST)和extract($_GET)提取變數"的建議萬分苦惱。實際上,extract的濫用不僅會有嚴重的安全性問題,而且會污染目前的符號表( active symbol table)。
那麼active symbol table又是什麼東西呢?
我们知道,PHP代码的执行过程中,几乎都是从全局作用域开始,依次扫描,顺序执行。如果遇到函数调用,则进入该函数的内部执行,该函数执行完毕之后会返回到调用程序继续执行。这意味着,必须要有某种机制用于区分不同阶段所要使用的符号表,否则就会造成编译和执行的错乱。Active symbol table便是用于标志当前活动的符号表(这时应该至少存在着全局的global symbol table和活动的active symbol table,通常情况下,active symbol table就是指global symbol table)。符号表并不是一开始就建立好的,而是随着编译程序的扫描不断添加和更新的。在进入函数调用时,zend(PHP的语言解释引擎)会创建该函数的符号表,并将active symbol table指向该符号表。也就是说,在任意时刻使用的的符号表都应该是当前的active symbol table。
以上就是符号表的全部内容了,我们简单抽离一下其中的关键内容:
更多的资料可以查看:
1. http://www.scs.stanford.edu/11wi-cs140/pintos/specs/sysv-abi-update.html/ch4.symtab.html
2. http://arantxa.ii.uam.es/~modonnel/Compilers/04_SymbolTablesI.pdf
2. Zval
在上一篇博客(PHP内核探索之变量(1)Zval)中,我们已经对zval的结构和基本原理有了一些了解。对zval不了解的童鞋可以先看看。为了方便阅读,我们再次贴出zval的结构:
<span>struct _zval_struct { zvalue_value value; </span><span>/*</span><span> value </span><span>*/</span><span> zend_uint refcount__gc; </span><span>/*</span><span> variable ref count </span><span>*/</span><span> zend_uchar type; </span><span>/*</span><span> active type </span><span>*/</span><span> zend_uchar is_ref__gc; </span><span>/*</span><span> if it is a ref variable </span><span>*/</span><span> }; typedef struct _zval_struct zval;</span>
三、引用
1. 引用计数
正如上节所言,zval是PHP变量底层的真正容器,为了节省空间,并不是每个变量都有自己独立的zval容器,例如对于赋值(assign-by-value)操作:$a = $b(假设$b,$a都不是引用型变量),Zend并不会为$b变量开辟新的空间,而是将符号表中a符号和b符号指向同一个zval。只有在其中一个变量发生变化时,才会执行zval分离的操作。这被称为COW(Copy-on-write)的机制,可以在一定程度上节省内存和提高效率。
为了实现上述机制,需要对zval的引用状态做标记,zval的结构中,refcount__gc便是用于计数的,这个值记录了有多少个变量指向该zval, 在上述赋值操作中,$a=$b ,会增加原始的$b的zval的refcount值。关于这一点,上次(PHP内核探索之变量(1)Zval)已经做了详细的解释,这里不再赘述。
2. 函数传参
在脚本执行的过程中,全局的符号表几乎是一直存在的,但除了这个全局的global symbol table,实际上还会生成其他的symbol table:例如函数调用的过程中,Zend会创建该函数的内部symbol table,用于存放函数内部变量的信息,而在函数调用结束后,会删除该symbol table。我们接下来以一个简单的函数调用为例,介绍一下在传参的过程中,变量和zval的状态变化,我们使用的测试脚本是:
function do_zval_test($s){ $s = "change "; return $s; } $a = "before"; $b = do_zval_test($a);
我们来逐步分析:
(1). $a = "before";
这会为$a变量开辟一个新的zval(refcount=1,is_ref=0),如下所示:
(2). 函数调用do_zval_test($a)
由于函数的调用,Zend会为do_zval_test这个函数创建单独的符号表(其中包含该函数内部的符号s),同时,由于$s实际上是函数的形参,因此并不会为$s创建新的zval,而是指向$a的zval。这时,$a指向的zval的refcount应该为3(分别是$a,$s和函数调用堆栈):
a: (refcount=3, is_ref=0)='before func'
如下图所示:
(3).函数内部执行$s = "change "
由于$s的值发生了改变,因此会执行zval分离,为s专门copy生成一个新的zval:
(4).函数返回 return $s ; $b = do_zval_test($a).
$b与$s共享zval(暂时),准备销毁函数中的符号表:
(5). 销毁函数中的符号表,回到Global环境中:
这里我们顺便说一句,在你使用debug_zval_dump()等函数查看zval的refcount时,会令zval本身的refcount值加1,所以实际的refcount的值应该是打印出的refcount减1,如下所示:
$src = "string"; debug_zval_dump($src);
结果是:
<span>string</span>(6) "string" refcount(2)
3. 引用初探
同上,我们还是直接上代码,然后一步步分析(这个例子比较简单,为了完整性,我们还是稍微分析一下):
$a = "simple test"; $b = &a; $c = &a; $b = 42; unset($c); unset($b);
则变量与zval的对应关系如下图所示:(由此可见,unset的作用仅仅是将变量从符号表中删除,并减少对应zval的refcount值)
上图中值得注意的最后一步,在unset($b)之后,zval的is_ref值又变成了0。
那如果是混合了引用(assign-by-reference)和普通赋值(assign-by-value)的脚本,又是什么情况呢?
我们的测试脚本:
(1). 先普通赋值后引用赋值
$a = "src"; $b = $a; $c = &$b;
具体的过程见下图:
(2). 先引用赋值后普通赋值
$a = "src"; $b = &$a; $c = $a;
具体过程见下图:
4. 传递引用
同样,向函数传递的参数也可以以引用的形式传递,这样可以在函数内部修改变量的值。作为实例,我们仍使用2(函数传参)中的脚本,只是参数改为引用的形式:
function do_zval_test(&$s){ $s = "after"; return $s; } $a = "before"; $b = do_zval_test($a);
这与上述函数传参过程基本一致,不同的是,引用的传递使得$a的值发生了变化。而且,在函数调用结束之后 $a的is_ref恢复成0:
可以看出,与普通的值传递相比,引用传递的不同在于:
(1) 第3步 $s = "change";时,并没有为$s新建一个zval,而是与$a指向同一个zval,这个zval的is_ref=1。
(2) 还是第3步。$s = "change";执行后,由于zval的is_ref=1,因此,间接的改变了$a的值
5. 引用返回
PHP支持的另一个特性是引用返回。我们知道,在C/C++中,函数返回值时,实际上会生成一个值的副本,而在引用返回时,并不会生成副本,这种引用返回的方式可以在一定程度上节省内存和提高效率。而在PHP中,情况并不完全是这样。那么,究竟什么是引用返回呢?PHP手册上是这么说的:"引用返回用在当想用函数找到引用应该被绑定在哪一个变量上面时",是不是一头雾水,完全不知所云?其实,英文手册上是这样描述的"Returning by reference is useful when you want to use a function to find to which variable a reference should be bound"。提取文中的主干和关键点,我们可以得到这样的信息:
(1). 引用返回是将引用绑定在一个变量上。
(2). 这个变量不是确定的,而是通过函数得到的(否者我们就可以使用普通的引用了)。
这其实也说明了引用返回的局限性:函数必须返回一个变量,而不能是一个表达式,否者就会出现类似下面的问题:
PHP Notice: Only variable references should be returned by reference in xxx(参看PHP手册中的Note).
那么,引用返回时如何工作的呢?例如,对于如下的例子:
function &find_node($key,&$tree){ $item = &$tree[$key]; return $item; } $tree = array(1=>'one',2=>'two',3=>'three'); $node =& find_node(3,$tree); $node ='new';
Zend都做了哪些工作呢?我们一步步来看。
(1). $tree = array(1=>'one',2=>'two',3=>'three')
同之前一样,这会在Global symbol table中添加tree这个symbol,并生成该变量的zval。同时,为数组$tree的每个元素都生成相应的zval:
tree: (refcount=1, is_ref=0)=<span>array ( </span>1 => (refcount=1, is_ref=0)='one', 2 => (refcount=1, is_ref=0)='two', 3 => (refcount=1, is_ref=0)=<span>'three' )</span>
如下图所示:
(2). find_node(3,&$tree)
由于函数调用,Zend会进入函数的内部,创建该函数的内部symbol table,同时,由于传递的参数是引用参数,因此zval的is_ref被标志为1,而refcount的值增加为3(分别是全局tree,内部tree和函数堆栈):
(3)$item = &$tree[$key];
由于item是$tree[$key]的引用(在本例的调用中,$key是3),因而更新$tree[$key]指向zval的is_ref和refcount值:
(4)return $item,并执行引用绑定:
(5)函数返回,销毁局部符号表。
tree对应的zval的is_ref恢复了0,refcount=1,$tree[3]被绑定在了$node变量上,对该变量的任何改变都会间接更改$tree[3]:
(6) 更改$node的值,会反射到$tree的节点上,$node ='new':
Note:为了使用引用返回,必须在函数定义和函数调用的地方都显式的使用&符号。
6. Global关键字
PHP中允许我们在函数内部使用Global关键字引用全局变量(不加global关键字时引用的是函数的局部变量),例如:
$var = "outside"; function inside() { $var = "inside"; echo $var; global $var; echo $var; } inside();
输出为insideoutside
我们只知道global关键字建立了一个局部变量和全局变量的绑定,那么具体机制是什么呢?
使用如下的脚本测试:
$var = "one"; function update_var($value){ global $var; unset($var); global $var; $var = $value; } update_var('four'); echo $var;
具体的分析过程为:
(1).$var = 'one';
同之前一样,这会在全局的symbol table中添加var符号,并创建相应的zval:
(2).update_var('four')
由于直接传递的是string而不是变量,因而会创建一个zval,该zval的is_ref=0,ref_count=2(分别是形参$value和函数的堆栈),如下所示:
(3)global $var
global $var这句话,实际上会执行两件事情:
(1).在函数内部的符号表中插入局部的var符号
(2).建立局部$var与全局变量$var之间的引用.
(4)unset($var);
这里要注意的是,unset只是删除函数内部符号表中var符号,而不是删除全局的。同时,更新原zval的refcount值和is_ref引用标志(引用解绑):
(5).global $var
同3,再次建立局部$var与全局的$var的引用:
(6)$var = $value;
更改$var对应的zval的值,由于引用的存在,全局的$var的值也随之改变:
(7)函数返回,销毁局部符号表(又回到最初的起点,但,一切已经大不一样了):
据此,我们可以总结出global关键字的过程和特性:
四、回到最初的问题
现在,我们对引用已经有了一个基本的认识。让我们回到最初的问题:
$a = array(1,2,3); foreach($a as &$v){ $v *= $v; } foreach($a as $v){ echo $v; }
这之中,究竟发生了什么事情呢?
(1).$a = array(1,2,3);
这会在全局的symbol table中生成$a的zval并且为每个元素也生成相应的zval:
(2). foreach($a as &$v) {$v *= $v;}
这里由于是引用绑定,所以相当于对数组中的元素执行:
$v = &$a<span>[</span><span>0</span><span>]</span><span>; </span>$v = &$a<span>[</span><span>1</span><span>]</span><span>; </span>$v = &$a<span>[</span><span>2</span><span>]</span><span>;</span>
执行过程如下:
我们发现,在这次的foreach执行完毕之后,$v = &$a[2].
(3)第二次foreach循环
foreach($a as $v){ echo $v; }
这次因为是普通的assign-by-value的赋值形式,因此,类似与执行:
$v = $a<span>[</span><span>0</span><span>]</span><span>; </span>$v = $a<span>[</span><span>1</span><span>]</span><span>; </span>$v = $a<span>[</span><span>2</span><span>]</span><span>;</span>
别忘了$v现在是$a[2]的引用,因此,赋值的过程会间接更改$a[2]的值。
过程如下:
因此,输出结果应该为144.
附:本文中的zval的调试方法。
如果要查看某一过程中zval的变化,最好的办法是在该过程的前后均加上调试代码。例如
$a = 123; xdebug_debug_zval('a'); $b=&$a; xdebug_debug_zval('a');
配合画图,可以得到一个直观的zval更新过程。
以上就介绍了PHP学习之变量:理解引用,包括了PHP变量引用方面的内容,希望对PHP教程有兴趣的朋友有所帮助。