程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 網頁編程 >> PHP編程 >> 關於PHP編程 >> PHP內核探索之變量(2)-理解引用

PHP內核探索之變量(2)-理解引用

編輯:關於PHP編程

PHP內核探索之變量(2)-理解引用


本文主要內容:

引論符號表與zval引用原理回到最初的問題

一、引論

  很久之前寫了一篇關於引用的文章,當時寫的寥寥草草,很多原理都沒有說清楚。最近在翻閱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。

  以上就是符號表的全部內容了,我們簡單抽離一下其中的關鍵內容:

符號表記錄了程序中符號的name-attribute對,這些信息對於編譯和執行是至關重要的。符號表類似一個map或者hashtable符號表不是一開始就建立好的,而是不斷添加和更新的過程。活動符號表是一個指針,指向的是當前活動的符號表。

  更多的資料可以查看:

  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環境中:<喎?"http://www.Bkjia.com/kf/ware/vc/" target="_blank" class="keylink">vc3Ryb25nPjwvcD4KPHA+IDxpbWcgc3JjPQ=="http://www.2cto.com/uploadfile/Collfiles/20141129/20141129083533169.jpg" alt="\">

  這裡我們順便說一句,在你使用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關鍵字的過程和特性:

函數中聲明global,會在函數內部生成一個局部的變量,並與全局的變量建立引用。函數中對global變量的任何更改操作都會間接更改全局變量的值。函數unset局部變量不會影響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更新過程。

參考文獻:

http://en.wikipedia.org/wiki/Symbol_tablehttp://arantxa.ii.uam.es/~modonnel/Compilers/04_SymbolTablesI.pdfhttp://web.cs.wpi.edu/~kal/courses/cs4533/module5/myst.htmlhttp://www.cs.dartmouth.edu/~mckeeman/cs48/mxcom/doc/TypeInference.pdfhttp://www.cs.cornell.edu/courses/cs412/2008sp/lectures/lec12.pdfhttp://php.net/manual/zh/language.references.return.phphttp://stackoverflow.com/questions/10057671/how-foreach-actually-works

由於寫作匆忙,文中難免會有錯誤之處,歡迎指出探討。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved