当我们学第一门语言时,比如大学课程中的c语言程序设计,也许曾经被前缀自增(++i) 和后缀自增 (i++)纠结过。 曾经以为我们懂了:
i++ :先引用后增加,先在i所在的表达式中使用i的当前值,后让i加1++i :先增加后引用,让i先加1,然后在i所在的表达式中使用i的新值这个表达基本没错,只能说不够精确。在《expert c programming》这本书中的附录中,有这样一段说明: ++i表示取i的地址,增加它的内容,然后把值放在寄存器中;i++表示取i的地址,把它的值装入寄存器中,然后增加内存中的i的值。 这里的寄存器存放的就是我们在表达式中使用的值。
在php中也有++$i和$i++,那么zend内核是如何实现这两种自增方式的呢? 看下面一个例子,在不运行这段代码的情况下,你认为会输出什么呢?
$i = 0;$i = $i++;echo $i;
咱们先不论答案是什么?我们直接从zend内核查看这种自增操作的实现。
使用vld查看包含了$i++和++$i的php代码生成的中间代码:
$i = 0;$i++;++$i;
使用vld命令(php -dvld.active=1 -dvld.verbosity=3 t.php)查看详细参数:
number of ops: 8compiled vars: !0 = $iline # * op fetch ext return operands--------------------------------------------------------------------------------- 2 0 > ext_stmt res[ is_unused ] op1[ is_unused ] op2[ is_unused ] 1 assign op1[is_cv !0 ] op2[ , is_const (0) 0 ] 3 2 ext_stmt res[ is_unused ] op1[ is_unused ] op2[ is_unused ] 3 post_inc res[ is_tmp_var ~1 ] op1[ is_cv !0 ] 4 free op1[is_tmp_var ~1 ] 4 5 ext_stmt res[ is_unused ] op1[ is_unused ] op2[ is_unused ] 6 pre_inc op1[is_cv !0 ] 5 7 > return op1[is_const (0) 1 ]branch: # 0; line: 2- 5; sop: 0; eop: 7path #1: 0,
从vld扩展的输出信息可以知道,前缀自增(++$i)对应的opcode为pre_inc,后缀自增($i++)对应的opcode为post_inc。 首先我们看前缀自增(++$i),++$i没有返回值或者说它的返回值为空。 根据中间代码和vld显示的op1的参数类型, 我们可以知道++$i的中间代码在执行是最终调用的是zend/zend_vm_execute.h文件中的zend_pre_inc_spec_cv_handler函数。 在zend_pre_inc_spec_cv_handler函数中有几个关键点:
cv类型变量的获取,它是调用_get_zval_ptr_ptr_cv获取cv类型变量。 这里的cv类型的变量是php编译期间的类似于缓存的作用,主要作用是提高某些变量的存储速度。increment_function函数,不管是实例变量,类变量或者常规的变量,最终都是调用increment_function函数实现变量的增加操作。 在这个函数中,程序会根据变量的类型做出不同的处理,在php5.3.1这个版本中,php支持is_long、is_double、is_null和is_string四种类型。 如果变量的类型是is_null,程序会将变量的值赋值为1。如果变量类型是字符串,程序会将其转化成整形或浮点型进行计算。使用return_value_unused宏清除返回结果,这个宏的作用是将result变量的类型设置为ext_type_unused类型。前缀自增(++$i)操作在zend内核中本质上是操作变量本身,而且在表达式中使用的也是这个变量本身。
了解了++$i的实现,我们来看下可能使用得更多的$i++操作的实现。 同样,从中间代码post_inc和op1的类型是is_cv,我们可以在zend/zend_vm_execute.h文件中找到其实现为zend_post_inc_spec_cv_handler。 与前面的zend_pre_inc_spec_cv_handler相比,它们都有一个取cv类型变量的过程,也有一个increment_function函数增加变量值的过程, 但是除此之外它多了一个操作,同时也少了一个操作。 它多的一个操作是:
ex_t(opline->result.u.var).tmp_var = **var_ptr;zendi_zval_copy_ctor(ex_t(opline->result.u.var).tmp_var);
这两行代码的作用是初始化返回值到临时变量,并且将原始的$i的值存储在这,这就是我们在前面使用vld查看生成的中间代码其结果为res[ is_tmp_var ~1 ]的原因。 在这个初始化完成后,程序会继续执行增加操作,在增加操作完成后,它就结束了,而之前的++$i操作则会将result设置为unused类型,这就是它少的那个操作。
后缀自增($i++)在表达式中使用的是存放在临时变量中原先的变量值,而变量本身的值已经增加了。 在php中这种变量的分离是通过临时变量+返回值解决。
到这里,我们可以回答最开始的问题了,它会输出0。因为在表达式中$i++的返回值是一个临时变量,也就是$i原来的值,也就是0。
