38 votes

Concaténation de chaînes lors de l'incrémentation

Ceci est mon code:

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

Ne devrait-il pas imprimer 66?

Pourquoi imprime-t-il 76?

66voto

ircmaxell Points 74865

Alright. C'est en fait assez simple de comportement, et il a à voir avec la façon dont les références travailler en PHP. Il n'est pas un bug, mais un comportement inattendu.

PHP utilise en interne de copie sur écriture. Ce qui signifie que les variables internes sont copiées lorsque vous écrivez pour eux (donc $a = $b; n'est pas copier la mémoire jusqu'à ce que vous modifiez en fait l'un d'entre eux). Avec des références, il n'a jamais fait des copies. C'est important pour plus tard.

Regardons ces opcodes:

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

Les deux premiers devraient être assez faciles à comprendre.

  • ATTRIBUEZ - en gros, nous sommes assinging la valeur de 5 dans le compilé variable nommée !0.
  • ASSIGN_REF - Nous sommes en train de créer une référence d' !0 de !1 (la direction n'a pas d'importance)

Jusqu'à présent, c'est simple. Maintenant vient le morceau intéressant:

  • PRE_INC - C'est l'opcode qui fait incrémente la variable. De remarque, c'est qu'il retourne son résultat dans une variable temporaire nommé $2.

Alors, regardons le code source derrière PRE_INC lorsqu'elle est appelée avec une variable:

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();
}

Maintenant, je ne vous attendez pas à comprendre ce qui fait tout de suite (c'est profond moteur de vaudou), mais nous allons marcher à travers elle.

Les deux premiers si les déclarations de vérifier pour voir si la variable est "safe" pour incrémenter (le vérifie d'abord si c'est un objet surchargé, la deuxième vérifie si la variable est la variable d'erreur $php_error).

La prochaine est vraiment le morceau intéressant pour nous. Depuis que nous sommes à la modification de la valeur, il doit préforme de copie sur écriture. Donc il appelle:

SEPARATE_ZVAL_IF_NOT_REF(var_ptr);

Maintenant, rappelez-vous, nous avons déjà défini la variable de référence ci-dessus. Si la variable n'est pas séparé... ce Qui signifie tout ce que nous faisons ici adviendra $b ainsi...

Ensuite, la variable est incrémentée (fast_increment_function()).

Enfin, il définit le résultat que lui-même. C'est la copie sur écriture de nouveau. Ce n'est pas de retourner la valeur de l'opération, mais la variable. Donc, ce PRE_INC retours est toujours une référence à l' $a et $b.

  • POST_INC - Ce comportement est similaire à PRE_INC, sauf pour un fait TRÈS important.

Nous allons vérifier le code source de nouveau:

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);
}

Cette fois, j'ai coupé tous les non-choses intéressantes. Alors, regardons ce qu'il fait.

Tout d'abord, il obtient le retour à la variable temporaire (~3 dans notre code ci-dessus).

Puis il copie la valeur de son argument (!1 ou $b) dans le résultat (et donc la référence est cassé).

Puis il incrémente l'argument.

Maintenant, rappelez-vous, l'argument !1 est la variable $b, ce qui a une référence à l' !0 ($a) et $2, qui, si vous vous en souvenez, était le résultat de l' PRE_INC.

Donc là vous l'avez. Il retourne 76 parce que la référence est maintenue dans PRE_INC du résultat.

Nous pouvons le prouver en forçant une copie, par l'attribution de la pré-inc à une variable temporaire première (par l'attribution normale, qui va casser la référence):

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

Qui fonctionne comme prévu. La preuve

Et on peut reproduire le comportement d'autres (bug) par l'introduction d'une fonction de maintenir la référence:

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

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

Qui fonctionne comme vous allez le voir (76): la Preuve

Remarque: la seule raison pour la fonction séparée ici, c'est que PHP analyseur n'aime pas $c = &++$a;. Nous avons donc besoin d'ajouter un niveau d'indirection par le biais de l'appel de la fonction pour le faire...

La raison pour laquelle je ne pas considérer cela comme un bug, c'est que c'est la façon dont les références sont censés travailler. Pré-incrémentation d'une variable en question sera de retour cette variable. Même un non-référencé variable doit retourner cette variable. Il peut ne pas être ce que vous attendez ici, mais il fonctionne très bien dans presque tous les autres cas...

Le Point Sous-Jacent

Si vous utilisez des références, vous le faites mal à propos de 99% du temps. Donc, ne pas utiliser de références, sauf si vous avez absolument besoin d'eux. PHP est beaucoup plus intelligente que vous pouvez penser à des optimisations de mémoire. Et votre utilisation de références vraiment entrave la façon dont il fonctionne. Ainsi, alors que vous pensez peut-être écrit smart code, vous allez vraiment avoir à écrire de moins en moins efficace et moins respectueux du code de la vaste majorité du temps...

Et si vous voulez en savoir plus sur les Références et comment des variables en PHP, la caisse Une De Mes Vidéos sur YouTube sur le sujet...

0voto

NLZ Points 672

Je pense que la ligne de concaténation complète est d'abord exécutée puis envoyée avec la fonction echo. Par exemple

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


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

EDIT: Également très important, $ b est égal à 7, mais fait écho avant d’ajouter:

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

0voto

Bgi Points 1924

EDIT: ajout de Corbin exemple: https://eval.in/34067

Il y a évidemment un bug dans PHP. Si vous exécutez ce code:

<?php

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

echo "\n";

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

echo "\n";

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

Vous obtenez:

66 76 76

Ce qui signifie que le même bloc (le 1er et le 3ème sont identiques) de code n'est pas toujours le même résultat. Apparemment, la référence et l'incrément sont en train de mettre du PHP dans un faux de l'état.

https://eval.in/34023

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X