知识大全 深入解析php中的foreach问题

Posted 指针

篇首语:读书是最好的学习。追随伟大人物的思想,是最富有趣味的一门科学。本文由小常识网(cha138.com)小编为大家整理,主要介绍了知识大全 深入解析php中的foreach问题相关的知识,希望对你有一定的参考价值。

  php 中引入了foreach结构 这是一种遍历数组的简单方式 相比传统的for循环 foreach能够更加便捷的获取键值对 在php 之 前 foreach仅能用于数组 php 之后 利用foreach还能遍历对象(详见 遍历对象) 本文中仅讨论遍历数组的情况 foreach虽然简单 不过它可能会出现一些意外的行为 特别是代码涉及引用的情况下 下面列举了几种case 有助于我们进一步认清foreach的本质 问题

复制代码 代码如下: $arr = array( ); foreach($arr as $k => &$v) $v = $v * ; // now $arr is array( ) foreach($arr as $k => $v) echo "$k" " => " "$v";

   先从简单的开始 如果我们尝试运行上述代码 就会发现最后输出为 => => => 为何不是 => => => ? 其实 我们可以认为 foreach($arr as $k => $v) 结构隐含了如下操作 分别将数组当前的 键 和当前的 值 赋给变量$k和$v 具体展开形如

复制代码 代码如下: foreach($arr as $k => $v) //在用户代码执行之前隐含了 个赋值操作 $v = currentVal(); $k = currentKey(); //继续运行用户代码 ……

   根据上述理论 现在我们重新来分析下第一个foreach 第 遍循环 由于$v是一个引用 因此$v = &$arr[ ] $v=$v* 相当于$arr[ ]* 因此$arr变成 第 遍循环 $v = &$arr[ ] $arr变成 第 遍循环 $v = &$arr[ ] $arr变成 随后代码进入了第二个foreach 第 遍循环 隐含操作$v=$arr[ ]被触发 由于此时$v仍然是$arr[ ]的引用 即相当于$arr[ ]=$arr[ ] $arr变成 第 遍循环 $v=$arr[ ] 即$arr[ ]=$arr[ ] $arr变成 第 遍循环 $v=$arr[ ] 即$arr[ ]=$arr[ ] $arr变成 OK 分析完毕 如何解决类似问题呢?php手册上有一段提醒 Warning : 数组最后一个元素的 $value 引用在 foreach 循环之后仍会保留 建议使用unset()来将其销毁

复制代码 代码如下: $arr = array( ); foreach($arr as $k => &$v) $v = $v * ; unset($v); foreach($arr as $k => $v) echo "$k" " => " "$v"; // 输出 => => =>

   从这个问题中我们可以看出 引用很有可能会伴随副作用 如果不希望无意识的修改导致数组内容变更 最好及时unset掉这些引用 问题

复制代码 代码如下: $arr = array( a b c ); foreach($arr as $k => $v) echo key($arr) "=>" current($arr); // 打印 =>b =>b =>b

   这个问题更加诡异 按照手册的说法 key和current分别是取数组中当前元素的的键值 那为何key($arr)一直是 current($arr)一直是b呢? 先用vld查看编译之后的opcode:

   我们从第 行的ASSIGN指令看起 它代表将array( a b c )赋值给$arr 由 于$arr为CV array( a b c )为TMP 因此ASSIGN指令找到实际执行的函数为 ZEND_ASSIGN_SPEC_CV_TMP_HANDLER 这里需要特别指出 CV是PHP 之后才增加的一种变量cache 它采用数组的 形式来保存zval** 被cache住的变量再次使用时无需去查找active符号表 而是直接去CV数组中获取 由于数组访问速度远超hash表 因 而可以提高效率

复制代码 代码如下: static int ZEND_FASTCALL ZEND_ASSIGN_SPEC_CV_TMP_HANDLER(ZEND_OPCODE_HANDLER_ARGS) zend_op *opline = EX(opline); zend_free_op free_op ; zval *value = _get_zval_ptr_tmp(&opline >op EX(Ts) &free_op TSRMLS_CC); // CV数组中创建出$arr**指针 zval **variable_ptr_ptr = _get_zval_ptr_ptr_cv(&opline >op EX(Ts) BP_VAR_W TSRMLS_CC); if (IS_CV == IS_VAR && !variable_ptr_ptr) …… else // 将array赋值给$arr value = zend_assign_to_variable(variable_ptr_ptr value TSRMLS_CC); if (!RETURN_VALUE_UNUSED(&opline >result)) AI_SET_PTR(EX_T(opline >result u var) var value); PZVAL_LOCK(value); ZEND_VM_NEXT_OPCODE();

   ASSIGN指令完成之后 CV数组中被加入zval**指针 指针指向实际的array 这表示$arr已经被CV缓存了起来 接下来执行数组的循环操作 我们来看FE_RESET指令 它对应的执行函数为ZEND_FE_RESET_SPEC_CV_HANDLER

复制代码 代码如下: static int ZEND_FASTCALL ZEND_FE_RESET_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS) …… if (……) …… else // 通过CV数组获取指向array的指针 array_ptr = _get_zval_ptr_cv(&opline >op EX(Ts) BP_VAR_R TSRMLS_CC); …… …… // 将指向array的指针保存到zend_execute_data >Ts中(Ts用于存放代码执行期的temp_variable) AI_SET_PTR(EX_T(opline >result u var) var array_ptr); PZVAL_LOCK(array_ptr); if (iter) …… else if ((fe_ht = HASH_OF(array_ptr)) != NULL) // 重置数组内部指针 zend_hash_internal_pointer_reset(fe_ht); if (ce) …… is_empty = zend_hash_has_more_elements(fe_ht) != SUCCESS; // 设置EX_T(opline >result u var) fe fe_pos用于保存数组内部指针 zend_hash_get_pointer(fe_ht &EX_T(opline >result u var) fe fe_pos); else …… ……

   这里主要将 个重要的指针存入了zend_execute_data >Ts中: •EX_T(opline >result u var) var 指向array的指针 •EX_T(opline >result u var) fe fe_pos 指向array内部元素的指针 FE_RESET指令执行完毕之后 内存中实际情况如下

   接下来我们继续查看FE_FETCH 它对应的执行函数为ZEND_FE_FETCH_SPEC_VAR_HANDLER

复制代码 代码如下: static int ZEND_FASTCALL ZEND_FE_FETCH_SPEC_VAR_HANDLER(ZEND_OPCODE_HANDLER_ARGS) zend_op *opline = EX(opline); // 注意指针是从EX_T(opline >op u var) var ptr获取的 zval *array = EX_T(opline >op u var) var ptr; …… switch (zend_iterator_unwrap(array &iter TSRMLS_CC)) default: case ZEND_ITER_INVALID: …… case ZEND_ITER_PLAIN_OBJECT: …… case ZEND_ITER_PLAIN_ARRAY: fe_ht = HASH_OF(array); // 特别注意 // FE_RESET指令中将数组内部元素的指针保存在EX_T(opline >op u var) fe fe_pos // 此处获取该指针 zend_hash_set_pointer(fe_ht &EX_T(opline >op u var) fe fe_pos); // 获取元素的值 if (zend_hash_get_current_data(fe_ht (void **) &value)==FAILURE) ZEND_VM_JMP(EX(op_array) >opcodes+opline >op u opline_num); if (use_key) key_type = zend_hash_get_current_key_ex(fe_ht &str_key &str_key_len &int_key NULL); // 数组内部指针移动到下一个元素 zend_hash_move_forward(fe_ht); // 移动之后的指针保存到EX_T(opline >op u var) fe fe_pos zend_hash_get_pointer(fe_ht &EX_T(opline >op u var) fe fe_pos); break; case ZEND_ITER_OBJECT: …… ……

   根据FE_FETCH的实现 我们大致上明白了foreach($arr as $k => $v)所做的事情 它会根据zend_execute_data >Ts的指针去获取数组元素 在获取成功之后 将该指针移动到下一个位置再重新保存

  

  简单来说 由于第一遍循环中FE_FETCH中已经将数组的内部指针移动到了第二个元素 所以在foreach内部调用key($arr)和current($arr)时 实际上获取的便是 和 b 那为何会输出 遍 =>b呢? 我们继续看第 行和第 行的SEND_REF指令 它表示将$arr参数压栈 紧接着一般会使用DO_FCALL指令去调用key和current函数 PHP并非被编译成本地机器码 因此php采用这样的opcode指令去模拟实际CPU和内存的工作方式 查阅PHP源码中的SEND_REF

复制代码 代码如下: static int ZEND_FASTCALL ZEND_SEND_REF_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS) …… // 从CV中获取$arr指针的指针 varptr_ptr = _get_zval_ptr_ptr_cv(&opline >op EX(Ts) BP_VAR_W TSRMLS_CC); …… // 变量分离 此处重新copy了一份array专门用于key函数 SEPARATE_ZVAL_TO_MAKE_IS_REF(varptr_ptr); varptr = *varptr_ptr; Z_ADDREF_P(varptr); // 压栈 zend_vm_stack_push(varptr TSRMLS_CC); ZEND_VM_NEXT_OPCODE();

   上述代码中的SEPARATE_ZVAL_TO_MAKE_IS_REF是一个宏

复制代码 代码如下: #define SEPARATE_ZVAL_TO_MAKE_IS_REF(ppzv) if (!PZVAL_IS_REF(*ppzv)) SEPARATE_ZVAL(ppzv); Z_SET_ISREF_PP((ppzv));

   SEPARATE_ZVAL_TO_MAKE_IS_REF的主要作用为 如果变量不是一个引用 则在内存中copy出一份新的 本例中它将array( a b c )复制了一份 因此变量分离之后的内存为 注意 变量分离完成之后 CV数组中的指针指向了新copy出来的数据 而通过zend_execute_data >Ts中的指针则依然可以获取旧的数据 接下来的循环就不一一赘述了 结合上图来说 •foreach结构使用的是下方蓝色的array 会依次遍历a b c •key current使用的是上方黄色的array 它的内部指针永远指向b 至此我们明白了为何key和current一直返回array的第二个元素 由于没有外部代码作用于copy出来的array 它的内部指针便永远不会移动 问题

复制代码 代码如下: $arr = array( a b c ); foreach($arr as $k => &$v) echo key($arr) => current($arr); // 打印 =>b =>c =>

   本题与问题 仅有一点区别 本题中的foreach使用了引用 用VLD查看本题 发现与问题 代码编译出来的opcode一样 因此我们采用问题 的跟踪方法 逐步查看opcode对应的实现 首先foreach会调用FE_RESET:

复制代码 代码如下: static int ZEND_FASTCALL ZEND_FE_RESET_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS) …… if (opline >extended_value & ZEND_FE_RESET_VARIABLE) // 从CV中获取变量 array_ptr_ptr = _get_zval_ptr_ptr_cv(&opline >op EX(Ts) BP_VAR_R TSRMLS_CC); if (array_ptr_ptr == NULL || array_ptr_ptr == &EG(uninitialized_zval_ptr)) …… else if (Z_TYPE_PP(array_ptr_ptr) == IS_OBJECT) …… else // 针对遍历array的情况 if (Z_TYPE_PP(array_ptr_ptr) == IS_ARRAY) SEPARATE_ZVAL_IF_NOT_REF(array_ptr_ptr); if (opline >extended_value & ZEND_FE_FETCH_BYREF) // 将保存array的zval设置为is_ref Z_SET_ISREF_PP(array_ptr_ptr); array_ptr = *array_ptr_ptr; Z_ADDREF_P(array_ptr); else …… ……

   问题 中已经分析了一部分FE_RESET的实现 这里需要特别注意 本例foreach获取值采用了引用 因此在执行的时候FE_RESET中会进入与上题不同的另一个分支 最终 FE_RESET会将array的is_ref设置为true 此时内存中只有一份array的数据 接下来分析SEND_REF

复制代码 代码如下: static int ZEND_FASTCALL ZEND_SEND_REF_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS) …… // 从CV中获取$arr指针的指针 varptr_ptr = _get_zval_ptr_ptr_cv(&opline >op EX(Ts) BP_VAR_W TSRMLS_CC); …… // 变量分离 由于此时CV中的变量本身就是一个引用 此处不会copy一份新的array SEPARATE_ZVAL_TO_MAKE_IS_REF(varptr_ptr); varptr = *varptr_ptr; Z_ADDREF_P(varptr); // 压栈 zend_vm_stack_push(varptr TSRMLS_CC); ZEND_VM_NEXT_OPCODE();

   宏SEPARATE_ZVAL_TO_MAKE_IS_REF仅仅分离is_ref=false的变量 由于之前array已经被设置了is_ref=true 因此它不会被拷贝一份副本 换句话说 此时内存中依然只有一份array数据

  

  上图解释了前 次循环为何会输出 =>b =>C 在第 次循环FE_FETCH的时候 将指针继续向前移动

复制代码 代码如下: ZEND_API int zend_hash_move_forward_ex(HashTable *ht HashPosition *pos) HashPosition *current = pos ? pos : &ht >pInternalPointer; IS_CONSISTENT(ht); if (*current) *current = (*current) >pListNext; return SUCCESS; else return FAILURE;

   由于此时内部指针已经指向了数组的最后一个元素 因此再向前移动会指向NULL 将内部指针指向NULL之后 我们再对数组调用key和current 则分别会返回NULL和false 表示调用失败 此时是echo不出字符的 问题

复制代码 代码如下: $arr = array( ); $tmp = $arr; foreach($tmp as $k => &$v) $v *= ; var_dump($arr $tmp); // 打印什么?

   该题与foreach关系不大 不过既然涉及到了foreach 就一起拿来讨论吧:) 代码里首先创建了数组$arr 随后将该数组赋给了$tmp 在接下来的foreach循环中 对$v进行修改会作用于数组$tmp上 但是却并不作用到$arr 为什么呢? 这是由于在php中 赋值运算是将一个变量的值拷贝到另一个变量中 因此修改其中一个 并不会影响到另一个 题外话 这并不适用于object类型 从PHP 起 对象的便总是默认通过引用进行赋值 举例来说

复制代码 代码如下: class A public $foo = ; $a = $a = new A; $a >foo= ; echo $a >foo; // 输出 $a 与$a 其实为同一个对象的引用

   回到题目中的代码 现在我们可以确定$tmp=$arr其实是值拷贝 整个$arr数组会被再复制一份给$tmp 理论上讲 赋值语句执行完毕之后 内存中会有 份一样的数组 也许有同学会疑问 如果数组很大 岂不是这种操作会很慢? 幸好php有更聪明的处理办法 实际上 当$tmp=$arr执行之后 内存中依然只有一份array 查看php源码中的zend_assign_to_variable实现(摘自php )

复制代码 代码如下: static inline zval* zend_assign_to_variable(zval **variable_ptr_ptr zval *value int is_tmp_var TSRMLS_DC) zval *variable_ptr = *variable_ptr_ptr; zval garbage; …… // 左值为object类型 if (Z_TYPE_P(variable_ptr) == IS_OBJECT && Z_OBJ_HANDLER_P(variable_ptr set)) …… // 左值为引用的情况 if (PZVAL_IS_REF(variable_ptr)) …… else // 左值refcount__gc= 的情况 if (Z_DELREF_P(variable_ptr)== ) …… else GC_ZVAL_CHECK_POSSIBLE_ROOT(*variable_ptr_ptr); // 非临时变量 if (!is_tmp_var) if (PZVAL_IS_REF(value) && Z_REFCOUNT_P(value) > ) ALLOC_ZVAL(variable_ptr); *variable_ptr_ptr = variable_ptr; *variable_ptr = *value; Z_SET_REFCOUNT_P(variable_ptr ); zval_copy_ctor(variable_ptr); else // $tmp=$arr会运行到这里 // value为指向$arr里实际array数据的指针 variable_ptr_ptr为$tmp里指向数据指针的指针 // 仅仅是复制指针 并没有真正拷贝实际的数组 *variable_ptr_ptr = value; // value的refcount__gc值+ 本例中refcount__gc为 Z_ADDREF_P之后为 Z_ADDREF_P(value); else …… Z_UNSET_ISREF_PP(variable_ptr_ptr); return *variable_ptr_ptr;

   可见$tmp = $arr的本质就是将array的指针进行复制 然后将array的refcount自动加 用图表达出此时的内存 依然只有一份array数组 既然只有一份array 那foreach循环中修改$tmp的时候 为何$arr没有跟着改变? 继续看PHP源码中的ZEND_FE_RESET_SPEC_CV_HANDLER函数 这是一个OPCODE HANDLER 它对应的OPCODE为FE_RESET 该函数负责在foreach开始之前 将数组的内部指针指向其第一个元素

复制代码 代码如下: static int ZEND_FASTCALL ZEND_FE_RESET_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS) zend_op *opline = EX(opline); zval *array_ptr **array_ptr_ptr; HashTable *fe_ht; zend_object_iterator *iter = NULL; zend_class_entry *ce = NULL; zend_bool is_empty = ; // 对变量进行FE_RESET if (opline >extended_value & ZEND_FE_RESET_VARIABLE) array_ptr_ptr = _get_zval_ptr_ptr_cv(&opline >op EX(Ts) BP_VAR_R TSRMLS_CC); if (array_ptr_ptr == NULL || array_ptr_ptr == &EG(uninitialized_zval_ptr)) …… // foreach一个object else if (Z_TYPE_PP(array_ptr_ptr) == IS_OBJECT) …… else // 本例会进入该分支 if (Z_TYPE_PP(array_ptr_ptr) == IS_ARRAY) // 注意此处的SEPARATE_ZVAL_IF_NOT_REF // 它会重新复制一个数组出来 // 真正分离$tmp和$arr 变成了内存中的 个数组 SEPARATE_ZVAL_IF_NOT_REF(array_ptr_ptr); if (opline >extended_value & ZEND_FE_FETCH_BYREF) Z_SET_ISREF_PP(array_ptr_ptr); array_ptr = *array_ptr_ptr; Z_ADDREF_P(array_ptr); else …… // 重置数组内部指针 …… cha138/Article/program/PHP/201311/20993

相关参考

知识大全 foreach使用引用注意的问题

  foreach通过在$value之前加上&很容易就能修改数组的单元如PHP代码foreach($arr as &$value)   

知识大全 php连接函数implode与分割explode的深入解析

  目前学习php的人有很多很多进行php培训的朋友在学习中总会问到这样一个问题:php连接函数implode是什么呢?php可以将字符串分割成数组同时翻过了也可以将数组连接成字符串确切的说是可以将数

知识大全 php遍历数组 list foreach each方法总结

在php中可以用来遍历数组的函数有很多如有for语句listeachforeach这四个函数这也是在php中遍历数组的几个主要的函数下面我来给大家介绍 foreach遍历数组我们在运用数组时

知识大全 php foreach与for语句用法区别介绍

  //foreach  $tar=array(  =>东  =>西  =>南  =>北  =>东南  =>西南  =>东北  =>西北  =>南

知识大全 js限制checkbox勾选的个数以及php获取多个checkbbox的方法深入解析

以下是对js限制checkbox勾选的个数以及php获取多个checkbbox的方法进行了详细的分析介绍需要的朋友可以参考下   首先是js限制checkbbox勾选个数的代码复制代码代码如

知识大全 深入Nginx + PHP 缓存详解

深入Nginx+PHP缓存详解  以下文字资料是由(全榜网网www.cha138.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧!以下是对Nginx中的

知识大全 解析PHP中的file

解析PHP中的file  以下文字资料是由(全榜网网www.cha138.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧!  PHP的file_get_

知识大全 解析PHP中的内存管理,PHP动态分配和释放内存

解析PHP中的内存管理,PHP动态分配和释放内存  以下文字资料是由(全榜网网www.cha138.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧!本篇

知识大全 解析PHP中的unset究竟会不会释放内存

解析PHP中的unset究竟会不会释放内存  以下文字资料是由(全榜网网www.cha138.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧!PHP中的

知识大全 解析php中的escape函数

  采用js对URL中的汉字进行escape编码<ahref=""onclick="windowopen(product_listphp?p_sort=+escape(脚本之家));">这