1. 程式人生 > >【從原始碼角度看php自增和自減】

【從原始碼角度看php自增和自減】

自增和自減基礎

學過程式語言的同學應該都可以隨口說出 ++a 和 a++ 的區別,具體的區別如下:

Example Name Effect
++$a Pre-increment Increments abyone,thenreturnsa.
$a++ Post-increment Returns a,thenincrementsa by one.
$a Pre-decrement Decrements abyone,thenreturnsa.
$a– Post-decrement Returns a,th
endecrements
a by one.

++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。

Happy coding.