【從原始碼角度看php自增和自減】
自增和自減基礎
學過程式語言的同學應該都可以隨口說出 ++a 和 a++ 的區別,具體的區別如下:
Example | Name | Effect |
---|---|---|
++$ a |
Pre-increment | Increments |
$ a++ |
Post-increment | Returns |
–$ a |
Pre-decrement | Decrements |
$ a– |
Post-decrement | Returns |
++a 表示取 a 的地址,增加記憶體中 a 的值,然後把值放在暫存器中。
a++ 表示取 a 的地址,把 a 的值裝入暫存器中,然後增加記憶體中 a 的值。
php 中的遞增和遞減
在php中,通常情況下的遞增和遞減沒什麼特別,下面我們看一些特殊的遞增和遞減。
$a = NULL; $a++; //int 1
$a = NULL; $a--; //null
$a = true ; $a++; //true
$a = true; $a--; //true
$a = false; $a++; //false
$a = false; $a--; //false
從上面可以看出,php中遞增和遞減運算子不影響布林值;遞減NULL值沒有效果,但是遞增NULL值的結果為1。
$a = 'C'; $a++; //string D
$a = 'C'; $a--; //string C
$a = 'C2'; $a++; //string C3
$a = 'C2'; $a--; //string C2
$a = '2C'; $a++; //string 2D
$a = '2C'; $a--; //string 2C
從上面可以看出,在php中,字串支援遞增,但是不支援遞減。
再看一下下面的程式碼:
$a = 'Z'; $a++; //string AA
$a = 'C9'; $a++; //string D0
‘A’執行遞增,結果為’B’;‘Z’執行遞增,結果為’AA‘,’C9’遞增,結果為’D0’。這和Perl相似。
php原始碼分析:$
a++與++$
a
首先說明一下,此處的分析基於 php5.6 的原始碼分析。
對於字首自增(++$a),包含的opcode為PRE_INC,其最終呼叫的是Zend/zend_vm_execute.h檔案中的ZEND_PRE_INC_SPEC_CV_HANDLER函式,原始碼如下:
static int ZEND_FASTCALL ZEND_PRE_INC_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
USE_OPLINE
zval **var_ptr;
SAVE_OPLINE();
var_ptr = _get_zval_ptr_ptr_cv_BP_VAR_RW(execute_data, opline->op1.var TSRMLS_CC);
if (IS_CV == IS_VAR && UNEXPECTED(var_ptr == NULL)) {
zend_error_noreturn(E_ERROR, "Cannot increment/decrement overloaded objects nor string offsets");
}
if (IS_CV == IS_VAR && UNEXPECTED(*var_ptr == &EG(error_zval))) {
if (RETURN_VALUE_USED(opline)) {
PZVAL_LOCK(&EG(uninitialized_zval));
EX_T(opline->result.var).var.ptr = &EG(uninitialized_zval);
}
CHECK_EXCEPTION();
ZEND_VM_NEXT_OPCODE();
}
SEPARATE_ZVAL_IF_NOT_REF(var_ptr);
if (UNEXPECTED(Z_TYPE_PP(var_ptr) == IS_OBJECT)
&& Z_OBJ_HANDLER_PP(var_ptr, get)
&& Z_OBJ_HANDLER_PP(var_ptr, set)) {
/* proxy object */
zval *val = Z_OBJ_HANDLER_PP(var_ptr, get)(*var_ptr TSRMLS_CC);
Z_ADDREF_P(val);
fast_increment_function(val);
Z_OBJ_HANDLER_PP(var_ptr, set)(var_ptr, val TSRMLS_CC);
zval_ptr_dtor(&val);
} else {
fast_increment_function(*var_ptr);
}
if (RETURN_VALUE_USED(opline)) {
PZVAL_LOCK(*var_ptr);
EX_T(opline->result.var).var.ptr = *var_ptr;
}
CHECK_EXCEPTION();
ZEND_VM_NEXT_OPCODE();
}
fast_increment_function函式實際呼叫的是increment_function函式:
ZEND_API int increment_function(zval op1)
{
switch (Z_TYPE_P(op1)) {
case IS_LONG:
if (Z_LVAL_P(op1) == LONG_MAX) {
/* switch to double */
double d = (double)Z_LVAL_P(op1);
ZVAL_DOUBLE(op1, d+1);
} else {
Z_LVAL_P(op1)++;
}
break;
case IS_DOUBLE:
Z_DVAL_P(op1) = Z_DVAL_P(op1) + 1;
break;
case IS_NULL:
ZVAL_LONG(op1, 1);
break;
case IS_STRING: {
long lval;
double dval;
switch (is_numeric_string(Z_STRVAL_P(op1), Z_STRLEN_P(op1), &lval, &dval, 0)) {
case IS_LONG:
str_efree(Z_STRVAL_P(op1));
if (lval == LONG_MAX) {
/* switch to double */
double d = (double)lval;
ZVAL_DOUBLE(op1, d+1);
} else {
ZVAL_LONG(op1, lval+1);
}
break;
case IS_DOUBLE:
str_efree(Z_STRVAL_P(op1));
ZVAL_DOUBLE(op1, dval+1);
break;
default:
/* Perl style string increment */
increment_string(op1);
break;
}
}
break;
case IS_OBJECT:
if (Z_OBJ_HANDLER_P(op1, do_operation)) {
zval *op2;
int res;
TSRMLS_FETCH();
MAKE_STD_ZVAL(op2);
ZVAL_LONG(op2, 1);
res = Z_OBJ_HANDLER_P(op1, do_operation)(ZEND_ADD, op1, op1, op2 TSRMLS_CC);
zval_ptr_dtor(&op2);
return res;
}
return FAILURE;
default:
return FAILURE;
}
return SUCCESS;
}
首先呼叫_get_zval_ptr_ptr_cv_BP_VAR_RW函式獲取CV(Compiled variable)型別變數;
其次會呼叫fast_increment_function函式,再呼叫increment_function函式,實現變數的增加操作,
在increment_function函式中,會根據變數的型別來進行對應的操作,從上面可以看出,依次判斷的型別有IS_LONG/IS_DOUBLE/IS_NULL/IS_STRING/IS_OBJECT。
如果是IS_LONG型別,若變數達到long的最大值,則將其轉化為double型別後加1,否則直接加1;
如果是IS_DOUBLE型別,則直接加1;
如果是IS_NULL型別,則會呼叫巨集ZVAL_LONG(op1, 1),直接返回long型別的1;
如果是IS_STRING型別,則會先將其轉化為數字型別,然後再根據上面判斷數字型別的邏輯判斷;
如果是IS_OBJECT型別,並且其內部定義了運算子操作的實現,那就呼叫這個handler來處理,進行
簡言之,字首自增實際上操作的是變數本身,在表示式中使用的也是變數本身。
對於字尾自增($a++),包含的opcode為POST_INC,其最終呼叫的是Zend/zend_vm_execute.h檔案中的ZEND_POST_INC_SPEC_CV_HANDLER函式,原始碼如下:
static int ZEND_FASTCALL ZEND_POST_INC_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
USE_OPLINE
zval **var_ptr, *retval;
SAVE_OPLINE();
var_ptr = _get_zval_ptr_ptr_cv_BP_VAR_RW(execute_data, opline->op1.var TSRMLS_CC);
if (IS_CV == IS_VAR && UNEXPECTED(var_ptr == NULL)) {
zend_error_noreturn(E_ERROR, "Cannot increment/decrement overloaded objects nor string offsets");
}
if (IS_CV == IS_VAR && UNEXPECTED(*var_ptr == &EG(error_zval))) {
ZVAL_NULL(&EX_T(opline->result.var).tmp_var);
CHECK_EXCEPTION();
ZEND_VM_NEXT_OPCODE();
}
retval = &EX_T(opline->result.var).tmp_var;
ZVAL_COPY_VALUE(retval, *var_ptr);
zendi_zval_copy_ctor(*retval);
SEPARATE_ZVAL_IF_NOT_REF(var_ptr);
if (UNEXPECTED(Z_TYPE_PP(var_ptr) == IS_OBJECT)
&& Z_OBJ_HANDLER_PP(var_ptr, get)
&& Z_OBJ_HANDLER_PP(var_ptr, set)) {
/* proxy object */
zval *val = Z_OBJ_HANDLER_PP(var_ptr, get)(*var_ptr TSRMLS_CC);
Z_ADDREF_P(val);
fast_increment_function(val);
Z_OBJ_HANDLER_PP(var_ptr, set)(var_ptr, val TSRMLS_CC);
zval_ptr_dtor(&val);
} else {
fast_increment_function(*var_ptr);
}
CHECK_EXCEPTION();
ZEND_VM_NEXT_OPCODE();
}
從上面可以看出,字尾自增與字首自增在底層實現的大部分是類似的,但是不同的點在於,字尾自增多了一個臨時變數,用於儲存原始的變數的值,但是它並沒有字首自增的RETURN_VALUE_USED操作。
簡言之,字尾自增使用的是存放在臨時變數中的值,即變數的原始值,而最終變數本身的值還是會增加。
一些奇怪的php自增和自減
$a = '2D9'; $a++; //string 2E0
$a = '2E0'; $a++; //float 3
$a = '010'; $a++; //11
$a = 010; $a++; //9
第一個很好理解。
對於第二個,並不是我們想的 ‘2E1’,而是3。注意輸出結果為 float 3,原來這裡 $
a=’2E0’,對$
a執行遞增,由於字串$
a中包含’E’,所以會被當作float來取值。科學計數法 2E0 表示 2*10^0 值為2,對其加1則結果為3。
對於第三個,$
a=’010’,對$
a執行遞增,字串$
a會被當作integer來處理,即為10,對其加1則結果為11。
對於第四個,$
a=010,對$
a執行遞增,$
a本來就是數字型別,由於是0開頭,表示8進位制,即為8,對其加1則結果為9。