在強類型的語言當中,當使用一個變量之前,我們需要先聲明這個變量。然而,對于PHP來說, 在使用一個變量時,我們不需要聲明,也不需要初始化,直接對其指派就可以使用,這是如何實作的?
在PHP中沒有對正常變量的聲明操作,如果要使用一個變量,直接進行指派操作即可。在指派操作的同時已經進行聲明操作。 一個簡單的指派操作:
$a = 10;
使用VLD擴充檢視其生成的中間代碼為 ASSIGN。 依此,我們找到其執行的函數為 ZEND_ASSIGN_SPEC_CV_CONST_HANDLER。 (找到這個函數的方法之一:$a為CV,10為CONST,操作為ASSIGN。) CV是PHP在5.1後增加的一個在編譯期的緩存。如我們在使用VLD檢視上面的PHP代碼生成的中間代碼時會看到:
compiled vars: !0 = $a
這個$a變量就是op_type為IS_CV的變量。IS_CV值的設定是在文法解析時進行的。可以參見Zend/zend_complie.c檔案中的zend_do_end_variable_parse函數。
在這個函數中,擷取這個指派操作的左值和右值的代碼為:
zval *value = &opline->op2.u.constant;
zval **variable_ptr_ptr = _get_zval_ptr_ptr_cv(&opline->op1,
EX(Ts), BP_VAR_W TSRMLS_CC);
由于右值為一個數值,我們可以了解為一個常量,則直接取操作數存儲的constant字段, 關于這個字段的說明将在後面的虛拟機章節說明。 左值是通過 _get_zval_ptr_ptr_cv函數擷取zval值。這個函數最後的調用順序為: [_get_zval_ptr_ptr_cv] --> [_get_zval_cv_lookup]
在_get_zval_cv_lookup函數中關鍵代碼為:
zend_hash_quick_find(EG(active_symbol_table), cv->name, cv->name_len+1,
cv->hash_value, (void **)ptr)
這是一個HashTable的查找函數,它的作用是從EG(active_symbol_table)中查找名稱為cv->name的變量,并将這個值指派給ptr。 最後,這個在符号表中找到的值将傳遞給ZEND_ASSIGN_SPEC_CV_CONST_HANDLER函數的variable_ptr_ptr變量。
以上是擷取左值和右值的過程,在這步操作後将執行指派操作的核心操作--指派。指派操作是通過調用zend_assign_to_variable函數實作。 在zend_assign_to_variable函數中,指派操作分為好幾種情況來處理,在程式中就是以幾層的if語句展現。
情況一:指派的左值存在引用(即zval變量中is_ref__gc字段不為0),并且左值不等于右值
這種情形描述起來比較抽象,如下面的示例:
$a = 10;
$b = &$a;
xdebug_debug_zval('a');
$a = 20;
xdebug_debug_zval('a');
試想,如果我們來做這個$b = &$a;的底層實作,我們可能會這樣做:
- 判斷左值是不是已經被引用過了;
- 左值已經被引用,則不改變左值的引用計數,将右值賦與左值;
事實上,ZE也是用同樣的方法來實作,其代碼如下:
if (PZVAL_IS_REF(variable_ptr)) {
if (variable_ptr!=value) {
zend_uint refcount = Z_REFCOUNT_P(variable_ptr);
garbage = *variable_ptr;
*variable_ptr = *value;
Z_SET_REFCOUNT_P(variable_ptr, refcount);
Z_SET_ISREF_P(variable_ptr);
if (!is_tmp_var) {
zendi_zval_copy_ctor(*variable_ptr);
}
zendi_zval_dtor(garbage);
return variable_ptr;
}
}
PZVAL_IS_REF(variable_ptr)判斷is_ref__gc字段是否為0。在左值不等于右值的情況下執行操作。 所有指向這個zval容器的變量的值都變成了*value。并且引用計數的值不變。下面是這種情況的一個示例:
上面的例子的輸出結果:
a:
(refcount=2, is_ref=1),int 10
a:
(refcount=2, is_ref=1),int 20
情況二:指派的左值不存在引用,左值的引用計數為1,左值等于右值
在這種情況下,應該是什麼都不會發生嗎?看一個示例:
$a = 10;
$a = $a;
看上去真的像是什麼都沒有發生, 左值的引用計數還是1,值仍是10 。 然而在這個指派過程中,$a的引用計數經曆了一次加一和一次減一的操作。 如以下代碼:
if (Z_DELREF_P(variable_ptr)==0) { // 引用計數減一操作
if (!is_tmp_var) {
if (variable_ptr==value) {
Z_ADDREF_P(variable_ptr); // 引用計數加一操作
}
...//省略
情況三:指派的左值不存在引用,左值的引用計數為1,右值存在引用
用一個PHP的示例來描述一下這種情況:
$a = 10;
$b = &$a;
$c = $a;
這裡的$c = $a;的操作就是我們所示的第三種情況。 對于這種情況,ZEND核心直接建立一個新的zval容器,左值的值為右值,并且左值的引用計數為1。 也就是說,這種情形$c不會與$a指向同一個zval。 其核心實作代碼如下:
garbage = *variable_ptr;
*variable_ptr = *value;
INIT_PZVAL(variable_ptr); // 初始化一個新的zval變量容器
zval_copy_ctor(variable_ptr);
zendi_zval_dtor(garbage);
return variable_ptr;
在這個例子中,若将 $c = $a; 換成 $c = &$a;,$a,$b和$c三個變量的引用計數會發生什麼變化?将 $b = &$a; 換成 $b = $a; 呢?
情況四:指派的左值不存在引用,左值的引用計數為1,右值不存在引用
這種情形如下面的例子:
$a = 10;
$c = $a;
這時,右值的引用計數加上,一般情況下,會對左值進行垃圾收集操作,将其移入垃圾緩沖池。垃圾緩沖池的功能是在PHP5.3後才有的。 在PHP核心中的代碼展現為:
Z_ADDREF_P(value); // 引用計數加1
*variable_ptr_ptr = value;
if (variable_ptr != &EG(uninitialized_zval)) {
GC_REMOVE_ZVAL_FROM_BUFFER(variable_ptr); // 調用垃圾收集機制
zval_dtor(variable_ptr);
efree(variable_ptr); // 釋放變量記憶體空間
}
return value;
情況五:指派的左值不存在引用,左值的引用計數為大于0,右值存在引用,并且引用計數大于0
一個示範這種情況的PHP示例:
$a = 10;
$b = $a;
$va = 20;
$vb = &$va;
$a = $va;
最後一個操作就是我們的情況五。 使用xdebug看引用計數發現,最終$a變量的引用計數為1,$va變量的引用計數為2,并且$va存在引用。 從源碼層分析這個原因:
ALLOC_ZVAL(variable_ptr); // 配置設定新的zval容器
*variable_ptr_ptr = variable_ptr;
*variable_ptr = *value;
zval_copy_ctor(variable_ptr);
Z_SET_REFCOUNT_P(variable_ptr, 1); // 設定引用計數為1
從代碼可以看出是新配置設定了一個zval容器,并設定了引用計數為1,印證了我們之前的例子$a變量的結果。
除上述五種情況之外,zend_assign_to_variable函數還對全部的臨時變量做了處理。 變量指派的各種操作全部由此函數完成。
變量的銷毀
在PHP中銷毀變量最常用的方法是使用unset函數。 unset函數并不是一個真正意義上的函數,它是一種語言結構。 在使用此函數時,它會根據變量的不同觸發不同的操作。
一個簡潔的例子:
$a = 10;
unset($a);
使用VLD擴充檢視其生成的中間代碼:
compiled vars: !0 = $a
line # * op fetch ext return operands
---------------------------------------------------------------------------------
2 0 > EXT_STMT
1 ASSIGN !0, 10
3 2 EXT_STMT
3 UNSET_VAR !0
4 > RETURN 1
去掉關于指派的中間代碼,得到unset函數生成的中間代碼為 UNSET_VAR,由于我們unse的是一個變量, 在Zend/zend_vm_execute.h檔案中查找到其最終調用的執行中間代碼的函數為: ZEND_UNSET_VAR_SPEC_CV_HANDLER 關鍵代碼代碼如下:
target_symbol_table = zend_get_target_symbol_table(opline, EX(Ts),
BP_VAR_IS, varname TSRMLS_CC);
if (zend_hash_quick_del(target_symbol_table, varname->value.str.val,
varname->value.str.len+1, hash_value) == SUCCESS) {
...//省略
}