一、符号表
计算机语言是人与机器交流的工具,但不幸的是,我们赖以生存和引以为傲的高级语言却无法直接在计算机上执行,因为计算机只能理解某种形式的机器语言。这意味着,高级语言必须要经过编译(或解释)过程才能被计算机理解和执行。在这其间,要经过词法分析、语法分析、语义分析、中间代码生成和优化等很多复杂的过程,而这些过程中,编译程序可能要反复用到源程序中出现的标识符等信息(例如变量的类型检查、语义分析阶段的语义检查),这些信息便是保存在不同的符号表中的。符号表保存了源程序中标识符的名字和属性信息,这些信息可能包括:类型、存储类型、作用域、存储分配信息和其他一些额外信息等。为了高效的插入和查询符号表项,很多编译器的符号表都使用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值。
- 函数传参
在脚本执行的过程中,全局的符号表几乎是一直存在的,但除了这个全局的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.png4. 传递引用
同样,向函数传递的参数也可以以引用的形式传递,这样可以在函数内部修改变量的值。
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]:
(6) 更改node的值,会反射到tree的节点上,$node ='new':
image.png
Note:为了使用引用返回,必须在函数定义和函数调用的地方都显式的使用&符号。
- 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和函数的堆栈),如下所示:
(3)global $var
global var这句话,实际上会执行两件事情:
1.在函数内部的符号表中插入局部的var符号
2.建立局部var与全局变量var之间的引用.
(4)unset($var);
这里要注意的是,unset只是删除函数内部符号表中var符号,而不是删除全局的。同时,更新原zval的refcount值和is_ref引用标志(引用解绑):
image.png
(5).global var
同3,再次建立局部var与全局的var的引用:
(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
网友评论