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)操作:a =b(假设b,a都不是引用型变量),Zend并不会为$b变量开辟新的空间,而是将符号表中a符号和b符号指向同一个zval。只有在其中一个变量发生变化时,才会执行zval分离的操作。这被称为COW(Copy-on-write)的机制,可以在一定程度上节省内存和提高效率。

为了实现上述机制,需要对zval的引用状态做标记,zval的结构中,refcount__gc便是用于计数的,这个值记录了有多少个变量指向该zval, 在上述赋值操作中,a=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(分别是a,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的赋值形式,因此,类似与执行:

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

过程如下:

image.png

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容

  • PHP把变量保存在zval容器里面。容器,container,可以想像成一块存储区域,或者一个盒子。 如上图所示,...
    文档随手记阅读 897评论 0 1
  • 每一种语言都有自己的自动垃圾回收机制,让程序员不必过分关心程序内存分配,但是在OOP中,有些对象需要显式的销毁;防...
    文档随手记阅读 4,735评论 2 3
  • 1.有人说 今天写着写着代码用到了unset所以想整理一下. 有的人说PHP的unset并不真正释放内存, 有的说...
    会长__阅读 1,018评论 0 5
  • 简介:PHP 是一门托管型语言,在 PHP 编程中,程序员不需要手工处理内存资源的分配与释放(使用 C 编写 PH...
    0b19e507ac0c阅读 592评论 0 0
  • PHP执行原理** php是一门应用非常简单,开发效率极高的一门语言,其弱类型的变量能省去程序员大量的定义变量、类...
    人在码途阅读 1,466评论 1 7