php4中引入了Foreach结构,这是一种遍历数组的简单方法。与传统的for循环相比,foreach可以更方便地获取键值对。php5之前,foreach只能用于数组;php5之后,还可以使用foreach遍历对象(请参见:遍历对象)。本文只讨论遍历数组的情况。Foreach很简单,但它可能会有一些意想不到的行为,尤其是当代码涉及引用时。下面列举几个案例,有助于我们进一步了解foreach的本质。1:复制代码如下:$arr=array(1,2,3);foreach($ arr as $ k=$ v){ $ v=$ v * 2;}//现在$arr是数组(2,4,6)foreach($arr as $k=$v) { echo '$k ','=',' $ v ';}从简单开始。如果我们试着运行上面的代码,我们会发现最终的输出是0=2 1=4 2=4。为什么不是0=2 1=4 2=6?实际上,我们可以认为foreach($arr as $k=$v)结构暗示了以下操作,将数组的当前“键”和当前“值”分别赋给变量$k和$v。展开形式如下:复制的代码如下:foreach($arr as $k=$v){ //在执行用户代码之前隐含两个赋值操作$ v=current val();$ k=CurrentKey();//继续运行用户代码……}根据上面的理论,现在我们重新分析第一个foreach:第一个循环。由于$v是引用,$v=$arr[0],$v=$v*2相当于$arr[0]*2,因此$arr成为2,2,3的第二个。$v=$arr[2],$arr变成2,4,6。然后代码进入第二个foreach:第一个循环,隐式操作$v=$arr[0]被触发。此时,$v仍然是$arr[2]的引用,相当于$arr [2]=$ arr [。即,$arr[2]=$arr[1],$arr成为2,4,4,$v=$arr[2]的第3个循环,即,$arr[2]=$arr[2],$arr成为2,4,4OK,分析结束。如何解决类似问题?php手册中有一个提醒,警告:数组的最后一个元素的$value引用将在foreach循环之后保留。建议Unset()将其销毁。复制代码如下:$arr=array(1,2,3);foreach($ arr as $ k=$ v){ $ v=$ v * 2;} unset($ v);foreach($arr as $k=$v) { echo '$k ','=',' $ v ';}//Output 0=2 1=4 2=6从这个问题可以看出,引用很可能伴随着副作用。如果您不想无意识的修改导致数组内容的改变,最好及时取消这些引用。问题2:复制代码如下:$arr=array('a ',' b ',' c ');foreach($ arr as $ k=$ v){ echo key($ arr),'=',current($ arr);}//打印1=b 1=b 1=b的问题就更离奇了。根据手册,key和current是数组中当前元素的键值。为什么key($arr)总是1,current($arr)总是b?首先用vld检查编译后的操作码:。
我们从第3行的分配指令看起,它代表将数组(' a ',' b ',' c ')赋值给$arr。由于$arr为CV,数组(' a ',' b ',' c ')为TMP,因此分配指令找到实际执行的函数为ZEND _ ASSIGN _ SPEC _ CV _ TMP _ HANDLER。这里需要特别指出,CV是PHP5.1之后才增加的一种变量缓存,它采用数组的形式来保存兹瓦尔**,被躲藏住的变量再次使用时无需去查找活跃的符号表,而是直接去履历数组中获取,由于数组访问速度远超混杂表,因而可以提高效率。复制代码代码如下:静态int ZEND _ FASTCALL ZEND _ ASSIGN _ SPEC _ CV _ TMP _ HANDLER(ZEND _ OPCODE _ HANDLER _ ARGS){ ZEND _ op * op line=EX(op line);zend _ free _ op free _ op2zval *值=_get_zval_ptr_tmp(opline-op2,EX(Ts),free _ op2 TSRMLS _ CC);//CV数组中创建出$arr**指针zval * * variable _ ptr _ ptr=_ get _ zval _ ptr _ ptr _ cv(op line-op1,EX(Ts),BP _ VAR _ W TSRMLS _ CC);if (IS_CV==IS_VAR!variable_ptr_ptr) { …… } else { //将排列赋值给$arr值=Zend _ assign _ to _ variable(variable _ ptr _ ptr,value,1 TSRMLS _ CC);if(!RETURN _ VALUE _ UNUSED(op行-结果)){ AI _ SET _ PTR(EX _ T(op行-结果。u . var .).var,值);PZVAL_LOCK(值);} } ZEND _ VM _ NEXT _ OPCODE();}ASSIGN指令完成之后,CV数组中被加入兹瓦尔**指针,指针指向实际的数组,这表示$arr已经被履历缓存了起来
接下来执行数组的循环操作,我们来看FE _复位指令,它对应的执行函数为ZEND_FE_RESET_SPEC_CV_HANDLER:复制代码代码如下:静态int ZEND _ FASTCALL ZEND _ FE _ RESET _ SPEC _ CV _ HANDLER(ZEND _ OPCODE _ HANDLER _ ARGS){……if(……){……} else {//通过履历数组获取指向排列的指针array _ ptr=_ get _ zval _ ptr _ cv(op线-op1,EX(Ts),BP _ VAR _ R TSRMLS _ CC);…… } …… //将指向排列的指针保存到zend_execute_data-Ts中(Ts用于存放代码执行期的温度变量)。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(操作线-结果。u . var)。Fe。Fe _ pos用于保存数组内部指针zend_hash_get_pointer(fe_ht,EX _ T(op-line-result。u . var)。Fe。Fe _ pos);} else { …… } ……}这里主要将2个重要的指针存入了zend_execute_data-Ts中: EX _ T(操作线-结果。u . var ).var -指向排列的指针EX _ T(操作线-结果。u . var)。Fe。Fe _ pos-指向排列内部元素的指针FE _复位指令执行完毕之后,内存中实际情况如下
接下来我们继续看FE_FETCH,它对应的执行函数是ZEND_FE_FETCH_SPEC_VAR_HANDLER:复制代码如下:静态int ZEND _ fastcall ZEND _ FE _ FETCH _ SPEC _ VAR _ HANDLER(ZEND _ opcode _ HANDLER _ args){ ZEND _ op * op .//注意指针是zval * array=EX _ T(op line-op 1 . u . VAR . VAR . VAR . ptr从EX _ T(op line-op 1 . u . VAR . VAR)中获取)。……switch(ZEND _ iterator _ unwrap(array,ITER TSRMLS _ CC)){ default : case ZEND _ ITER _ invalid :……case ZEND _ ITER _平原_ object : {……} case ZEND _ ITER _平原_ array : Fe _ ht=HASH _ OF(array);//特别说明://在Fe _ reset指令中,数组内部元素的指针保存在ex _ t (opline-op1.u.var)中,Fe . Fe _ pos//在这里获取指针Zend _ hash _ set _ pointer (Fe _ ht,ex _ t (opline-op1.u.var)。//获取元素if的值(Zend _ hash _ get _ current _ data(Fe _ ht,(void * *)值)=failure){ Zend _ VM _ jmp(ex(op _ array)-操作码op line-op 2 . u . op line _ num);} if(use _ key){ key _ type=Zend _ hash _ get _ current _ key _ ex(Fe _ ht,str_key,str_key_len,int_key,1,NULL);}//数组内部的指针移动到下一个元素Zend _ hash _ move _ forward(Fe _ ht);//移动的指针保存到ex _ t(op line-op1 . u . var). Fe . Fe _ poszend _ hash _ get _ pointer(Fe _ ht,ex _ t(op line-op1 . u . var). Fe . Fe _ pos);打破;案例Zend _ ITER _ object :……}……}根据FE_FETCH的实现,我们一般理解foreach($arr as $k=$v)是做什么的。它将根据zend_execute_data-Ts的指针获取数组元素,然后将指针移动到下一个位置并再次保存。
简单来说,因为数组的内部指针已经在第一个循环中移动到FE_FETCH中的第二个元素,所以在foreach内部调用key($arr)和current($arr)时,它们实际上会得到1和‘b’。为什么1=b输出三次?让我们转到第9行和第13行的SEND_REF指令,这意味着$arr参数被推送到堆栈。然后,DO_FCALL指令通常用于调用键和当前函数。Php没有编译成本地机器码,所以PHP用这样的操作码指令来模拟CPU和内存的实际工作模式。在PHP源代码中查找SEND_REF:复制的代码如下:静态int Zend _ fast call Zend _ SEND _ REF _ spec _ cv _ handler(Zend _ opcode _ handler _ args){……//从CV中获取$arr指针的指针varp tr _ ptr=_ Get _ zval _ ptr _ ptr _ CV(op line-op1,ex (ts),BP _ var _ w TSR mls _ cc);//变量是分开的,这里复制了一个数组的副本,专门用于key函数separate _ zval _ to _ make _ is _ ref(varptr _ ptr);varptr=* varptr _ ptrz _ ADDREF _ P(varp tr);//push Zend _ VM _ stack _ push(varp tr TSR mls _ 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的主要功能是在内存中复制一个不是引用的新变量。在这个例子中,它复制了数组(' a ',' b ',' c ')。因此,变量分离后的内存为:。
注意,变量分离完成后,CV数组中的指针指向新复制的数据,而旧数据仍然可以通过zend_execute_data-Ts中的指针获取。我不会重复下面的循环。结合上图,foreach结构使用底部的蓝色数组,并将使用顶部的黄色数组依次遍历A、B、c键和电流。它的内部指针总是指向b。到目前为止,我们已经理解了为什么key和current总是返回到数组的第二个元素。因为没有外部代码作用于复制的数组,所以它的内部指针永远不会移动。问题3:复制代码如下:$arr=array('a ',' b ',' c ');foreach($ arr as $ k=$ v){ echo key($ arr),'=',current($ arr);}//Print 1=b 2=c=这个主题和问题2只有一个区别:这个主题中的foreach使用引用。和VLD一起看这个题目,发现和问题2的代码编译的操作码是一样的。因此,我们使用问题2的跟踪方法来逐步查看操作码的相应实现。首先foreach会调用FE_RESET:复制代码如下:静态int Zend _ fast call Zend _ FE _ reset _ spec _ cv _ handler(Zend _ opcode _ handler _ args)。{…if(op line-extended _ value Zend _ Fe _ reset _ variable){//从CV获取变量数组_ ptr _ ptr=_ get _ zval _ ptr _ ptr _ CV(op line-op1,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 {//在遍历数组if的情况下(Z _ TYPE _ PP(array _ ptr _ ptr)=IS _ array){ separate _ zval _ if _ not _ ref(array _ ptr _ ptr);if(op line-extended _ value Zend _ Fe _ fetch _ ByRef){//将保存数组的zval设置为is _ ref z _ set _ isref _ PP(array _ ptr _ ptr);} } array _ ptr=* array _ ptr _ ptrz _ ADDREF _ P(array _ ptr);}} else {.} .}问题2中已经分析了FE_RESET的部分实现。这里需要注意的是,在这个例子中,foreach得到的值采用的是引用,所以在执行过程中FE_RESET中会输入与上述问题不同的另一个分支。最终,FE_RESET会将数组的is_ref设置为true,内存中只有一个数组数据的副本。然后分析SEND_REF:复制代码如下:静态int Zend _ fastcall Zend _ SEND _ REF _ spec _ cv _ handler(Zend _ opcode _ handler _ args){……//从CV获取$arr指针的指针varp tr _ ptr=_ Get _ zval _ ptr _ ptr _ CV(op line-op1,ex (ts),BP _ var _ w TSR mls _ cc);//变量分离,由于此时CV中的变量本身就是一个引用,这里不会复制一个新的arrayseparate _ zval _ to _ make _ is _ ref(varptr _ ptr);varptr=* varptr _ ptrz _ ADDREF _ P(varp tr);//push Zend _ VM _ stack _ push(varp tr TSR mls _ cc);ZEND _ VM _ NEXT _ OPCODE();}宏SEPARATE_ZVAL_TO_MAKE_IS_REF只分离is_ref=false的变量。因为数组之前已经被设置为is_ref=true,所以不会被复制。换句话说,内存中仍然只有一个数组数据。
上图解释了为什么在前2个周期输出1=b 2=C。在第三个周期FE_FETCH,向前移动指针。复制代码如下: Zend _ API int Zend _ hash _ move _ forward _ ex(hashtable * ht,hashposition * pos){ hashposition * current=pos?位置: ht-pInternalPointer;IS _ CONFERENCE(ht);if(* current){ * current=(* current)-plist next;返回SUCCESS}否则返回FAILURE}此时,内部指针已经指向数组的最后一个元素,因此向前移动将指向NULL。将内部指针指向NULL后,我们在数组上调用key和current,然后会分别返回NULL和false,表示调用失败。这时,没有人物可以呼应。问题4:复制代码如下:$arr=array(1,2,3);$ tmp=$ arrforeach($ tmp as $ k=$ v){ $ v *=2;}var_dump($arr,$ tmp);//打印什么?这个话题和foreach关系不大,但是既然涉及到foreach,那就一起讨论吧。)代码首先创建了一个数组$arr,然后将其分配给$tmp。在下一个foreach循环中,修改$v将影响数组$tmp,但不会影响$arr。为什么呢?这是因为在php中,赋值操作是将一个变量的值复制到另一个变量中,所以修改一个变量不会影响另一个变量。题外话:这不适用于对象类型。从PHP5开始,默认情况下总是通过引用来分配对象。例如,复制代码如下: class A { public $ foo=1;}$a1=$a2=新A;$ a1-foo=100;echo $ a2-foo;//Output 100,$a1和$a2实际上是同一对象的引用。现在我们可以确认$tmp=$arr实际上是一个值副本,整个$arr数组将被复制到$tmp。理论上,在赋值语句执行后,内存中会有两个相同的数组。可能有些同学会问,如果阵列很大,这个操作不会很慢吗?幸运的是,php有一个更聪明的方法。实际上,在执行$tmp=$arr之后,内存中仍然只有一个数组。检查php源代码中zend_assign_to_variable的实现(来自php5.3.26):复制的代码如下:静态内联zval * Zend _ assign _ to _ variable(zval * * variable _ ptr _ ptr,zval * value,int is _ tmp _ var TSRMLS _ DC){ zval * variable _ ptr=* variable _ ptr _ ptr;zval垃圾;//左值为object type if(z _ type _ p(variable _ ptr)==is _ object z _ obj _ handler _ p(variable _ ptr,Set)){…}//左值为reference if(pzval _ is _ ref(variable _ ptr)){…} else {//左值ref count _ GC=1 if(z _ delref _ p(variable _ ptr)=0){…。//非临时变量if(!is_tmp_var) { if (PZVAL_IS_REF(值)Z_REFCOUNT_P(值)0) { ALLOC_ZVAL(变量_ ptr);* variable _ ptr _ ptr=variable _ ptr;* variable _ ptr=*值;Z_SET_REFCOUNT_P(变量_ptr,1);zval_copy_ctor(变量_ ptr);} else {//$tmp=$arr将在这里运行,//value是指向$arr中实际数组数据的指针,variable_ptr_ptr是指向$tmp中数据指针的指针。//它只是一个复制指针,但并没有真正复制实际的数组*变量_ptr_ptr=值;//value的refcount__gc值为1,在本例中refcount__gc为1,Z_ADDREF_P后面是2 Z_ADDREF_P(值);} } else {……} _ Z _ UNSET _ ISREF _ PP(变量_ ptr _ ptr);} return * variable _ ptr _ ptr}可见$tmp=$arr的本质是复制数组的指针,然后自动将数组的refcount增加1。此时的内存用图表示,数组仍然只有一个数组:。
既然只有一份数组,那为每一个循环中修改$tmp的时候,为何$arr没有跟着改变?继续看服务器端编程语言(专业超文本预处理器的缩写)源码中的ZEND_FE_RESET_SPEC_CV_HANDLER函数,这是一个OPCODE HANDLER,它对应的操作码为FE _复位。该函数负责在为每一个开始之前,将数组的内部指针指向其第一个元素。复制代码代码如下:静态int ZEND _ FASTCALL ZEND _ FE _ RESET _ SPEC _ CV _ HANDLER(ZEND _ OPCODE _ HANDLER _ ARGS){ ZEND _ op * op line=EX(op line);zval *array_ptr,* * array _ ptr _ ptrHashTable * Fe _ htZend _ object _ iterator * ITER=NULL;Zend _ class _ entry * ce=NullZend _ bool is _ empty=0;//对变量进行FE _ RESET if(op line-extended _ value ZEND _ FE _ RESET _ VARIABLE){ array _ ptr _ ptr=_ get _ zval _ ptr _ ptr _ cv(op line-op1,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){//注意此处的分离_ZVAL_IF_NOT_REF //它会重新复制一个数组出来//真正分离$tmp和$arr,变成了内存中的2个数组SEPARATE _ ZVAL _ IF _ NOT _ REF(array _ ptr _ ptr);if(op-line-extended _ value ZEND _ FE _ FETCH _ BYREF){ Z _ SET _ ISREF _ PP(array _ ptr _ ptr);} } array _ ptr=* array _ ptr _ ptrz _ ADDREF _ P(array _ ptr);} } else { …… } //重置数组内部指针……}从代码中可以看出,真正执行变量分离并不是在赋值语句执行的时候,而是推迟到了使用变量的时候,这也是写时复制机制在服务器端编程语言(专业超文本预处理器的缩写)中的实现FE _复位之后,内存的变化如下
上图解释了为何为每一个并不会对原来的$arr产生影响。至于ref_count以及is_ref的变化情况,感兴趣的同学可以详细阅读ZEND_FE_RESET_SPEC_CV_HANDLER和ZEND _ SWITCH _ FREE _ SPEC _ VAR _ HANDLER的具体实现(均位于php-src/zend/zend_vm_execute.h中),本文不做详细剖析:)