PHP内核探索之变量-理解引用

一、符号表

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

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

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

image.png

我们并不去关注符号表的具体结构,只需要知道:每个函数、类、命名空间等都有自己的独立的符号表(与全局的符号表分开)。符号表并不是一开始就建立好的,而是随着编译程序的扫描不断添加和更新的。在脚本执行的过程中,全局的符号表几乎是一直存在的,但除了这个全局的global symbol table,实际上还会生成其他的symbol table:例如在函数调用的过程中,Zend会创建该函数的内部symbol table,用于存放函数内部变量的信息,而在函数调用结束后,会删除该symbol table。关于符号表总结如下:
1.符号表记录了程序中符号的name-attribute对,这些信息对于编译和执行是至关重要的。
2.符号表类似一个map或者hashtable
3.符号表不是一开始就建立好的,而是不断添加和更新的过程。
4.活动符号表是一个指针,指向的是当前活动的符号表。

二、引用
1.引用计数

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

为了实现上述机制,需要对zval的引用状态做标记,zval的结构中,refcount__gc便是用于计数的,这个值记录了有多少个变量指向该zval, 在上述赋值操作中,b ,会增加原始的$b的zval的refcount值。

  1. 函数传参

在脚本执行的过程中,全局的符号表几乎是一直存在的,但除了这个全局的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),如下所示:

image.png

(2). 函数调用do_zval_test($a)

由于函数的调用,Zend会为do_zval_test这个函数创建单独的符号表(其中包含该函数内部的符号s),同时,由于s实际上是函数的形参,因此并不会为s创建新的zval,而是指向a的zval。这时,a指向的zval的refcount应该为3(分别是s和函数调用堆栈):
xdebug_debug_zval("a");//查看zval变化命令

a: (refcount=3, is_ref=0)='before func'
  如下图所示:


image.png

(3).函数内部执行$s = "change "

由于$s的值发生了改变,因此会执行zval分离,为s专门copy生成一个新的zval:

image.png

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

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


image.png

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


image.png

3.引用初探

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

$a = "simple test";
$b = &a;
$c = &a;
 
$b = 42;
unset($c);
unset($b);

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

image.png

上图中值得注意的最后一步,在unset($b)之后,zval的is_ref值又变成了0。

那如果是混合了引用(assign-by-reference)和普通赋值(assign-by-value)的脚本,又是什么情况呢?

我们的测试脚本:

(1). 先普通赋值后引用赋值

$a = "src";
$b = $a;
$c = &$b;

具体的过程见下图:

image.png

(2). 先引用赋值后普通赋值

$a = "src";
$b = &$a;
$c = $a;

具体过程见下图:

image.png

4.  传递引用

同样,向函数传递的参数也可以以引用的形式传递,这样可以在函数内部修改变量的值。

function do_zval_test(&$s){
    $s = "after";
    return $s;
}
 
$a = "before";
$b = do_zval_test($a);

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


image.png

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

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

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

5.  引用返回

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'
)
如下图所示:


image.png

(2). find_node(3,&$tree)

由于函数调用,Zend会进入函数的内部,创建该函数的内部symbol table,同时,由于传递的参数是引用参数,因此zval的is_ref被标志为1,而refcount的值增加为3(分别是全局tree,内部tree和函数堆栈):


image.png

(3)item = &tree[$key];

由于item是tree[key]的引用(在本例的调用中,key是3),因而更新tree[key]指向zval的is_ref和refcount值:


image.png

(4)return $item,并执行引用绑定:

image.png

(5)函数返回,销毁局部符号表。

tree对应的zval的is_ref恢复了0,refcount=1,tree[3]被绑定在了node变量上,对该变量的任何改变都会间接更改tree[3]:

image.png

(6) 更改node的值,会反射到tree的节点上,$node ='new':
image.png

Note:为了使用引用返回,必须在函数定义和函数调用的地方都显式的使用&符号。

  1. 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:


image.png

(2).update_var('four')
由于直接传递的是string而不是变量,因而会创建一个zval,该zval的is_ref=0,ref_count=2(分别是形参$value和函数的堆栈),如下所示:

image.png

(3)global $var
  global var这句话,实际上会执行两件事情:
1.在函数内部的符号表中插入局部的var符号
2.建立局部var与全局变量var之间的引用.

image.png

(4)unset($var);
这里要注意的是,unset只是删除函数内部符号表中var符号,而不是删除全局的。同时,更新原zval的refcount值和is_ref引用标志(引用解绑):


image.png

(5).global var
同3,再次建立局部var与全局的var的引用:

image.png

(6)var = value;
  更改var对应的zval的值,由于引用的存在,全局的var的值也随之改变:


image.png

(7)函数返回,销毁局部符号表(又回到最初的起点,但,一切已经大不一样了

image.png

据此,我们可以总结出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:

image.png

(2). foreach(a as &v) {v *= $v;}

这里由于是引用绑定,所以相当于对数组中的元素执行:

v = &a[0];
v = &a[1];
v = &a[2];
执行过程如下:


image.png

我们发现,在这次的foreach执行完毕之后,v = &$a[2].

(3)第二次foreach循环

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

这次因为是普通的assign-by-value的赋值形式,因此,类似与执行:

a[0];
a[1];
a[2];
别忘了v现在是a[2]的引用,因此,赋值的过程会间接更改$a[2]的值。

过程如下:

image.png

因此,输出结果应该为144。你答对了吗?

版权声明:本文为CSDN博主「ohmygirl」的原创文章
原文链接:https://blog.csdn.net/ohmygirl/article/details/41577797

你可能感兴趣的:(PHP内核探索之变量-理解引用)