[置顶] PHP内核探索之变量(2)-理解引用

本文主要内容:

  1. 引论
  2. 符号表与zval
  3. 援用原理
  4. 回到最初的问题

1、引论

  很久之前写了1篇关于援用的文章,当时写的寥寥草草,很多原理都没有说清楚。最近在翻阅Derick Rethans(home:
http://derickrethans.nl/ Github:
https://github.com/derickr)大牛之前做的报告时,发现了1篇讲授PHP援用机制的文章,也就是这个PDF.文中从zval和符号表的角度讲授了援用计数、援用传参、援用返回、全局参数等的原理,洋洋洒洒,图文并茂,甚是精彩,建议童鞋们有时间都读读原版,相信会有很多的收获。

  空话不多说,接着说今天的正题。

  我们知道,很多语言都提供了援用的机制,援用可让我们使用不同的名字(或符号)访问一样的内容。PHP手册中对援用的定义是:"在PHP中援用意味着用不同的名字访问同1个变量内容。这其实不像C的指针,替换的是,援用是符号表别名。",换句话说,援用实现了某种情势的"绑定"。例如我们常常碰到的这类面试题,便是援用的典范:

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

foreach($a as $v){
echo $v;
}

  抛开本题的输出不谈,我们今天就跟随Derick Rethans先辈的脚步,1步1步去揭开援用的神秘面纱。

2、  符号表和zval

  在开始援用的原理之前,我们有必要对文中反复出现的术语做个简单的说明,其中最主要也最重要的便是: 1.符号表 2.zval.

1.   符号表

  计算机语言是人与机器交换的工具,但不幸的是,我们赖以生存和引以为傲的高级语言却没法直接在计算机上履行,由于计算机只能理解某种情势的机器语言。这意味着,高级语言必须要经过编译(或解释)进程才能被计算机理解和履行。在这其间,要经过词法分析、语法分析、语义分析、中间代码生成和优化等很多复杂的进程,而这些进程中,编译程序可能要反复用到源程序中出现的标识符等信息(例如变量的类型检查、语义分析阶段的语义检查),这些信息便是保存在不同的符号表中的。符号表保存了源程序中标识符的名字和属性信息,这些信息可能包括:类型、存储类型、作用域、存储分配信息和其他1些额外信息等。为了高效的插入和查询符号表项,很多编译器的符号表都使用Hashtable来实现。我们可以简单的理解为:符号表就是1个保存了符号名和该符号的各类属性的hashtable或map。例如,对程序:

$str = 'this is a test';

function foo( $a, $b ){
$tmp = 12;
return $tmp + $a + $b;
}

function to(){

}

1个可能的符号表(并不是实际的符号表)是类似这样的结构:

 

  我们其实不去关注符号表的具体结构,只需要知道:每一个函数、类、命名空间等都有自己的独立的符号表(与全局的符号表分开)。说到这里,突然想起来1件事情,最开始使用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)。符号表其实不是1开始就建立好的,而是随着编译程序的扫描不断添加和更新的。在进入函数调用时,zend(PHP的语言解释引擎)会创建该函数的符号表,并将active symbol table指向该符号表。也就是说,在任意时刻使用的的符号表都应当是当前的active symbol table。

  以上就是符号表的全部内容了,我们简单抽离1下其中的关键内容:

  1. 符号表记录了程序中符号的name-attribute对,这些信息对编译和履行是相当重要的。
  2. 符号表类似1个map或hashtable
  3. 符号表不是1开始就建立好的,而是不断添加和更新的进程。
  4. 活动符号表是1个指针,指向的是当前活动的符号表。

  更多的资料可以查看:

  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

  在上1篇博客(PHP内核探索之变量(1)Zval)中,我们已对zval的结构和基本原理有了1些了解。对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;

3、援用

1.  援用计数

  正如上节所言,zval是PHP变量底层的真正容器,为了节省空间,其实不是每一个变量都有自己独立的zval容器,例如对赋值(assign-by-value)操作:$a = $b(假定$b,$a都不是援用型变量),Zend其实不会为$b变量开辟新的空间,而是将符号表中a符号和b符号指向同1个zval。只有在其中1个变量产生变化时,才会履行zval分离的操作。这被称为COW(Copy-on-write)的机制,可以在1定程度上节省内存和提高效力。

  为了实现上述机制,需要对zval的援用状态做标记,zval的结构中,refcount__gc便是用于计数的,这个值记录了有多少个变量指向该zval, 在上述赋值操作中,$a=$b ,会增加原始的$b的zval的refcount值。关于这1点,上次(PHP内核探索之变量(1)Zval)已做了详细的解释,这里不再赘述。

2.         函数传参

  在脚本履行的进程中,全局的符号表几近是1直存在的,但除这个全局的global symbol table,实际上还会生成其他的symbol table:例如函数调用的进程中,Zend会创建该函数的内部symbol table,用于寄存函数内部变量的信息,而在函数调用结束后,会删除该symbol table。我们接下来以1个简单的函数调用为例,介绍1下在传参的进程中,变量和zval的状态变化,我们使用的测试脚本是:

function do_zval_test($s){
$s = "change ";
return $s;
}

$a = "before";
$b = do_zval_test($a);

我们来逐渐分析:

(1).   $a = "before";

  这会为$a变量开辟1个新的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生成1个新的zval:

 

(4).函数返回 return $s ; $b = do_zval_test($a).

  $b与$s同享zval(暂时),准备烧毁函数中的符号表:

 

(5).   烧毁函数中的符号表,回到Global环境中:

 

  这里我们顺便说1句,在你使用debug_zval_dump()等函数查看zval的refcount时,会令zval本身的refcount值加1,所以实际的refcount的值应当是打印出的refcount减1,以下所示:

$src = "string";
debug_zval_dump($src);

结果是:

string(6) "string" refcount(2)

3.         援用初探

同上,我们还是直接上代码,然后1步步分析(这个例子比较简单,为了完全性,我们还是略微分析1下):

$a = "simple test";
$b = &a;
$c = &a;

$b = 42;
unset($c);
unset($b);

则变量与zval的对应关系以下图所示:(因而可知,unset的作用仅仅是将变量从符号表中删除,并减少对应zval的refcount值)

 

上图中值得注意的最后1步,在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);

这与上述函数传参进程基本1致,不同的是,援用的传递使得$a的值产生了变化。而且,在函数调用结束以后 $a的is_ref恢复成0:

 

可以看出,与普通的值传递相比,援用传递的不同在于:

(1)     第3步 $s = "change";时,并没有为$s新建1个zval,而是与$a指向同1个zval,这个zval的is_ref=1。

(2)     还是第3步。$s = "change";履行后,由于zval的is_ref=1,因此,间接的改变了$a的值

5.  援用返回

  PHP支持的另外一个特性是援用返回。我们知道,在C/C++中,函数返回值时,实际上会生成1个值的副本,而在援用返回时,其实不会生成副本,这类援用返回的方式可以在1定程度上节省内存和提高效力。而在PHP中,情况其实不完全是这样。那末,究竟甚么是援用返回呢?PHP手册上是这么说的:"援用返回用在当想用函数找到援用应当被绑定在哪个变量上面时",是否是1头雾水,完全不知所云?其实,英文手册上是这样描写的"Returning
by reference is useful when you want to use a function to find to which variable a reference should be bound
"。提取文中的主干和关键点,我们可以得到这样的信息:

(1).       援用返回是将援用绑定在1个变量上。

(2).       这个变量不是肯定的,而是通过函数得到的(否者我们就能够使用普通的援用了)。

这其实也说明了援用返回的局限性:函数必须返回1个变量,而不能是1个表达式,否者就会出现类似下面的问题:

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步步来看。

(1).    $tree = array(1=>’one’,2=>’two’,3=>’three’)

同之前1样,这会在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关键字建立了1个局部变量和全局变量的绑定,那末具体机制是甚么呢?

使用以下的脚本测试:

$var = "one";
function update_var($value){
global $var;
unset($var);
global $var;
$var = $value;
}

update_var('four');
echo $var;

具体的分析进程为:

(1).$var = ‘one’;

     同之前1样,这会在全局的symbol table中添加var符号,并创建相应的zval:

 

(2).update_var(‘four’)

     由于直接传递的是string而不是变量,因此会创建1个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)函数返回,烧毁局部符号表(又回到最初的出发点,但,1切已大不1样了):

 

据此,我们可以总结出global关键字的进程和特性:

  1. 函数中声明global,会在函数内部生成1个局部的变量,并与全局的变量建立援用
  2. 函数中对global变量的任何更改操作都会间接更改全局变量的值。
  3. 函数unset局部变量不会影响global,而只是消除与全局变量的绑定。

4、回到最初的问题

现在,我们对援用已有了1个基本的认识。让我们回到最初的问题:

$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)第2次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的调试方法。

如果要查看某1进程中zval的变化,最好的办法是在该进程的前后均加上调试代码。例如

$a = 123;
xdebug_debug_zval('a');
$b=&$a;
xdebug_debug_zval('a');

配合画图,可以得到1个直观的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

由于写作匆忙,文中难免会有毛病的地方,欢迎指出探讨。

波比源码 – 精品源码模版分享 | www.bobi11.com
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是赞助,收取费用仅维持本站的日常运营所需!
7. 本站源码并不保证全部能正常使用,仅供有技术基础的人学习研究,请谨慎下载
8. 如遇到加密压缩包,请使用WINRAR解压,如遇到无法解压的请联系管理员!

波比源码 » [置顶] PHP内核探索之变量(2)-理解引用

1 评论

  1. After all, what a great site and informative posts, I will upload inbound link – bookmark this web site? Regards, Reader. Metropol Halı Karaca Halı Öztekin ve Selçuklu Halı Cami Halısı ve Cami Halıları Türkiye’nin En Büyük Cami Halısı Fabrikasıyız…

发表评论

Hi, 如果你对这款模板有疑问,可以跟我联系哦!

联系站长
赞助VIP 享更多特权,建议使用 QQ 登录
喜欢我嘛?喜欢就按“ctrl+D”收藏我吧!♡