Подтвердить что ты не робот

Конкатенация строк при увеличении

Это мой код:

$a = 5;
$b = &$a;
echo ++$a.$b++;

Не следует ли распечатать 66?

Почему он печатает 76?

4b9b3361

Ответ 1

Хорошо. Это на самом деле довольно прямолинейное поведение, и оно связано с тем, как ссылки работают в PHP. Это не ошибка, а неожиданное поведение.

PHP внутренне использует copy-on-write. Это означает, что внутренние переменные копируются при их записи (поэтому $a = $b; не копирует память до тех пор, пока вы не измените один из них). Со ссылкой он никогда не копирует. Это важно для более позднего.

Посмотрим на эти коды операций:

line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   2     0  >   ASSIGN                                                   !0, 5
   3     1      ASSIGN_REF                                               !1, !0
   4     2      PRE_INC                                          $2      !0
         3      POST_INC                                         ~3      !1
         4      CONCAT                                           ~4      $2, ~3
         5      ECHO                                                     ~4
         6    > RETURN                                                   1

Первые два должны быть легко понятны.

  • ASSIGN. В принципе, мы оцениваем значение 5 в скомпилированной переменной с именем !0.
  • ASSIGN_REF. Мы создаем ссылку от !0 до !1 (направление не имеет значения)

До сих пор это прямо. Теперь идет интересный бит:

  • PRE_INC. Это код операции, который фактически увеличивает эту переменную. Следует отметить, что он возвращает свой результат во временную переменную с именем $2.

Итак, посмотрим исходный код PRE_INC при вызове с переменной:

static int ZEND_FASTCALL  ZEND_PRE_INC_SPEC_VAR_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    USE_OPLINE
    zend_free_op free_op1;
    zval **var_ptr;

    SAVE_OPLINE();
    var_ptr = _get_zval_ptr_ptr_var(opline->op1.var, execute_data, &free_op1 TSRMLS_CC);

    if (IS_VAR == IS_VAR && UNEXPECTED(var_ptr == NULL)) {
        zend_error_noreturn(E_ERROR, "Cannot increment/decrement overloaded objects nor string offsets");
    }
    if (IS_VAR == IS_VAR && UNEXPECTED(*var_ptr == &EG(error_zval))) {
        if (RETURN_VALUE_USED(opline)) {
            PZVAL_LOCK(&EG(uninitialized_zval));
            AI_SET_PTR(&EX_T(opline->result.var), &EG(uninitialized_zval));
        }
        if (free_op1.var) {zval_ptr_dtor(&free_op1.var);};
        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);
        AI_SET_PTR(&EX_T(opline->result.var), *var_ptr);
    }

    if (free_op1.var) {zval_ptr_dtor(&free_op1.var);};
    CHECK_EXCEPTION();
    ZEND_VM_NEXT_OPCODE();
}

Теперь я не ожидаю, что вы сразу поймете, что это делает (это глубокий двигатель вуду), но пропустите его.

Первые два оператора if проверяют, является ли переменная "безопасной" для увеличения (первая проверяет, является ли это перегруженным объектом, вторая проверяет, является ли переменная особой переменной ошибки $php_error).

Далее - это действительно интересный бит для нас. Поскольку мы изменяем значение, ему нужно предварительно форматировать copy-on-write. Поэтому он вызывает:

SEPARATE_ZVAL_IF_NOT_REF(var_ptr);

Теперь, помните, мы уже установили переменную как ссылку выше. Таким образом, переменная не разделяется... Что означает, что все, что мы делаем с ней здесь, будет также с $b...

Затем переменная увеличивается (fast_increment_function()).

Наконец, он устанавливает результат как сам. Это копирование на запись еще раз. Он не возвращает значение операции, а фактическую переменную . Итак, что возвращает PRE_INC, еще ссылка на $a и $b.

  • POST_INC. Это ведет себя аналогично PRE_INC, за исключением одного ОЧЕНЬ важного факта.

Снова проверьте исходный код:

static int ZEND_FASTCALL  ZEND_POST_INC_SPEC_VAR_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    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);
    fast_increment_function(*var_ptr);
}

На этот раз я отрезал все ненужные вещи. Поэтому давайте посмотрим, что он делает.

Во-первых, он получает временную переменную возврата (~3 в нашем коде выше).

Затем он копирует значение из своего аргумента (!1 или $b) в результат (и, следовательно, ссылка не работает).

Затем он увеличивает аргумент.

Теперь помните, что аргумент !1 - это переменная $b, которая имеет ссылку на !0 ($a) и $2, что, если вы помните, было результатом от PRE_INC.

Итак, у вас это есть. Он возвращает 76, потому что ссылка поддерживается в результате PRE_INC.

Мы можем доказать это, выставив копию, предварительно назначив pre-inc во временную переменную (через обычное присвоение, которое нарушит ссылку):

$a = 5;
$b = &$a;
$c = ++$a;
$d = $b++;
echo $c.$d;

Работает так, как вы ожидали. Доказательство

И мы можем воспроизвести другое поведение (вашу ошибку), введя функцию для поддержания ссылки:

function &pre_inc(&$a) {
    return ++$a;
}

$a = 5;
$b = &$a;
$c = &pre_inc($a);
$d = $b++;
echo $c.$d;

Что работает, когда вы его видите (76): Доказательство

Примечание. Единственная причина для отдельной функции здесь заключается в том, что парсер PHP не любит $c = &++$a;. Поэтому нам нужно добавить уровень косвенности через вызов функции, чтобы сделать это...

Причина, по которой я не считаю это ошибкой, заключается в том, что она должна работать. Предварительное приращение переменной, на которую ссылается, вернет эту переменную. Даже переменная без ссылки должна возвращать эту переменную. Возможно, это не то, что вы ожидаете здесь, но оно работает довольно хорошо практически в каждом другом случае...

Базовая точка

Если вы используете ссылки, вы делаете это неправильно около 99% времени. Поэтому не используйте ссылки, если вам не нужны абсолютно. PHP намного умнее, чем вы думаете при оптимизации памяти. И ваше использование ссылок действительно мешает тому, как это может работать. Поэтому, пока вы думаете, что можете писать смарт-код, вы действительно собираетесь писать менее эффективный и менее дружелюбный код в подавляющем большинстве случаев...

И если вы хотите узнать больше о ссылках и о том, как переменные работают в PHP, проверьте одно из моих видеороликов YouTube по этому вопросу...

Ответ 2

Я думаю, что полная строка конкатенации сначала выполняется и отправляется с помощью функции эха. На примере

$a = 5;
$b = &$a;
echo ++$a.$b++;
// output 76


$a = 5;
$b = &$a;
echo ++$a;
echo $b++;
// output 66

EDIT: Также очень важно, что $b равно 7, но эхо до добавления:

$a = 5;
$b = &$a;
echo ++$a.$b++; //76
echo $b;
// output 767

Ответ 3

EDIT: добавление примера Corbin: https://eval.in/34067

Очевидно, что в PHP существует ошибка. Если вы выполните этот код:

<?php

{
$a = 5;
echo ++$a.$a++;
}

echo "\n";

{
$a = 5;
$b = &$a;
echo ++$a.$b++;
}

echo "\n";

{
$a = 5;
echo ++$a.$a++;
}

Вы получаете:

66 76 76

Это означает, что один и тот же блок (1-й и 3-й один) кода не всегда возвращает тот же результат. По-видимому, ссылка и приращение помещают PHP в фиктивное состояние.

https://eval.in/34023