在PHP中的每一个问题的深入分析
前言:uff1a在PHP4的foreach结构的引入是一个简单的方法来遍历数组。相比传统的循环,foreach是得到键值对更方便。在PHP5,foreach仅能用于阵列;在PHP5,foreach可以用来遍历对象(见:遍历对象)。这篇文章只讨论遍历数组的情况。
一是简单的,但它可能会有一些意外的行为,特别是当涉及到参考代码。
以下几种情况列表,帮助我们进一步了解foreach的本质。
问题1:
复制代码代码如下所示:
$ ARR =阵列(1,2,3);
foreach(ARR美元美元美元K = v){
$ = $ v * 2;
}
ARR是 / /美元现在阵列(2, 4, 6)
foreach(ARR美元美元美元K = v){
}
首先,从简单的开始,如果我们尝试运行上面的代码,我们会发现最终的输出是0=2 = 1 = 4 = 2 = 4。
为什么不是0 = 2 1 = 4 2 = 6
事实上,我们可以认为foreach(ARR美元美元K = > $ V)结构意味着以下操作分别阵列电流'关键'和'电流的值赋给变量$ K至五具体展开如下:
复制代码代码如下所示:
foreach(ARR美元美元美元K = v){
2用户代码执行前隐含的赋值
$ V = currentval();
$ k =当前的();
运行用户代码
......
}
根据上述理论,我们再分析第一个:
第一个周期,因为V是一个参考,所以$ V = $ ARR { 0 },V = V×2美元美元,相当于$ ARR { 0 }×2美元,因此成为2,2,3 ARR
第二次,$ V = $ ARR { 1 },ARR为2,4,3美元
第三倍,$ V = $ ARR { 2 },ARR为2美元
然后代码进入第二个:
第一环,隐藏运行$ V = $ ARR { 0 }被触发,因为$ V仍然是一个参考至ARR { 2 },即为ARR { 2 } = $ ARR { 0 }等效,美元对2,4,2 ARR
第二次,$ V = $ ARR { 1 },即$ ARR { 2 } = $ ARR { 1 },还成为三美元
第三倍,$ V = $ ARR { 2 },即$ ARR { 2 } = $ ARR { 2 },还成为三美元
好的,分析完成了。
如何解决类似问题PHP手册上有一个提示:
Warning: the $value reference of the last element of the array is still retained after the foreach loop.It is suggested that unset () be used to destroy it.
复制代码代码如下所示:
$ ARR =阵列(1,2,3);
foreach(ARR美元美元美元K = v){
$ = $ v * 2;
}
unset($ V);
foreach(ARR美元美元美元K = v){
}
1 = 4 2 = 6 0 2输出
我们可以从这个问题,参考可能伴有副作用。如果你不想在不知不觉中改性使阵列的含量变化,最好是把这些引用的时间设置。
问题2:
复制代码代码如下所示:
$ ARR =阵列(A,B,C);
foreach(ARR美元美元美元K = v){
回声的关键($ ARR),= >
}
1 = 1 = 1
这个问题更奇怪,根据手册,键和电流分别是数组中当前元素的键值。
那么为什么关键($ ARR)已经1,电流($ ARR)是B
看看编译码:先用VLD
我们可以看到从分配指令的第三线,它表示数组(A,B,C)为改编
因为美元ARR CV,阵列(A,B,C)是TMP,分配指示找到一个函数,实际上执行zend_assign_spec_cv_tmp_handler.it应该特别指出的是,简历是一个变量缓存PHP5.1环境后增加zval **利用它存储阵列形式是活变量缓存再次使用时不需要寻找积极的符号表,而是直接得到CV数组,数组的访问比哈希快得多,从而提高效率。
复制代码代码如下所示:
静态变量zend_fastcall zend_assign_spec_cv_tmp_handler(zend_opcode_handler_args)
{
zend_op * opline =前(opline);
zend_free_op free_op2;
zval *值= _get_zval_ptr_tmp(opline -> OP2,EX(TS),free_op2 tsrmls_cc);
/ /创建$ *指针数组arr的CV
zval ** variable_ptr_ptr = _get_zval_ptr_ptr_cv(opline -> OP1、前(TS),bp_var_w tsrmls_cc);
如果(is_cv = = variable_ptr_ptr { is_var!)
......
}
{其他
/ /阵列为ARR分配
价值= zend_assign_to_variable(variable_ptr_ptr,价值1 tsrmls_cc);
如果(!return_value_unused(opline ->结果)){
ai_set_ptr(ex_t(opline ->结果。u.var)。Var,值);
pzval_lock(价值);
}
}
zend_vm_next_opcode();
}
指定的指令完成后,CV数组添加到zval *指针和指针指向的数组,这表明美元已缓存的CV ARR。
接下来,执行数组的循环操作,我们看fe_reset指令,这相当于zend_fe_reset_spec_cv_handler执行功能:
复制代码代码如下所示:
静态变量zend_fastcall zend_fe_reset_spec_cv_handler(zend_opcode_handler_args)
{
......
如果(…{)
......
{人}
按CV数组获取指向数组的指针
array_ptr = _get_zval_ptr_cv(opline -> OP1、前(TS),bp_var_r tsrmls_cc);
......
}
......
/ /保存指针将指向数组zend_execute_data -> TS(TS是用来储存的temp_variable代码执行期)
ai_set_ptr(ex_t(opline ->结果。u.var)。Var,array_ptr);
pzval_lock(array_ptr);
如果(ITER){
......
} else if((fe_ht = hash_of(array_ptr))!= NULL){
复位/内部指针数组
zend_hash_internal_pointer_reset(fe_ht);
如果(CE){
......
}
is_empty = zend_hash_has_more_elements(fe_ht)!=成功;
ex_t(opline ->结果。u.var)。fe.fe_pos / /设置保存内部指针数组
zend_hash_get_pointer(fe_ht,ex_t(opline ->结果。u.var)。铁。fe_pos);
{人}
......
}
......
}
这里有2个主要的指针在zend_execute_data -> Ts:
ex_t(opline ->结果。u.var)。Var -指针数组
ex_t(opline ->结果。u.var)。fe.fe_pos指针到数组的内部要素
该fe_reset指令执行后,在内存中的实际情况如下:
然后我们继续看fe_fetch,对应于zend_fe_fetch_spec_var_handler执行功能:
复制代码代码如下所示:
静态变量zend_fastcall zend_fe_fetch_spec_var_handler(zend_opcode_handler_args)
{
zend_op * opline =前(opline);
注意指针是从ex_t /(opline -> OP1。u.var)Var.ptr获得。
zval *数组ex_t(opline -> OP1。u.var)。Var.ptr;
......
开关(zend_iterator_unwrap(阵列,ITER tsrmls_cc)){
违约:
案例zend_iter_invalid:
......
案例zend_iter_plain_object:{
......
}
案例zend_iter_plain_array:
fe_ht = hash_of(阵列);
特别注意/:
的fe_reset指令指针或数组内部的元素存储在ex_t(opline -> OP1。u.var fe.fe_pos)。
在这里获取指针
zend_hash_set_pointer(fe_ht,ex_t(opline -> OP1。u.var)。铁。fe_pos);
获取该元素的值
如果(zend_hash_get_current_data(fe_ht,((void *)值)= =失败){
zend_vm_jmp(前(op_array)->操作码+ opline -> OP2。u.opline_num);
}
如果(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_pos);
打破;
案例zend_iter_object:
......
}
......
}
据fe_fetch,我们一般理解foreach(ARR美元美元K = > $ V)做的。它基于对zend_execute_data - > TS指针数组元素,并移动指针到下一位置再成功后保存的指针。
简单地说,因为fe_fetch移动阵列在第一周期的第二元件的内部指针,所以当我们呼叫键($ ARR)和电流($ ARR)在foreach,实际上我们得到1 and'b。
那么为什么它要输出3个1
我们继续看第九行和第十三行的send_ref指令,这表明美元的ARR参数压。然后do_fcall指令通常是用来打电话的关键和当前的functions.php不会编译为本地机器码,所以PHP使用这样一个操作码指令来模拟实际的CPU和内存的工作的方式。
期待在PHP源代码的send_ref:
复制代码代码如下所示:
静态变量zend_fastcall zend_send_ref_spec_cv_handler(zend_opcode_handler_args)
{
......
/ /指针数组访问指针美元从简历
varptr_ptr = _get_zval_ptr_ptr_cv(opline -> OP1、前(TS),bp_var_w tsrmls_cc);
......
这里又是拷贝变量分隔符,这是键数组的一个特殊函数。
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是宏:
复制代码代码如下所示:
#定义separate_zval_to_make_is_ref(ppzv)
如果(!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结构采用下面的蓝色阵列,它将通过一个,B、C依次。
键和电流在上面使用一个黄色数组,它的内部指针总是指向B。
在这一点上,我们理解为什么键和当前已经返回到数组的第二个元素。因为没有外部代码作用于复制的数组,所以它的内部指针永远不会移动。
问题3:
复制代码代码如下所示:
$ ARR =阵列(A,B,C);
foreach(ARR美元美元美元K = v){
回声的关键($ ARR),>,电流($ ARR);
2,打印1
只有一个差异的问题2和问题之间的问题:在foreach使用参考。看看VLD的问题和发现问题的操作码是2码编译。所以我们使用问题2跟踪方法逐步看相应的操作码的实现。
首先,每一个会打电话给fe_reset:
复制代码代码如下所示:
Static int ZEND_FASTCALL ZEND_FE_RESET_SPEC_CV_HANDLER (ZEND_OPCODE_HANDLER_ARGS)
{
......
如果(opline -> extended_value zend_fe_reset_variable){
从CV / GET变量
array_ptr_ptr = _get_zval_ptr_ptr_cv(opline -> OP1、前(TS),bp_var_r tsrmls_cc);
如果(array_ptr_ptr = = null | | = =如array_ptr_ptr(uninitialized_zval_ptr)){
......
}
如果(z_type_pp(array_ptr_ptr)= = is_object){
......
}
{其他
根据数组/遍历
如果(z_type_pp(array_ptr_ptr)= = is_array){
separate_zval_if_not_ref(array_ptr_ptr);
如果(opline -> extended_value zend_fe_fetch_byref){
/ /保存变量数组设置为is_ref
z_set_isref_pp(array_ptr_ptr);
}
}
array_ptr = * array_ptr_ptr;
z_addref_p(array_ptr);
}
{人}
......
}
......
}
对fe_reset实施部分已经在问题2分析。需要特别注意的是,在这里,在这个例子中的foreach收购价值参考,所以在执行,fe_reset将进入另一个分支是不同的问题。
最后,fe_reset将阵列真的is_ref,仅用一个数组在内存中的数据。
其次,分析send_ref:
复制代码代码如下所示:
静态变量zend_fastcall zend_send_ref_spec_cv_handler(zend_opcode_handler_args)
{
......
/ /指针数组访问指针美元从简历
varptr_ptr = _get_zval_ptr_ptr_cv(opline -> OP1、前(TS),bp_var_w tsrmls_cc);
......
变量分离,因为CV本身的变量是这里的引用,而不是一个新的拷贝数组。
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.since阵列已经在is_ref =正确设置变量,它不会被复制一份。换句话说,仍然只有一个副本存储阵列中的数据在这个时间。
上面的图解释了为什么前2个周期输出1 = 2 = > > B C。指针前进的fe_fetch第三周期时间。
复制代码代码如下所示:
zend_api int zend_hash_move_forward_ex(哈希表* HT,hashposition * POS)
{
hashposition *电流= POS收款机:HT—> pinternalpointer;
is_consistent(HT);
如果(*当前){
*电流=(现行)-> plistnext;
返回成功;
别的}
返回失败;
}
由于内部指针指向数组的最后一个元素,前向移动将指向null。在将内部指针指向null之后,我们再次调用键和电流到数组,然后分别返回null和false,这意味着调用失败,这意味着回声不打印字符。
问题4:
复制代码代码如下所示:
$ ARR =阵(1, 2, 3);
$ TMP = $ ARR;
foreach($ TMP $ K = $ V){
$ *=2;
}
var_dump(ARR美元美元,TMP); / /打印什么
这个问题必须每个关系不大,但由于它涉及到每一个,我们一起讨论。)
在代码中,我们首先创建数组arr数组,然后分配至TMP。在下一个foreach循环,修改$ V将数组$ TMP,但这并不影响美元的改编
为什么
这是因为在PHP中,赋值操作是将一个变量的值复制到另一个变量中,因此修改其中一个变量不会影响另一个变量。
PS:这不适用于对象,从PHP5,对象会默认分配的参考,例如:
复制代码代码如下所示:
类{
公共$ = 1;
}
$ = $ =新的;
$=100;
对于同一个对象引用,实际上是为了响应相同的对象引用而返回100、$ A1和A2
在标题后面的代码,我们现在可以确定为TMP =美元ARR实际上是复制值,而整个$ ARR阵列将被复制至TMP了。理论上,在任务完成后,将有2份同一个内存中的数组。
也许有些学生会怀疑,如果数组是大的,不是很慢吗
幸运的是,PHP有一个更聪明的方法。事实上,当TMP =美元美元ARR被执行,还有在内存中只有一个阵列。看看PHP源zend_assign_to_variable实现(从php5.3.26):
复制代码代码如下所示:
静态内联机制* zend_assign_to_variable(zval ** variable_ptr_ptr,zval *值,int is_tmp_var tsrmls_dc)
{
* = * variable_ptr_ptr variable_ptr zval;
zval垃圾;
......
类型对象
如果(z_type_p(variable_ptr)is_object = z_obj_handler_p(variable_ptr,集)){
......
}
引经左值 / /
如果(pzval_is_ref(variable_ptr)){
......
{人}
在refcount__gc = 1 / 案例/
如果(z_delref_p(variable_ptr)= = 0){
......
{人}
gc_zval_check_possible_root(* variable_ptr_ptr);
临时变量
如果(!is_tmp_var){
如果(pzval_is_ref(价值)z_refcount_p(价值)> 0){
alloc_zval(variable_ptr);
* variable_ptr_ptr = variable_ptr;
* variable_ptr = *价值;
z_set_refcount_p(variable_ptr,1);
zval_copy_ctor(variable_ptr);
{人}
/ / $ TMP =美元将运行到这里,
/ /价值是指实际阵列数据指针数组美元,$ TMP指向数据的指针variable_ptr_ptr
只复制指针,没有实际数组的真实拷贝。
* variable_ptr_ptr =价值;
+ 1价值/ refcount__gc值,在这种情况下refcount__gc是1,z_addref_p 2
z_addref_p(价值);
}
{人}
......
}
}
z_unset_isref_pp(variable_ptr_ptr);
}
variable_ptr_ptr回报*;
}
可以看出,TMP =美元美元ARR的本质是复制数组的指针,然后自动加1。数组的引用计数在此时表达的记忆,还有只有一个数组。
因为只有一个数组,当$ TMP在foreach循环中修改,为什么还没有改变美元吗
看看在PHP源代码的zend_fe_reset_spec_cv_handler功能,这是一个操作程序,以及相应的操作fe_reset.the功能负责指向数组的内部指针指向其第一个元素的foreach开始之前。
复制代码代码如下所示:
静态变量zend_fastcall zend_fe_reset_spec_cv_handler(zend_opcode_handler_args)
{
zend_op * opline =前(opline);
array_ptr zval *,* * array_ptr_ptr;
* fe_ht哈希表;
zend_object_iterator *iter = null;
zend_class_entry * = null;
zend_bool is_empty = 0;
fe_reset /变量
如果(opline -> extended_value zend_fe_reset_variable){
array_ptr_ptr = _get_zval_ptr_ptr_cv(opline -> OP1、前(TS),bp_var_r tsrmls_cc);
如果(array_ptr_ptr = = null | | = =如array_ptr_ptr(uninitialized_zval_ptr)){
......
}
一个对象 / /每
如果(z_type_pp(array_ptr_ptr)= = is_object){
......
}
{其他
进入分支机构/会议
如果(z_type_pp(array_ptr_ptr)= = is_array){
的separate_zval_if_not_ref / /注
它将重新复制一个数组。
TMP与美元美元 / /真正的分离度,为2数组在内存中
separate_zval_if_not_ref(array_ptr_ptr);
如果(opline -> extended_value zend_fe_fetch_byref){
z_set_isref_pp(array_ptr_ptr);
}
}
array_ptr = * array_ptr_ptr;
z_addref_p(array_ptr);
}
{人}
......
}
复位/内部指针数组
......
}
从代码中可以看出,执行赋值语句时,实际执行变量的分离是不存在的,但被推迟到使用变量的时间。这也是php中拷贝写机制的实现。
在fe_reset,内存变化如下:
上面的图解释了为什么每一个不为ref_count和is_ref变化对原arr.as美元影响,有兴趣的同学可以阅读详细zend_fe_reset_spec_cv_handler和zend_switch_free_spec_var_handler具体实施。