变量的作用域

讨论完 PHP 7 的变量实现和变量的类型,接下来讨论一下变量的作用域。什么是变量的作用域呢?简单地说,就是定义变量在代码中可以使用的范围。那么在 PHP 7 中变量和变量的作用域又是如何实现的呢?

全局变量

简单地说,全局变量就是在程序的任何一处都可以使用的变量。在 PHP 底层维护了全局的符号表(symbol_table),它本身是一个 HashTable, PHP 代码中的全局变量都维护在这个 HashTable 中,符号表的作用域是整个 PHP 代码。

为了方便理解,下面以一段 PHP 代码为例,并通过 gdb 来进行追踪:

<?php
$a = "hello world";

在上面的 php 文件中只有一个句话,通过 gdb 一步步追踪,最终发现会调用 zend_attach_symbol_table 这个函数,这个函数会将 “$a” 加到全局的符号表中。

限于篇幅,这里只将 zend_attach_symbol_table 函数的核心代码展示出来,如下面所示:

ZEND_API void zend_attach_symbol_table(zend_execute_data *execute_data) /* {{{ */
{
    zend_op_array *op_array = &execute_data->func->op_array;
    HashTable *ht = execute_data->symbol_table;

    /* copy real values from symbol table into CV slots and create INDIRECT references to CV in symbol table  */
    if (EXPECTED(op_array->last_var)) {
        //代码省略//
            do {
                    zval *zv = zend_hash_find(ht, *str);
                    if (zv) {
                              if (Z_TYPE_P(zv) == IS_INDIRECT) {
                              zval *val = Z_INDIRECT_P(zv);
                              ZVAL_COPY_VALUE(var, val);
    //代码省略//

zend_attach_symbol_table 函数的主要作用是将一个自定义的符号表与当前的执行环境关联起来,从而使得在执行期间可以访问和操作这个符号表中的变量和函数。

局部变量

局部变量是在函数内部定义说明的。函数的调用过程是不断地压栈和出栈,出栈后内部变量被销毁,因此其作用域仅限于函数内,离开该函数后再使用这种变量是非法的。

为了便于理解,现以 PHP 代码为例说明一下,如下所示:

<?php

class User
{
    public function hi()
    {
        $a = 100;
        $b = time();
    }
}

这里定义了一个类,在类中实现了一个方法,方法中定义了两个变量 ab。因为变量 ab 在方法内部,其他地方不能直接访问,所以只能在方法内部使用。

中间变量

在 PHP 代码中有一种操作,会产生一种类型为 IS_TMP_VAR 的变量,姑且称为 “中间变量”。它的产生比较简单,以如下代码为例:

<?php
$a = 1;
$b = $a + 1;

代码中定义了一个变量,接着做了一次加法运算,然后通过 vld 查看它的 opcode,查看时将 -dvld.verbosity=1 参数加上。

vld 查看的 opcode 如下:

Finding entry points
Branch analysis from position: 0
Add 0
Add 1
Add 2
Add 3
Jump found. (Code = 62) Position 1 = -2
filename:       /home/vagrant/test.php
function name:  (null)
number of ops:  4
compiled vars:  !0 = $a, !1 = $b
line     #* E I O op                          fetch          ext  return  operands
-------------------------------------------------------------------------------------
  2     0  E >   ASSIGN    OP1[  IS_CV !0 ] OP2[ ,  IS_CONST (0) 1 ]
  3     1         ADD                                        RES[  IS_TMP_VAR ~2 ]    OP1[  IS_CV !0 ] OP2[ ,  IS_CONST (0) 1 ]
        2        ASSIGN      OP1[  IS_CV !1 ] OP2[ ,  IS_TMP_VAR ~2 ]
  5     3      > RETURN      OP1[  IS_CONST (0) 1 ]
branch: #  0; line:     2-    5; sop:     0; eop:     3; out1:  -2
path #1: 0,

通过 vld 可以看到,$a + 1 执行后生成一个中间变量(类型为 IS_TMP_VAR),然后将中间变量赋值给 $b,这个中间过程用户无法感知,它仅在当前作用域内有效。

静态变量

静态变量是在 PHP 代码中使用的场景较多的一种变量。下面同样以一段 PHP 代码为例,然后再看看它底层的原理,代码如下面所示:

<?php
class A {
    public function test($ab){
        static $n = 10;
        $n = $n * $ab;
        return $n;
    }
}
$obj = new A();
$i = 1;
while($i < 10){
    echo "number $i return ". $obj->test($i) ."\n";
    $i++;
}

通过执行上面的代码可以发现静态变量 $n 的值在函数 test 结束后并没有被销毁。PHP 代码执行过程中会将局部变量存储在 zend_execute_data 相邻的内存中,但是静态变量会存在 _zend_op_array.static_variables 中。局部变量在函数执行结束后被销毁,而静态变量不会被销毁。

常量

常量也有全局常量和局部常量的区分,两者的作用域不同。在全局变量的结构中有 zend_constantsHashTable,同时类和对象 constants_tableHashTable 用来存放类中定义的常量。常量的类型为 IS_CONSTANT,同时常量具有可引用和可拷贝的属性,但是常量不能被回收。

#define IS_CONSTANT_EX  (IS_CONSTANT | ((IS_TYPE_CONSTANT  |  IS_TYPE_REFCOUNTED  |  IS_TYPE_COPYABLE)  <<  Z_TYPE_FLAGS_SHIFT))