php中的foreach题目
添加时间:2013-6-25 点击量:
媒介:
php4中引入了foreach布局,这是一种遍历数组的简单体式格式。比拟传统的for轮回,foreach可以或许加倍便捷的获取键值对。在php5之前,foreach仅能用于数组;php5之后,哄骗foreach还能遍历对象(详见:遍历对象)。本文中仅评论辩论遍历数组的景象。
foreach固然简单,不过它可能会呈现一些不测的行动,希罕是代码涉及引用的景象下。
下面列举了几种case,有助于我们进一步认清foreach的本质。
题目1:
¥arr = array(1,2,3);
foreach(¥arr as ¥k => &¥v) {
¥v = ¥v 2;
}
// now ¥arr is array(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){
//在用户代码履行之前隐含了2个赋值操纵
¥v = currentVal();
¥k = currentKey();
//持续运行用户代码
……
}
按照上述理论,如今我们从头来解析下第一个foreach:
第1遍轮回,因为¥v是一个引用,是以¥v = &¥arr[0],¥v=¥v2相当于¥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()来将其烧毁。
¥arr = array(1,2,3);
foreach(¥arr as ¥k => &¥v) {
¥v = ¥v 2;
}
unset(¥v);
foreach(¥arr as ¥k => ¥v) {
echo ¥k, => , ¥v;
}
// 输出 0=>2 1=>4 2=>6
从这个题目中我们可以看出,引用很有可能会伴随副感化。若是不无意识的批改导致数组内容变革,好及时unset掉这些引用。
题目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查看编译之后的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表,因而可以进步效力。
static int ZEND_FASTCALL ZEND_ASSIGN_SPEC_CV_TMP_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
zend_op opline = EX(opline);
zend_free_op free_op2;
zval value = _get_zval_ptr_tmp(&opline->op2, EX(Ts), &free_op2 TSRMLS_CC);
// CV数组中创建出¥arr指针
zval variable_ptr_ptr = _get_zval_ptr_ptr_cv(&opline->op1, 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, 1 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->op1, 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 {
……
}
……
}
这里首要将2个首要的指针存入了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->op1.u.var).var.ptr获取的
zval array = EX_T(opline->op1.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->op1.u.var).fe.fe_pos
// 此处获取该指针
zend_hash_set_pointer(fe_ht, &EX_T(opline->op1.u.var).fe.fe_pos);
// 获取元素的值
if (zend_hash_get_current_data(fe_ht, (void ) &value)==FAILURE) {
ZEND_VM_JMP(EX(op_array)->opcodes+opline->op2.u.opline_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(opline->op1.u.var).fe.fe_pos
zend_hash_get_pointer(fe_ht, &EX_T(opline->op1.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)时,实际上获取的便是1和b。
那为何会输出3遍1=>b呢?
我们持续看第9行和第13行的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->op1, 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,它的内部指针便永远不会移动。
题目3:
¥arr = array(a,b,c);
foreach(¥arr as ¥k => &¥v) {
echo key(¥arr), =>, current(¥arr);
}
// 打印 1=>b 2=>c =>
本题与题目2仅有一点差别:本题中的foreach应用了引用。用VLD查看本题,发明与题目2代码编译出来的opcode一样。是以我们采取题目2的跟踪办法,慢慢查看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->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 {
// 针对遍历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 {
……
}
……
}
题目2中已经解析了一项目组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->op1, 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数据。
上图说了然前2次轮回为何会输出1=>b 2=>C。在第3次轮回FE_FETCH的时辰,将指针持续向前移动。
ZEND_API int zend_hash_move_forward_ex(HashTable ht, HashPosition pos)
{
HashPosition current = pos ? pos : &ht->pInternalPointer;
IS_CISTENT(ht);
if (current) {
current = (current)->pListNext;
return SUCCESS;
} else
return FAILURE;
}
因为此时内部指针已经指向了数组的最后一个元素,是以再向前移动会指向NULL。将内部指针指向NULL之后,我们再对数组调用key和current,则分别会返回NULL和false,默示调用失败,此时是echo不出字符的。
题目4:
¥arr = array(1, 2, 3);
¥tmp = ¥arr;
foreach(¥tmp as ¥k => &¥v){
¥v = 2;
}
var_dump(¥arr, ¥tmp); // 打印什么?
该题与foreach关系不大,不过既然涉及到了foreach,就一路拿来评论辩论吧:)
代码里起建了数组¥arr,随后将该数组赋给了¥tmp,在接下来的foreach轮回中,对¥v进行批改会感化于数组¥tmp上,然则却并不感化到¥arr。
为什么呢?
这是因为在php中,赋值运算是将一个变量的值拷贝到另一个变量中,是以批改此中一个,并不会影响到另一个。
题外话:这并不实用于object类型,从PHP5起,对象的便老是默认经由过程引用进行赋值,举例来说:
class A{
public ¥foo = 1;
}
¥a1 = ¥a2 = new A;
¥a1->foo=100;
echo ¥a2->foo; // 输出100,¥a1与¥a2其实为同一个对象的引用
回到题目中的代码,如今我们可以断定¥tmp=¥arr其实是值拷贝,全部¥arr数组会被再复制一份给¥tmp。理论上讲,赋值语句履行完毕之后,内存中会有2份一样的数组。
也许有同窗会疑问,若是数组很大,岂不是这种操纵会很慢?
幸好php有更聪慧的处理惩罚办法。实际上,当¥tmp=¥arr履行之后,内存中依然只有一份array。查看php源码中的zend_assign_to_variable实现(摘自php5.3.26):
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=1的景象
if (Z_DELREF_P(variable_ptr)==0) {
……
} else {
GC_ZVAL_CHECK_POSSIBLE_ROOT(variable_ptr_ptr);
// 非姑且变量
if (!is_tmp_var) {
if (PZVAL_IS_REF(value) && Z_REFCOUNT_P(value) > 0) {
ALLOC_ZVAL(variable_ptr);
variable_ptr_ptr = variable_ptr;
variable_ptr = value;
Z_SET_REFCOUNT_P(variable_ptr, 1);
zval_copy_ctor(variable_ptr);
} else {
// ¥tmp=¥arr会运行到这里,
// value为指向¥arr里实际array数据的指针,variable_ptr_ptr为¥tmp里指向数据指针的指针
// 仅仅是复制指针,并没有真正拷贝实际的数组
variable_ptr_ptr = value;
// value的refcount__gc值+1,本例中refcount__gc为1,Z_ADDREF_P之后为2
Z_ADDREF_P(value);
}
} else {
……
}
}
Z_UNSET_ISREF_PP(variable_ptr_ptr);
}
return variable_ptr_ptr;
}
可见¥tmp = ¥arr的本质就是将array的指针进行复制,然后将array的refcount主动加1.用图表达出此时的内存,依然只有一份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 = 0;
// 对变量进行FE_RESET
if (opline->extended_value & ZEND_FE_RESET_VARIABLE) {
array_ptr_ptr = _get_zval_ptr_ptr_cv(&opline->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) {
// 重视此处的SEPARATE_ZVAL_IF_NOT_REF
// 它会从头复制一个数组出来
// 真正分别¥tmp和¥arr,变成了内存中的2个数组
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的具体实现(均位于php-src/zend/zend_vm_execute.h中),本文不做具体解析:)
媒介:
php4中引入了foreach布局,这是一种遍历数组的简单体式格式。比拟传统的for轮回,foreach可以或许加倍便捷的获取键值对。在php5之前,foreach仅能用于数组;php5之后,哄骗foreach还能遍历对象(详见:遍历对象)。本文中仅评论辩论遍历数组的景象。
foreach固然简单,不过它可能会呈现一些不测的行动,希罕是代码涉及引用的景象下。
下面列举了几种case,有助于我们进一步认清foreach的本质。
题目1:
¥arr = array(1,2,3);
foreach(¥arr as ¥k => &¥v) {
¥v = ¥v 2;
}
// now ¥arr is array(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){
//在用户代码履行之前隐含了2个赋值操纵
¥v = currentVal();
¥k = currentKey();
//持续运行用户代码
……
}
按照上述理论,如今我们从头来解析下第一个foreach:
第1遍轮回,因为¥v是一个引用,是以¥v = &¥arr[0],¥v=¥v2相当于¥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()来将其烧毁。
¥arr = array(1,2,3);
foreach(¥arr as ¥k => &¥v) {
¥v = ¥v 2;
}
unset(¥v);
foreach(¥arr as ¥k => ¥v) {
echo ¥k, => , ¥v;
}
// 输出 0=>2 1=>4 2=>6
从这个题目中我们可以看出,引用很有可能会伴随副感化。若是不无意识的批改导致数组内容变革,好及时unset掉这些引用。
题目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查看编译之后的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表,因而可以进步效力。
static int ZEND_FASTCALL ZEND_ASSIGN_SPEC_CV_TMP_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
zend_op opline = EX(opline);
zend_free_op free_op2;
zval value = _get_zval_ptr_tmp(&opline->op2, EX(Ts), &free_op2 TSRMLS_CC);
// CV数组中创建出¥arr指针
zval variable_ptr_ptr = _get_zval_ptr_ptr_cv(&opline->op1, 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, 1 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->op1, 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 {
……
}
……
}
这里首要将2个首要的指针存入了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->op1.u.var).var.ptr获取的
zval array = EX_T(opline->op1.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->op1.u.var).fe.fe_pos
// 此处获取该指针
zend_hash_set_pointer(fe_ht, &EX_T(opline->op1.u.var).fe.fe_pos);
// 获取元素的值
if (zend_hash_get_current_data(fe_ht, (void ) &value)==FAILURE) {
ZEND_VM_JMP(EX(op_array)->opcodes+opline->op2.u.opline_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(opline->op1.u.var).fe.fe_pos
zend_hash_get_pointer(fe_ht, &EX_T(opline->op1.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)时,实际上获取的便是1和b。
那为何会输出3遍1=>b呢?
我们持续看第9行和第13行的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->op1, 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,它的内部指针便永远不会移动。
题目3:
¥arr = array(a,b,c);
foreach(¥arr as ¥k => &¥v) {
echo key(¥arr), =>, current(¥arr);
}
// 打印 1=>b 2=>c =>
本题与题目2仅有一点差别:本题中的foreach应用了引用。用VLD查看本题,发明与题目2代码编译出来的opcode一样。是以我们采取题目2的跟踪办法,慢慢查看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->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 {
// 针对遍历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 {
……
}
……
}
题目2中已经解析了一项目组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->op1, 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数据。
上图说了然前2次轮回为何会输出1=>b 2=>C。在第3次轮回FE_FETCH的时辰,将指针持续向前移动。
ZEND_API int zend_hash_move_forward_ex(HashTable ht, HashPosition pos)
{
HashPosition current = pos ? pos : &ht->pInternalPointer;
IS_CISTENT(ht);
if (current) {
current = (current)->pListNext;
return SUCCESS;
} else
return FAILURE;
}
因为此时内部指针已经指向了数组的最后一个元素,是以再向前移动会指向NULL。将内部指针指向NULL之后,我们再对数组调用key和current,则分别会返回NULL和false,默示调用失败,此时是echo不出字符的。
题目4:
¥arr = array(1, 2, 3);
¥tmp = ¥arr;
foreach(¥tmp as ¥k => &¥v){
¥v = 2;
}
var_dump(¥arr, ¥tmp); // 打印什么?
该题与foreach关系不大,不过既然涉及到了foreach,就一路拿来评论辩论吧:)
代码里起建了数组¥arr,随后将该数组赋给了¥tmp,在接下来的foreach轮回中,对¥v进行批改会感化于数组¥tmp上,然则却并不感化到¥arr。
为什么呢?
这是因为在php中,赋值运算是将一个变量的值拷贝到另一个变量中,是以批改此中一个,并不会影响到另一个。
题外话:这并不实用于object类型,从PHP5起,对象的便老是默认经由过程引用进行赋值,举例来说:
class A{
public ¥foo = 1;
}
¥a1 = ¥a2 = new A;
¥a1->foo=100;
echo ¥a2->foo; // 输出100,¥a1与¥a2其实为同一个对象的引用
回到题目中的代码,如今我们可以断定¥tmp=¥arr其实是值拷贝,全部¥arr数组会被再复制一份给¥tmp。理论上讲,赋值语句履行完毕之后,内存中会有2份一样的数组。
也许有同窗会疑问,若是数组很大,岂不是这种操纵会很慢?
幸好php有更聪慧的处理惩罚办法。实际上,当¥tmp=¥arr履行之后,内存中依然只有一份array。查看php源码中的zend_assign_to_variable实现(摘自php5.3.26):
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=1的景象
if (Z_DELREF_P(variable_ptr)==0) {
……
} else {
GC_ZVAL_CHECK_POSSIBLE_ROOT(variable_ptr_ptr);
// 非姑且变量
if (!is_tmp_var) {
if (PZVAL_IS_REF(value) && Z_REFCOUNT_P(value) > 0) {
ALLOC_ZVAL(variable_ptr);
variable_ptr_ptr = variable_ptr;
variable_ptr = value;
Z_SET_REFCOUNT_P(variable_ptr, 1);
zval_copy_ctor(variable_ptr);
} else {
// ¥tmp=¥arr会运行到这里,
// value为指向¥arr里实际array数据的指针,variable_ptr_ptr为¥tmp里指向数据指针的指针
// 仅仅是复制指针,并没有真正拷贝实际的数组
variable_ptr_ptr = value;
// value的refcount__gc值+1,本例中refcount__gc为1,Z_ADDREF_P之后为2
Z_ADDREF_P(value);
}
} else {
……
}
}
Z_UNSET_ISREF_PP(variable_ptr_ptr);
}
return variable_ptr_ptr;
}
可见¥tmp = ¥arr的本质就是将array的指针进行复制,然后将array的refcount主动加1.用图表达出此时的内存,依然只有一份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 = 0;
// 对变量进行FE_RESET
if (opline->extended_value & ZEND_FE_RESET_VARIABLE) {
array_ptr_ptr = _get_zval_ptr_ptr_cv(&opline->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) {
// 重视此处的SEPARATE_ZVAL_IF_NOT_REF
// 它会从头复制一个数组出来
// 真正分别¥tmp和¥arr,变成了内存中的2个数组
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的具体实现(均位于php-src/zend/zend_vm_execute.h中),本文不做具体解析:)