0

0

php中的foreach问题_PHP教程

php中文网

php中文网

发布时间:2016-07-20 11:13:36

|

992人浏览过

|

来源于php中文网

原创

前言:

php4中引入了foreach结构,这是一种遍历数组的简单方式。相比传统的for循环,foreach能够更加便捷的获取键值对。在php5之前,foreach仅能用于数组;php5之后,利用foreach还能遍历对象(详见:遍历对象)。本文中仅讨论遍历数组的情况。

foreach虽然简单,不过它可能会出现一些意外的行为,特别是代码涉及引用的情况下。

下面列举了几种case,有助于我们进一步认清foreach的本质。

问题1:

$arr = array(<span>,<span>,<span><span><span foreach>($arr <span as> $k => &<span>= $v * <span><span><span><span now is array>

<span foreach>($arr <span as> $k =><span echo><span><span><span>, <span><span><span>, <span><span><span><span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>

先从简单的开始,如果我们尝试运行上述代码,就会发现最后输出为0=>2  1=>4  2=>4

立即学习PHP免费学习笔记(深入)”;

为何不是0=>2  1=>4  2=>6 ?

其实,我们可以认为 ( => 隐含了如下操作,分别将数组当前的'键'和当前的'值'赋给变量$k和$v。具体展开形如:

<span foreach>(<span> <span as> <span> => <span><span></span></span><span><span></span></span><span> =<span currentval></span>    </span><span> =<span currentkey></span>
    </span><span><span>
<span></span></span></span></span></span></span></span>

根据上述理论,现在我们重新来分析下第一个foreach:

第1遍循环,由于$v是一个引用,因此$v = &$arr[0],$v=$v*2相当于$arr[0]*2,因此$arr变成2,2,3

第2遍循环,$v = &$arr[1],$arr变成2,4,3

第3遍循环,$v = &$arr[2],$arr变成2,4,6

随后代码进入了第二个foreach:

第1遍循环,隐含操作$v=$arr[0]被触发,由于此时$v仍然是$arr[2]的引用,即相当于$arr[2]=$arr[0],$arr变成2,4,2

第2遍循环,$v=$arr[1],即$arr[2]=$arr[1],$arr变成2,4,4

第3遍循环,$v=$arr[2],即$arr[2]=$arr[2],$arr变成2,4,4

OK,分析完毕。

如何解决类似问题呢?php手册上有一段提醒:

Warning : 数组最后一个元素的 $value 引用在 foreach 循环之后仍会保留。建议使用unset()来将其销毁。
<span> = <span array>(1,2,3<span><span foreach>(<span> <span as> <span> => &<span><span><span> = <span> * 2<span><span unset>(<span><span><span foreach>(<span> <span as> <span> => <span><span><span echo> "<span>", " => ", "<span>"<span><span><span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>

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

问题2:

<span> = <span array>('a','b','c'<span><span foreach>(<span> <span as> <span> => <span><span><span echo> <span key>(<span>), "=>", <span current>(<span>)<span></span><br>// 打印 1=>b 1=>b 1=>b</span></span></span></span></span></span></span></span></span></span></span></span></span></span>

这个问题更加诡异。按照手册的说法,key和current分别是取数组中当前元素的的键值。

那为何key($arr)一直是1,current($arr)一直是b呢?

先用vld查看编译之后的opcode:

我们从第3行的ASSIGN指令看起,它代表将array('a','b','c')赋值给$arr。

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

<span static> <span int><span zend_fastcall zend_assign_spec_cv_tmp_handler zend_op>*opline =<span ex zend_free_op free_op2 zval>*value = _get_zval_ptr_tmp(&opline->op2, EX(Ts), &<span free_op2 tsrmls_cc><span><span cv>
    zval **variable_ptr_ptr = _get_zval_ptr_ptr_cv(&opline-><span op1 ex bp_var_w tsrmls_cc><span if> (IS_CV == IS_VAR && !<span variable_ptr_ptr><span else><span><span><span>
         value = zend_assign_to_variable(variable_ptr_ptr, value, <span><span tsrmls_cc><span if> (!RETURN_VALUE_UNUSED(&opline-><span result ai_set_ptr>->result.u.<span var>).<span var><span value pzval_lock zend_vm_next_opcode></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>

ASSIGN指令完成之后,CV数组中被加入zval**指针,指针指向实际的array,这表示$arr已经被CV缓存了起来。

接下来执行数组的循环操作,我们来看FE_RESET指令,它对应的执行函数为ZEND_FE_RESET_SPEC_CV_HANDLER:

<span static> <span int><span zend_fastcall zend_fe_reset_spec_cv_handler><span if><span><span else><span><span><span>
        array_ptr = _get_zval_ptr_cv(&opline-><span op1 ex bp_var_r tsrmls_cc><span><span>array的指针保存到zend_execute_data->Ts中(Ts用于存放代码执行期的temp_variable)</span>
    AI_SET_PTR(EX_T(opline->result.u.<span var>).<span var><span array_ptr pzval_lock><span if><span><span else> <span if> ((fe_ht = HASH_OF(array_ptr)) !=<span null><span><span>
<span zend_hash_internal_pointer_reset><span if><span is_empty>= zend_hash_has_more_elements(fe_ht) !=<span success><span><span>
        zend_hash_get_pointer(fe_ht, &EX_T(opline->result.u.<span var><span><span else><span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>

这里主要将2个重要的指针存入了中:

  • 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:

<span static> <span int><span zend_fastcall zend_fe_fetch_spec_var_handler zend_op>*opline =<span ex><span><span>
    zval *array = EX_T(opline->op1.u.<span var>).<span var><span .ptr><span switch> (zend_iterator_unwrap(array, &<span iter tsrmls_cc><span default><span :><span case><span zend_iter_invalid:><span case><span zend_iter_plain_object:><span case><span zend_iter_plain_array: fe_ht>=<span hash_of><span><span><span><span fe_reset><span><span>
            zend_hash_set_pointer(fe_ht, &EX_T(opline->op1.u.<span var><span><span><span>
            <span if> (zend_hash_get_current_data(fe_ht, (<span void> **) &value)==<span failure zend_vm_jmp>->opcodes+opline-><span op2.u.opline_num><span if><span key_type>= zend_hash_get_current_key_ex(fe_ht, &str_key, &str_key_len, &int_key, <span><span null><span><span>
<span zend_hash_move_forward><span><span>
            zend_hash_get_pointer(fe_ht, &EX_T(opline->op1.u.<span var><span><span break><span><span case><span zend_iter_object:></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>

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

简单来说,由于第一遍循环中FE_FETCH中已经将数组的内部指针移动到了第二个元素,所以在foreach内部调用key($arr)和current($arr)时,实际上获取的便是1和'b'。

那为何会输出3遍1=>b呢?

我们继续看第9行和第13行的SEND_REF指令,它表示将$arr参数压栈。紧接着一般会使用DO_FCALL指令去调用key和current函数。PHP并非被编译成本地机器码,因此php采用这样的opcode指令去模拟实际CPU和内存的工作方式。

查阅PHP源码中的SEND_REF:

<span static> <span int><span zend_fastcall zend_send_ref_spec_cv_handler></span></span><span><span>
    varptr_ptr = _get_zval_ptr_ptr_cv(&opline-><span op1 ex bp_var_w tsrmls_cc><span><span>
<span separate_zval_to_make_is_ref varptr>= *<span varptr_ptr z_addref_p><span><span>
<span zend_vm_stack_push tsrmls_cc zend_vm_next_opcode></span></span></span></span></span></span></span></span></span></span></span>

上述代码中的

<span> SEPARATE_ZVAL_TO_MAKE_IS_REF(ppzv)    \
    <span if> (!PZVAL_IS_REF(*<span ppzv separate_zval z_set_isref_pp></span></span></span>

的主要作用为,如果变量不是一个引用,则在内存中copy出一份新的。本例中它将array('a','b','c')复制了一份。因此变量分离之后的内存为:

注意,变量分离完成之后,CV数组中的指针指向了新copy出来的数据,而通过中的指针则依然可以获取旧的数据。

接下来的循环就不一一赘述了,结合上图来说:

  • foreach结构使用的是下方蓝色的array,会依次遍历a,b,c
  • key、current使用的是上方黄色的array,它的内部指针永远指向b

至此我们明白了为何key和current一直返回array的第二个元素,由于没有外部代码作用于copy出来的array,它的内部指针便永远不会移动。

问题3:

<span> = <span array>('a','b','c'<span><span foreach>(<span> <span as> <span> => &<span><span><span echo> <span key>(<span>), '=>', <span current>(<span>)<span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
<span></span>

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

首先foreach会调用FE_RESET:

<span static> <span int><span zend_fastcall zend_fe_reset_spec_cv_handler><span if> (opline->extended_value &<span zend_fe_reset_variable><span><span>
        array_ptr_ptr = _get_zval_ptr_ptr_cv(&opline-><span op1 ex bp_var_r tsrmls_cc><span if> (array_ptr_ptr == NULL || array_ptr_ptr == &<span eg><span else> <span if> (Z_TYPE_PP(array_ptr_ptr) ==<span is_object><span else><span><span><span>
            <span if> (Z_TYPE_PP(array_ptr_ptr) ==<span is_array separate_zval_if_not_ref><span if> (opline->extended_value &<span zend_fe_fetch_byref><span><span>
<span z_set_isref_pp array_ptr>= *<span array_ptr_ptr z_addref_p><span else><span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>

问题2中已经分析了一部分FE_RESET的实现。这里需要特别注意,本例foreach获取值采用了引用,因此在执行的时候FE_RESET中会进入与上题不同的另一个分支。

最终,FE_RESET会将array的is_ref设置为true,此时内存中只有一份array的数据。

接下来分析SEND_REF:

<span static> <span int><span zend_fastcall zend_send_ref_spec_cv_handler><span><span>
    varptr_ptr = _get_zval_ptr_ptr_cv(&opline-><span op1 ex bp_var_w tsrmls_cc><span><span><span><span>
<span separate_zval_to_make_is_ref varptr>= *<span varptr_ptr z_addref_p><span><span>
<span zend_vm_stack_push tsrmls_cc zend_vm_next_opcode></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>

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

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

ZEND_API <span int> zend_hash_move_forward_ex(HashTable *ht, HashPosition *<span pos hashposition>*current = pos ? pos : &ht-><span pinternalpointer is_consistent><span if> (*<span current>*current = (*current)-><span plistnext><span return><span success><span else>
        <span return><span failure></span></span></span></span></span></span></span></span></span></span></span>

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

 问题4:

<span> = <span array>(1, 2, 3<span><span> = <span><span><span foreach>(<span><span as> <span> => &<span><span><span> *= 2<span><span var_dump>(<span>, <span>); <span><span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>

该题与foreach关系不大,不过既然涉及到了foreach,就一起拿来讨论吧:)

代码里首先创建了数组$arr,随后将该数组赋给了$tmp,在接下来的foreach循环中,对$v进行修改会作用于数组$tmp上,但是却并不作用到$arr。

为什么呢?

这是由于在php中,赋值运算是将一个变量的值拷贝到另一个变量中,因此修改其中一个,并不会影响到另一个。

题外话:这并不适用于object类型,从PHP5起,对象的便总是默认通过引用进行赋值,举例来说:

<span class><span a><span public> <span> = 1<span><span> = <span> = <span new><span a><span>->foo=100<span><span echo> <span>->foo; <span><span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>

回到题目中的代码,现在我们可以确定$tmp=$arr其实是值拷贝,整个$arr数组会被再复制一份给$tmp。理论上讲,赋值语句执行完毕之后,内存中会有2份一样的数组。

也许有同学会疑问,如果数组很大,岂不是这种操作会很慢?

幸好php有更聪明的处理办法。实际上,当$tmp=$arr执行之后,内存中依然只有一份array。查看php源码中的zend_assign_to_variable实现(摘自php5.3.26):

<span static> inline zval* zend_assign_to_variable(zval **variable_ptr_ptr, zval *value, <span int><span is_tmp_var tsrmls_dc zval>*variable_ptr = *<span variable_ptr_ptr zval garbage><span><span>
    <span if> (Z_TYPE_P(variable_ptr) == IS_OBJECT && Z_OBJ_HANDLER_P(variable_ptr, <span set><span><span><span>
    <span if><span><span else><span><span><span>
        <span if> (Z_DELREF_P(variable_ptr)==<span><span><span else><span gc_zval_check_possible_root>*<span variable_ptr_ptr><span><span>
            <span if> (!<span is_tmp_var><span if> (PZVAL_IS_REF(value) && Z_REFCOUNT_P(value) > <span><span alloc_zval>*variable_ptr_ptr =<span variable_ptr>*variable_ptr = *<span value z_set_refcount_p><span><span zval_copy_ctor><span else><span><span><span></span>                    // value为指向$arr里实际array数据的指针,variable_ptr_ptr为$tmp里指向数据指针的指针
                    </span><span><span>
                    *variable_ptr_ptr =<span value><span><span value>
<span z_addref_p><span else><span z_unset_isref_pp><span return> *<span variable_ptr_ptr></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>

可见$tmp = $arr的本质就是将array的指针进行复制,然后将array的自动加1.用图表达出此时的内存,依然只有一份array数组:

既然只有一份array,那foreach循环中修改$tmp的时候,为何$arr没有跟着改变?

继续看PHP源码中的ZEND_FE_RESET_SPEC_CV_HANDLER函数,这是一个OPCODE HANDLER,它对应的OPCODE为FE_RESET。该函数负责在foreach开始之前,将数组的内部指针指向其第一个元素。

<span static><span int zend_fastcall zend_fe_reset_spec_cv_handler zend_op>*opline =<span ex zval>*array_ptr, **<span array_ptr_ptr hashtable>*<span fe_ht zend_object_iterator>*iter = <span null><span zend_class_entry>*ce = <span null><span zend_bool is_empty>= 0<span><span><span>
    <span if> (opline->extended_value &<span zend_fe_reset_variable array_ptr_ptr>= _get_zval_ptr_ptr_cv(&opline->op1, EX(Ts),<span bp_var_r tsrmls_cc><span if> (array_ptr_ptr == <span null> || array_ptr_ptr == &<span eg><span><span foreach>
        <span else> <span if> (Z_TYPE_PP(array_ptr_ptr) == <span is_object><span><span else><span><span><span>
            <span if> (Z_TYPE_PP(array_ptr_ptr) == <span is_array><span><span><span></span>                // 它会重新复制一个数组出来
                // 真正分离$tmp和$arr,变成了内存中的2个数组</span>
<span separate_zval_if_not_ref><span if> (opline->extended_value &<span zend_fe_fetch_byref z_set_isref_pp array_ptr>= *<span array_ptr_ptr z_addref_p><span else><span><span><span>
<span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>

从代码中可以看出,真正执行变量分离并不是在赋值语句执行的时候,而是推迟到了使用变量的时候,这也是Copy On Write机制在PHP中的实现。

FE_RESET之后,内存的变化如下:

上图解释了为何foreach并不会对原来的$arr产生影响。至于ref_count以及is_ref的变化情况,感兴趣的同学可以详细阅读ZEND_FE_RESET_SPEC_CV_HANDLER和ZEND_SWITCH_FREE_SPEC_VAR_HANDLER的具体实现(均位于php-src/zend/zend_vm_execute.h中),本文不做详细剖析:)

www.bkjia.comtruehttp://www.bkjia.com/PHPjc/440367.htmlTechArticle前言: php4中引入了foreach结构,这是一种遍历数组的简单方式。相比传统的for循环,foreach能够更加便捷的获取键值对。在php5之前,foreach仅...

相关文章

PHP速学教程(入门到精通)
PHP速学教程(入门到精通)

PHP怎么学习?PHP怎么入门?PHP在哪学?PHP怎么学才快?不用担心,这里为大家提供了PHP速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载

相关标签:

php

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
pixiv网页版官网登录与阅读指南_pixiv官网直达入口与在线访问方法
pixiv网页版官网登录与阅读指南_pixiv官网直达入口与在线访问方法

本专题系统整理pixiv网页版官网入口及登录访问方式,涵盖官网登录页面直达路径、在线阅读入口及快速进入方法说明,帮助用户高效找到pixiv官方网站,实现便捷、安全的网页端浏览与账号登录体验。

928

2026.02.13

微博网页版主页入口与登录指南_官方网页端快速访问方法
微博网页版主页入口与登录指南_官方网页端快速访问方法

本专题系统整理微博网页版官方入口及网页端登录方式,涵盖首页直达地址、账号登录流程与常见访问问题说明,帮助用户快速找到微博官网主页,实现便捷、安全的网页端登录与内容浏览体验。

307

2026.02.13

Flutter跨平台开发与状态管理实战
Flutter跨平台开发与状态管理实战

本专题围绕Flutter框架展开,系统讲解跨平台UI构建原理与状态管理方案。内容涵盖Widget生命周期、路由管理、Provider与Bloc状态管理模式、网络请求封装及性能优化技巧。通过实战项目演示,帮助开发者构建流畅、可维护的跨平台移动应用。

183

2026.02.13

TypeScript工程化开发与Vite构建优化实践
TypeScript工程化开发与Vite构建优化实践

本专题面向前端开发者,深入讲解 TypeScript 类型系统与大型项目结构设计方法,并结合 Vite 构建工具优化前端工程化流程。内容包括模块化设计、类型声明管理、代码分割、热更新原理以及构建性能调优。通过完整项目示例,帮助开发者提升代码可维护性与开发效率。

29

2026.02.13

Redis高可用架构与分布式缓存实战
Redis高可用架构与分布式缓存实战

本专题围绕 Redis 在高并发系统中的应用展开,系统讲解主从复制、哨兵机制、Cluster 集群模式及数据分片原理。内容涵盖缓存穿透与雪崩解决方案、分布式锁实现、热点数据优化及持久化策略。通过真实业务场景演示,帮助开发者构建高可用、可扩展的分布式缓存系统。

103

2026.02.13

c语言 数据类型
c语言 数据类型

本专题整合了c语言数据类型相关内容,阅读专题下面的文章了解更多详细内容。

54

2026.02.12

雨课堂网页版登录入口与使用指南_官方在线教学平台访问方法
雨课堂网页版登录入口与使用指南_官方在线教学平台访问方法

本专题系统整理雨课堂网页版官方入口及在线登录方式,涵盖账号登录流程、官方直连入口及平台访问方法说明,帮助师生用户快速进入雨课堂在线教学平台,实现便捷、高效的课程学习与教学管理体验。

17

2026.02.12

豆包AI网页版入口与智能创作指南_官方在线写作与图片生成使用方法
豆包AI网页版入口与智能创作指南_官方在线写作与图片生成使用方法

本专题汇总豆包AI官方网页版入口及在线使用方式,涵盖智能写作工具、图片生成体验入口和官网登录方法,帮助用户快速直达豆包AI平台,高效完成文本创作与AI生图任务,实现便捷智能创作体验。

764

2026.02.12

PostgreSQL性能优化与索引调优实战
PostgreSQL性能优化与索引调优实战

本专题面向后端开发与数据库工程师,深入讲解 PostgreSQL 查询优化原理与索引机制。内容包括执行计划分析、常见索引类型对比、慢查询优化策略、事务隔离级别以及高并发场景下的性能调优技巧。通过实战案例解析,帮助开发者提升数据库响应速度与系统稳定性。

92

2026.02.12

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
PHP课程
PHP课程

共137课时 | 12.3万人学习

JavaScript ES5基础线上课程教学
JavaScript ES5基础线上课程教学

共6课时 | 11.3万人学习

PHP新手语法线上课程教学
PHP新手语法线上课程教学

共13课时 | 0.9万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号