찾다
백엔드 개발PHP 튜토리얼PHP内核探索之变量(2)-理解引用

本文主要内容:

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

一、引论

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

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

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

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

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

二、  符号表和zval

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

1.   符号表

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

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

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

 

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

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

  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으로 문의하세요.
PHP의 현재 상태 : 웹 개발 동향을 살펴보십시오PHP의 현재 상태 : 웹 개발 동향을 살펴보십시오Apr 13, 2025 am 12:20 AM

PHP는 현대 웹 개발, 특히 컨텐츠 관리 및 전자 상거래 플랫폼에서 중요합니다. 1) PHP는 Laravel 및 Symfony와 같은 풍부한 생태계와 강력한 프레임 워크 지원을 가지고 있습니다. 2) Opcache 및 Nginx를 통해 성능 최적화를 달성 할 수 있습니다. 3) PHP8.0은 성능을 향상시키기 위해 JIT 컴파일러를 소개합니다. 4) 클라우드 네이티브 애플리케이션은 Docker 및 Kubernetes를 통해 배포되어 유연성과 확장 성을 향상시킵니다.

PHP 대 기타 언어 : 비교PHP 대 기타 언어 : 비교Apr 13, 2025 am 12:19 AM

PHP는 특히 빠른 개발 및 동적 컨텐츠를 처리하는 데 웹 개발에 적합하지만 데이터 과학 및 엔터프라이즈 수준의 애플리케이션에는 적합하지 않습니다. Python과 비교할 때 PHP는 웹 개발에 더 많은 장점이 있지만 데이터 과학 분야에서는 Python만큼 좋지 않습니다. Java와 비교할 때 PHP는 엔터프라이즈 레벨 애플리케이션에서 더 나빠지지만 웹 개발에서는 더 유연합니다. JavaScript와 비교할 때 PHP는 백엔드 개발에서 더 간결하지만 프론트 엔드 개발에서는 JavaScript만큼 좋지 않습니다.

PHP vs. Python : 핵심 기능 및 기능PHP vs. Python : 핵심 기능 및 기능Apr 13, 2025 am 12:16 AM

PHP와 Python은 각각 고유 한 장점이 있으며 다양한 시나리오에 적합합니다. 1.PHP는 웹 개발에 적합하며 내장 웹 서버 및 풍부한 기능 라이브러리를 제공합니다. 2. Python은 간결한 구문과 강력한 표준 라이브러리가있는 데이터 과학 및 기계 학습에 적합합니다. 선택할 때 프로젝트 요구 사항에 따라 결정해야합니다.

PHP : 웹 개발의 핵심 언어PHP : 웹 개발의 핵심 언어Apr 13, 2025 am 12:08 AM

PHP는 서버 측에서 널리 사용되는 스크립팅 언어이며 특히 웹 개발에 적합합니다. 1.PHP는 HTML을 포함하고 HTTP 요청 및 응답을 처리 할 수 ​​있으며 다양한 데이터베이스를 지원할 수 있습니다. 2.PHP는 강력한 커뮤니티 지원 및 오픈 소스 리소스를 통해 동적 웹 컨텐츠, 프로세스 양식 데이터, 액세스 데이터베이스 등을 생성하는 데 사용됩니다. 3. PHP는 해석 된 언어이며, 실행 프로세스에는 어휘 분석, 문법 분석, 편집 및 실행이 포함됩니다. 4. PHP는 사용자 등록 시스템과 같은 고급 응용 프로그램을 위해 MySQL과 결합 할 수 있습니다. 5. PHP를 디버깅 할 때 error_reporting () 및 var_dump ()와 같은 함수를 사용할 수 있습니다. 6. 캐싱 메커니즘을 사용하여 PHP 코드를 최적화하고 데이터베이스 쿼리를 최적화하며 내장 기능을 사용하십시오. 7

PHP : 많은 웹 사이트의 기초PHP : 많은 웹 사이트의 기초Apr 13, 2025 am 12:07 AM

PHP가 많은 웹 사이트에서 선호되는 기술 스택 인 이유에는 사용 편의성, 강력한 커뮤니티 지원 및 광범위한 사용이 포함됩니다. 1) 배우고 사용하기 쉽고 초보자에게 적합합니다. 2) 거대한 개발자 커뮤니티와 풍부한 자원이 있습니다. 3) WordPress, Drupal 및 기타 플랫폼에서 널리 사용됩니다. 4) 웹 서버와 밀접하게 통합하여 개발 배포를 단순화합니다.

과대 광고 : 오늘 PHP의 역할을 평가합니다과대 광고 : 오늘 PHP의 역할을 평가합니다Apr 12, 2025 am 12:17 AM

PHP는 현대적인 프로그래밍, 특히 웹 개발 분야에서 강력하고 널리 사용되는 도구로 남아 있습니다. 1) PHP는 사용하기 쉽고 데이터베이스와 완벽하게 통합되며 많은 개발자에게 가장 먼저 선택됩니다. 2) 동적 컨텐츠 생성 및 객체 지향 프로그래밍을 지원하여 웹 사이트를 신속하게 작성하고 유지 관리하는 데 적합합니다. 3) 데이터베이스 쿼리를 캐싱하고 최적화함으로써 PHP의 성능을 향상시킬 수 있으며, 광범위한 커뮤니티와 풍부한 생태계는 오늘날의 기술 스택에 여전히 중요합니다.

PHP의 약한 참고 자료는 무엇이며 언제 유용합니까?PHP의 약한 참고 자료는 무엇이며 언제 유용합니까?Apr 12, 2025 am 12:13 AM

PHP에서는 약한 참조가 약한 회의 클래스를 통해 구현되며 쓰레기 수집가가 물체를 되 찾는 것을 방해하지 않습니다. 약한 참조는 캐싱 시스템 및 이벤트 리스너와 같은 시나리오에 적합합니다. 물체의 생존을 보장 할 수 없으며 쓰레기 수집이 지연 될 수 있음에 주목해야합니다.

PHP의 __invoke 마법 방법을 설명하십시오.PHP의 __invoke 마법 방법을 설명하십시오.Apr 12, 2025 am 12:07 AM

\ _ \ _ 호출 메소드를 사용하면 객체를 함수처럼 호출 할 수 있습니다. 1. 객체를 호출 할 수 있도록 메소드를 호출하는 \ _ \ _ 정의하십시오. 2. $ obj (...) 구문을 사용할 때 PHP는 \ _ \ _ invoke 메소드를 실행합니다. 3. 로깅 및 계산기, 코드 유연성 및 가독성 향상과 같은 시나리오에 적합합니다.

See all articles

핫 AI 도구

Undresser.AI Undress

Undresser.AI Undress

사실적인 누드 사진을 만들기 위한 AI 기반 앱

AI Clothes Remover

AI Clothes Remover

사진에서 옷을 제거하는 온라인 AI 도구입니다.

Undress AI Tool

Undress AI Tool

무료로 이미지를 벗다

Clothoff.io

Clothoff.io

AI 옷 제거제

AI Hentai Generator

AI Hentai Generator

AI Hentai를 무료로 생성하십시오.

인기 기사

R.E.P.O. 에너지 결정과 그들이하는 일 (노란색 크리스탈)
3 몇 주 전By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O. 최고의 그래픽 설정
3 몇 주 전By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O. 아무도들을 수없는 경우 오디오를 수정하는 방법
3 몇 주 전By尊渡假赌尊渡假赌尊渡假赌
WWE 2K25 : Myrise에서 모든 것을 잠금 해제하는 방법
4 몇 주 전By尊渡假赌尊渡假赌尊渡假赌

뜨거운 도구

Atom Editor Mac 버전 다운로드

Atom Editor Mac 버전 다운로드

가장 인기 있는 오픈 소스 편집기

Eclipse용 SAP NetWeaver 서버 어댑터

Eclipse용 SAP NetWeaver 서버 어댑터

Eclipse를 SAP NetWeaver 애플리케이션 서버와 통합합니다.

PhpStorm 맥 버전

PhpStorm 맥 버전

최신(2018.2.1) 전문 PHP 통합 개발 도구

드림위버 CS6

드림위버 CS6

시각적 웹 개발 도구

mPDF

mPDF

mPDF는 UTF-8로 인코딩된 HTML에서 PDF 파일을 생성할 수 있는 PHP 라이브러리입니다. 원저자인 Ian Back은 자신의 웹 사이트에서 "즉시" PDF 파일을 출력하고 다양한 언어를 처리하기 위해 mPDF를 작성했습니다. HTML2FPDF와 같은 원본 스크립트보다 유니코드 글꼴을 사용할 때 속도가 느리고 더 큰 파일을 생성하지만 CSS 스타일 등을 지원하고 많은 개선 사항이 있습니다. RTL(아랍어, 히브리어), CJK(중국어, 일본어, 한국어)를 포함한 거의 모든 언어를 지원합니다. 중첩된 블록 수준 요소(예: P, DIV)를 지원합니다.