本文主要內容:php
好久以前寫了一篇關於引用的文章,當時寫的寥寥草草,不少原理都沒有說清楚。最近在翻閱Derick Rethans(home: http://derickrethans.nl/ Github: https://github.com/derickr)大牛以前作的報告時,發現了一篇講解PHP引用機制的文章,也就是這個PDF.文中從zval和符號表的角度講解了引用計數、引用傳參、引用返回、全局參數等的原理,洋洋灑灑,圖文並茂,甚是精彩,建議童鞋們有時間都讀讀原版,相信會有很多的收穫。html
廢話很少說,接着說今天的正題。node
咱們知道,不少語言都提供了引用的機制,引用可讓咱們使用不一樣的名字(或符號)訪問一樣的內容。PHP手冊中對引用的定義是:"在PHP中引用意味着用不一樣的名字訪問同一個變量內容。這並不像C的指針,替代的是,引用是符號表別名。",換句話說,引用實現了某種形式的"綁定"。例如咱們常常碰到的這類面試題,即是引用的典範:git
$a = array(1,2,3,4); foreach($a as &$v){ $v *= $v; } foreach($a as $v){ echo $v; }
拋開本題的輸出不談,咱們今天就跟隨Derick Rethans前輩的腳步,一步一步去揭開引用的神祕面紗。github
在開始引用的原理以前,咱們有必要對於文中反覆出現的術語作個簡單的說明,其中最主要也最重要的即是: 1.符號表 2.zval.web
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的結構:
struct _zval_struct { zvalue_value value; /* value */ zend_uint refcount__gc; /* variable ref count */ zend_uchar type; /* active type */ zend_uchar is_ref__gc; /* if it is a ref variable */ }; typedef struct _zval_struct zval;
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);
結果是:
string(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)=array ( 1 => (refcount=1, is_ref=0)='one', 2 => (refcount=1, is_ref=0)='two', 3 => (refcount=1, is_ref=0)='three' )
以下圖所示:
(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[0]; $v = &$a[1]; $v = &$a[2];
執行過程以下:
咱們發現,在此次的foreach執行完畢以後,$v = &$a[2].
(3)第二次foreach循環
foreach($a as $v){ echo $v; }
此次由於是普通的assign-by-value的賦值形式,所以,相似與執行:
$v = $a[0]; $v = $a[1]; $v = $a[2];
別忘了$v如今是$a[2]的引用,所以,賦值的過程會間接更改$a[2]的值。
過程以下:
所以,輸出結果應該爲144.
附:本文中的zval的調試方法。
若是要查看某一過程當中zval的變化,最好的辦法是在該過程的先後均加上調試代碼。例如
$a = 123; xdebug_debug_zval('a'); $b=&$a; xdebug_debug_zval('a');
配合畫圖,能夠獲得一個直觀的zval更新過程。
參考文獻:
因爲寫做匆忙,文中不免會有錯誤之處,歡迎指出探討。