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(abc)赋值给$arr
由 于$arr为CVarray(abc)为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>resultuvar)var 指向array的指针
•EX_T(opline>resultuvar)fefe_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(abc)复制了一份因此变量分离之后的内存为
注意变量分离完成之后CV数组中的指针指向了新copy出来的数据而通过zend_execute_data>Ts中的指针则依然可以获取旧的数据
接下来的循环就不一一赘述了结合上图来说
•foreach结构使用的是下方蓝色的array会依次遍历abc
•keycurrent使用的是上方黄色的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 {
……
}
// 重置数组内部指针
……
}
从代码中可以看出真正执行变量分离并不是在赋值语句执行的时候而是推迟到了使用变量的时候这也是Copy On Write机制在PHP中的实现
FE_RESET之后内存的变化如下
上 图解释了为何foreach并不会对原来的$arr产生影响至于ref_count以及is_ref的变化情况感兴趣的同学可以详细阅读 ZEND_FE_RESET_SPEC_CV_HANDLER和ZEND_SWITCH_FREE_SPEC_VAR_HANDLER的具体实现(均位于 phpsrc/zend/zend_vm_executeh中)本文不做详细剖析:)