ホームページ  >  記事  >  バックエンド開発  >  PHP カーネルで調べる変数 (2) - 参照を理解する

PHP カーネルで調べる変数 (2) - 参照を理解する

WBOY
WBOYオリジナル
2016-06-23 13:44:22734ブラウズ

この記事の主な内容:

  1. はじめに
  2. シンボルテーブルとzval
  3. 引用の原則
  4. 元の質問に戻る

1. 引用についての記事を昔書きました。非常に大雑把に書かれており、多くの原則が明確に説明されていません。最近、Derick Rethans (ホーム: http://derickrethans.nl/ Github: https://github.com/derickr) によって作成された以前のレポートを調べていたときに、PHP 参照メカニズムについて説明した記事を見つけました。この記事は、zval とシンボル テーブルの観点から、参照カウント、参照の受け渡し、参照の戻り、グローバル パラメーターなどの原理を説明しており、非常に雄弁で、図解が豊富で、非常に刺激的です。時間があれば、子供たちにオリジナル版を読むことをお勧めします。少しでも得るものがたくさんあると思います。

早速、今日の本題に入りましょう。

多くの言語が、異なる名前(またはシンボル)を使用して同じコンテンツにアクセスできるようにする参照メカニズムを提供していることはわかっています。 PHP マニュアルでの参照の定義は次のとおりです。「PHP における参照とは、同じ変数の内容に別の名前でアクセスすることを意味します。これは C のポインタとは異なります。代わりに、参照はシンボル テーブルのエイリアスです。」言い換えれば、参照 何らかの形式の「バインディング」が実装されています。たとえば、私たちがよく遭遇するこのタイプのインタビュー質問は引用のモデルです:

$a = array(1,2,3,4);foreach($a as &$v){     $v *= $v;} foreach($a as $v){     echo $v;}

この質問の出力はさておき、今日は先輩 Derick Rethans の足跡をたどり、引用を段階的に明らかにしていきます。謎の一歩。

2. シンボルテーブルと zval

原則の引用を始める前に、記事内で繰り返し登場する用語について簡単に説明する必要があります。 最も重要で重要なものは次のとおりです。 1. シンボルテーブル 2. zval。

1. シンボルテーブル

コンピューター言語は人間と機械の間のコミュニケーションツールですが、残念ながら、私たちが依存し誇りに思っている高級言語はコンピューター上で直接実行することができません。コンピュータは何らかの形式の機械語しか理解できません。これは、高級言語がコンピューターによって理解され実行される前に、コンパイル (または解釈) プロセスを経る必要があることを意味します。その際、字句解析、構文解析、意味解析、中間コード生成、最適化など多くの複雑な処理が必要となり、コンパイラはソースプログラム中に出現する識別子(変数など)などの情報を繰り返し利用することがあります。 . 型チェック、意味解析段階での意味チェック)、この情報はさまざまなシンボル テーブルに保存されます。シンボル テーブルには、ソース プログラム内の識別子の名前と属性情報が格納されます。この情報には、タイプ、ストレージ タイプ、スコープ、ストレージ割り当て情報、その他の追加情報が含まれます。シンボル テーブル エントリを効率的に挿入およびクエリするために、多くのコンパイラ シンボル テーブルは Hashtable を使用して実装されています。これは単純に次のように理解できます。シンボル テーブルは、シンボル名とシンボルのさまざまな属性を保存するハッシュテーブルまたはマップです。たとえば、プログラムの場合:

$str = 'this is a test'; function foo( $a, $b ){    $tmp = 12;    return $tmp + $a + $b;}  function to(){ }

可能な シンボル テーブル (実際のシンボル テーブルではない) は、次のような構造です:

の特定の構造には注意を払いません。シンボル テーブルのみ知っておく必要があります。各関数、クラス、名前空間などには、独自の独立したシンボル テーブル (グローバル シンボル テーブルとは別) があります。そういえば、PHPでプログラミングを始めたばかりの頃、extract()関数のマニュアルを読んでいて、「配列から現​​在のシンボルテーブルに変数をインポートする」という文の意味がよくわからなかったのを思い出しました。答えは、「変数の抽出にextract($_POST)やextract($_GET)を使うのは推奨されない」という先人たちのアドバイスに非常に悩んでいるからです。実際、抽出の乱用は重大なセキュリティ問題を引き起こすだけでなく、現在のシンボル テーブル (アクティブ シンボル テーブル) を汚染します。

それでは、アクティブシンボルテーブルとは何でしょうか?

PHP コードの実行中、ほとんどの場合、グローバル スコープから開始され、順次スキャンされ、順次実行されることがわかっています。関数呼び出しが発生した場合、関数の内部実行が開始され、関数の実行後、呼び出し元のプログラムに戻って実行が継続されます。これは、さまざまなステージで使用されるシンボル テーブルを区別する何らかのメカニズムが必要であることを意味します。そうでないと、コンパイルと実行で混乱が生じます。アクティブ シンボル テーブルは、現在のアクティビティをマークするために使用されるシンボル テーブルです (この時点では、少なくともグローバル グローバル シンボル テーブルとアクティブなアクティブ シンボル テーブルが必要です。通常の状況では、アクティブ シンボル テーブルはグローバル シンボル テーブルを参照します)。 。シンボル テーブルは最初から確立されるのではなく、コンパイラがスキャンするにつれて継続的に追加および更新されます。関数呼び出しを開始すると、zend (PHP の言語解釈エンジン) は関数のシンボル テーブルを作成し、アクティブなシンボル テーブルがそのシンボル テーブルを指すようにします。言い換えれば、いつでも使用されるシンボル テーブルは、現在アクティブなシンボル テーブルでなければなりません。

上記はシンボルテーブルの全内容です。主要な内容を簡単に抽出してみましょう。
  1. 符号表记录了程序中符号的name-attribute对,这些信息对于编译和执行是至关重要的。
  2. 符号表类似一个map或者hashtable
  3. 符号表不是一开始就建立好的,而是不断添加和更新的过程。
  4. 活动符号表是一个指针,指向的是当前活动的符号表。

  更多的资料可以查看:

  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关键字的过程和特性:

  1. 函数中声明global,会在函数内部生成一个局部的变量,并与全局的变量建立引用
  2. 函数中对global变量的任何更改操作都会间接更改全局变量的值。
  3. 函数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更新过程。

参考文献:

  1. http://en.wikipedia.org/wiki/Symbol_table
  2. http://arantxa.ii.uam.es/~modonnel/Compilers/04_SymbolTablesI.pdf
  3. http://web.cs.wpi.edu/~kal/courses/cs4533/module5/myst.html
  4. http://www.cs.dartmouth.edu/~mckeeman/cs48/mxcom/doc/TypeInference.pdf
  5. http://www.cs.cornell.edu/courses/cs412/2008sp/lectures/lec12.pdf
  6. http://php.net/manual/zh/language.references.return.php
  7. http://stackoverflow.com/questions/10057671/how-foreach-actually-works

由于写作匆忙,文中难免会有错误之处,欢迎指出探讨。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。