最近工作中在foreach中使用引用的時候出現一個怪現象,使用2次foreach的時候數組值發生了改變,代碼示例如下
<?php $arr = array('1','2','3'); foreach($arr as &$row){ } foreach($arr as $row){ }
我的預期結果是1,2,3 但是實際結果輸出1,2,2
奇怪了,遍歷數組難道還會改變數組的值麼,猜測原因肯定出現在&row這個引用上。
在第2個循環裡打印$arr
Array
(
[0] => 1
[1] => 2
[2] => 1
)
Array
(
[0] => 1
[1] => 2
[2] => 2
)
Array
(
[0] => 1
[1] => 2
[2] => 2
)
解釋下具體的執行流程
1.第1個foreach結束,$row成為$arr[3]的引用
2.第2個foreach循環第1次的時候$row=1,所以此時$arr[3]=1,所以此時$arr=array(1,2,1),
3.依次類推,第2個foreach循環結束的時候$row=2,所以此時$arr=array(1,2,2);
在琢磨這個代碼的時候,又翻了下php手冊對引用的解釋: 在 PHP中引用意味著用不同的名字訪問同一個變量內容,這並不像 C 的指針:例如你不能對他們做指針運算,他們並不是實際的內存地址。
最接近的比喻是 Unix 的文件名和文件本身——變量名是目錄條目,而變量內容則是文件本身。引用可以被看作是 Unix 文件系統中的硬鏈接(如果拿windows做比喻那就是快捷方式)。
看到這兒,在手冊上又發現了一段代碼
<?php $array1 = array(1,2); $x = &$array1[1]; // Unused reference $array2 = $array1; // reference now also applies to $array2 ! $array2[1]=22; // (changing [0] will not affect $array1) print_r($array1);
Produces:
Array
(
[0] => 1
[1] => 22 // var_dump() will show the & here
)
結果又出乎意料,注釋掉$x = &$array1[1],改變$array2的值不影響$arry1的值,聯想到php垃圾回收的引用計數器,每個php變量存在於zval容器裡,zval容器除了包含變量的值和類型,還有2個額外的信息,
第1個是is_ref,是個bool值,用來標識變量是否是引用集合,通過這個我就知道這個變量是否是普通變量或者引用變量啦,第2個額外字段是refcount,表示指向這個zval容器的變量個數。
zval結構如下
typedef struct _zval_struct zval; ... struct _zval_struct { /* Variable information */ zvalue_value value; /* value */ zend_uint refcount__gc; zend_uchar type; /* active type */ zend_uchar is_ref__gc; };
$a = 'ok';
xdebug_debug_zval('a');
a:(refcount=1, is_ref=0),string 'ok' (length=2)
$b = &$a;
xdebug_debug_zval('a');
xdebug_debug_zval('b');
a:(refcount=2, is_ref=1),string 'ok' (length=2)
b:(refcount=2, is_ref=1),string 'ok' (length=2)
這時,引用次數是2,因為同一個變量容器被變量a和變量 b關聯.當沒必要時,php不會去復制已生成的變量容器,變量容器在"refcount"變成0時就被銷毀
由於php內置函數debug_zval_dump不能看到zval的is_ref信息,所以這裡使用xdebug_debug_zval,你需要安裝xdebug擴展。
$a = 'ok'; $b = $a; $a = 'no'; xdebug_debug_zval('a'); xdebug_debug_zval('b');
$a最後的值毫無疑問變成了'no',php是怎麼做的呢?
當執行$b=$a;的時候$a和$b指向了同一個zval容器,refcount=2,
最後改變$a的值的時候,會執行php的copy on write機制
PHP的copy on write機制:
php在修改一個變量以前,會檢查refcount值,refcount大於1,php會復制一個新的zval,並將原來zval refcount減1,
並修改symbol_table(符號表),使得兩個變量分離,這個機制就是所謂的copy on write(寫時復制),
$a = 'ok';
$b = &$a;
$b = 'no';
$a,$b最後的值毫無疑問都變成了'no',
執行到第2行的時候$a, $b指向同一個zval容器,refcount=2,is_ref=1,這時回執行php的change on write機制
PHP的change on write機制:
is_ref=1的時候,php會修改zval的值,但不會復制zval,這個過程稱作(change on write:寫時改變)
回到最開始手冊上那個數組賦值的問題,array1和$array2
$array1 = array(1,2); $x = &$array1[1]; // Unused reference $array2 = $array1; // reference now also applies to $array2 ! //xdebug_debug_zval('x'); xdebug_debug_zval('array1'); xdebug_debug_zval('array2'); print_r($array1);
(refcount=2, is_ref=0),
array (size=2)
0 => (refcount=1, is_ref=0),int 1
1 => (refcount=2, is_ref=1),int 2
array2:
(refcount=2, is_ref=0),
array (size=2)
0 => (refcount=1, is_ref=0),int 1
1 => (refcount=2, is_ref=1),int 2
Array ( [0] => 1 [1] => 2 )
可以看到$array1, $array2指向的是同一個zval,而且$array1[1], $array2[1],$x指向的也是同一個zval,
而且屬性ref_count=1,所以修改$array2[1]的值的時候,$array1[1]的值也一起修改了.