213

I have this code:

$a = array ('zero','one','two', 'three');

foreach ($a as &$v) {

}

foreach ($a as $v) {
  echo $v.PHP_EOL;
}

Can somebody explain why the output is: zero one two two .

From zend certification study guide.

Macmade
  • 52,708
  • 13
  • 106
  • 123
Centurion
  • 5,169
  • 6
  • 28
  • 47
  • 71
    `unset($your_used_reference);` every time you use a `foreach($var as &$your_used_reference)`! – Wrikken Jul 22 '10 at 09:39
  • 3
    thanks for point, but I just wanted to understand the logic of how the second foreach works. – Centurion Jul 22 '10 at 12:07
  • 14
    Why is $your_used_reference (or $v in OPs post) not unset automatically? It's scope is the foreach and shouldn't exist beyond? – Hurix Jan 17 '14 at 15:54
  • 16
    @Hurix Loops in PHP don't have their own scope. – Mark Amery Dec 15 '14 at 17:37
  • This question is now [discussed on Meta](https://meta.stackoverflow.com/q/360250/3151675). – Tamás Sengel Dec 05 '17 at 18:40
  • @MarkBaker It seems like this question is better asked than the duplicate target, so maybe you could consider reopening this one and closing the other one instead. – Donald Duck Dec 06 '17 at 14:30
  • 2
    This isn't a duplicate of https://stackoverflow.com/q/4969243 (asked Feb 2011). This question was asked first. The other one is the duplicate. – Sean the Bean Oct 12 '18 at 13:45
  • PHP manual recommends calling `unset($v);` after the first foreach loop to avoid this problem. http://php.net/manual/en/control-structures.foreach.php – Sean the Bean Oct 12 '18 at 14:09

9 Answers9

188

I had to spend a few hours to figure out why a[3] is changing on each iteration. This is the explanation at which I arrived.

There are two types of variables in PHP: normal variables and reference variables. If we assign a reference of a variable to another variable, the variable becomes a reference variable.

for example in

$a = array('zero', 'one', 'two', 'three');

if we do

$v = &$a[0]

the 0th element ($a[0]) becomes a reference variable. $v points towards that variable; therefore, if we make any change to $v, it will be reflected in $a[0] and vice versa.

now if we do

$v = &$a[1]

$a[1] will become a reference variable and $a[0] will become a normal variable (Since no one else is pointing to $a[0] it is converted to a normal variable. PHP is smart enough to make it a normal variable when no one else is pointing towards it)

This is what happens in the first loop

foreach ($a as &$v) {

}

After the last iteration $a[3] is a reference variable.

Since $v is pointing to $a[3] any change to $v results in a change to $a[3]

in the second loop,

foreach ($a as $v) {
  echo $v.'-'.$a[3].PHP_EOL;
}

in each iteration as $v changes, $a[3] changes. (because $v still points to $a[3]). This is the reason why $a[3] changes on each iteration.

In the iteration before the last iteration, $v is assigned the value 'two'. Since $v points to $a[3], $a[3] now gets the value 'two'. Keep this in mind.

In the last iteration, $v (which points to $a[3]) now has the value of 'two', because $a[3] was set to two in the previous iteration. two is printed. This explains why 'two' is repeated when $v is printed in the last iteration.

2ps
  • 15,099
  • 2
  • 27
  • 47
M S
  • 3,995
  • 3
  • 25
  • 36
  • 8
    I love your explanation but this is still one of those things that gets me constantly confused. I have to remember to unset to _stay_ sane. :-) – liamvictor Jul 14 '15 at 10:37
  • 4
    For me, a `reference variable` is the `reference`, in this example `$v`. `$a`, on the other hand, always remains a normal variable and is not a reference variable in my point of view. Therefore, PHP is also not "smart" in that it knows that a variable has no references to it, because it does not really matter for the variable itself if it is referenced somewhere. The important thing to understand is that `$v` is a reference and not a variable. Otherwise a very good explanation. – Matthias S Nov 22 '18 at 18:36
  • 1
    you say "$a[1] will become a reference variable and $a[0] will become a normal variable"; this is not really correct: technically every variable is a reference to a memory space; $v = &$a[1] means $v and $a[1] will be references to the same memory space; – Andrei Diaconescu Jul 12 '22 at 13:19
  • 2
    Great explanation! This is caused by the fact that variables and not scoped to the loop so the reference `$v` is leaked from the first loop. This would be a really nice trick for intentional code obfuscation... As usual, the fix is to `unset($v)` to get rid of the reference or move that code in a new function so that $v gets its own scope and doesn't leak to following code. – Mikko Rantalainen Aug 16 '22 at 13:08
157

Because on the second loop, $v is still a reference to the last array item, so it's overwritten each time.

You can see it like that:

$a = array ('zero','one','two', 'three');

foreach ($a as &$v) {

}

foreach ($a as $v) {
  echo $v.'-'.$a[3].PHP_EOL;
}

As you can see, the last array item takes the current loop value: 'zero', 'one', 'two', and then it's just 'two'... : )

Boaz
  • 19,892
  • 8
  • 62
  • 70
Macmade
  • 52,708
  • 13
  • 106
  • 123
  • 1
    ok, this means that in the last iteration the last item or remains two or it is assigned two again, why not three why it stops at last-1? :) – Centurion Jul 22 '10 at 14:40
  • 5
    It does not stop. The last array item is assigned with the current loop value. So it's assigned 'zero', then 'one', then 'two'. On the last iteration, it's assigned with its very own value, which is 'two', because of the previous iteration. So it simply remains 'two'. – Macmade Jul 22 '10 at 14:46
  • 12
    After improving the output I was able to understand: `$v gets the item [0](zero). $a[3] is now zero $v gets the item [1](one). $a[3] is now one $v gets the item [2](two). $a[3] is now two $v gets the item [3](two). $a[3] is now two` – Eduardo Sep 02 '13 at 02:18
  • 3
    This is so counter intuitive. It would make sense to me that $v would drop out of memory when the loop is left. But, it is what it is. Seems like a flaw in the language. In C, I think $v would be out of scope outside of the loop. – jamador May 27 '15 at 19:32
  • 2
    @jamador The PHP manual recommends calling `unset($item);` after the first foreach loop to avoid this problem. http://php.net/manual/en/control-structures.foreach.php – Sean the Bean Oct 12 '18 at 14:09
  • @jamador The programmer is expected to understand the scoping rules for the programming language. For PHP, the variable scope (including references) is *always* the whole function, similar to JavaScript `var`. – Mikko Rantalainen Aug 16 '22 at 13:10
52

First loop

$v = $a[0];
$v = $a[1];
$v = $a[2];
$v = $a[3];

Yes! Current $v = $a[3] position.

Second loop

$a[3] = $v = $a[0], echo $v; // same as $a[3] and $a[0] == 'zero'
$a[3] = $v = $a[1], echo $v; // same as $a[3] and $a[1] == 'one'
$a[3] = $v = $a[2], echo $v; // same as $a[3] and $a[2] == 'two'
$a[3] = $v = $a[3], echo $v; // same as $a[3] and $a[3] == 'two'

because $a[3] is assigned by before processing.

shadyyx
  • 15,825
  • 6
  • 60
  • 95
TaeL
  • 1,026
  • 9
  • 17
28

I got here just by accident and the OP's question got my attention. Unfortunately I do not understand any of the explanations from the top. Seems to me like everybody knows it, gets it, accetps it, just cannot explain.

Luckily, a pure sentence from PHP documentation on foreach makes this completely clear:

Warning: Reference of a $value and the last array element remain even after the foreach loop. It is recommended to destroy it by unset().

shadyyx
  • 15,825
  • 6
  • 60
  • 95
  • I was thinking about the same thing, why a local variable inside foreach even bother. Thx – Shiji.J Feb 18 '19 at 18:36
  • 1
    This kind of things make PHP to sucks more. I think that this behaviour will induce to developers to introduce bugs in their code and Zend should change this behaviour. – Juan Lago Feb 12 '20 at 09:24
16

I think this code show the procedure more clear.

<?php

$a = array ('zero','one','two', 'three');

foreach ($a as &$v) {
}

var_dump($a);

foreach ($a as $v) {
  var_dump($a);
}

Result: (Take attention on the last two array)

array(4) {
  [0]=>
  string(4) "zero"
  [1]=>
  string(3) "one"
  [2]=>
  string(3) "two"
  [3]=>
  &string(5) "three"
}
array(4) {
  [0]=>
  string(4) "zero"
  [1]=>
  string(3) "one"
  [2]=>
  string(3) "two"
  [3]=>
  &string(4) "zero"
}
array(4) {
  [0]=>
  string(4) "zero"
  [1]=>
  string(3) "one"
  [2]=>
  string(3) "two"
  [3]=>
  &string(3) "one"
}
array(4) {
  [0]=>
  string(4) "zero"
  [1]=>
  string(3) "one"
  [2]=>
  string(3) "two"
  [3]=>
  &string(3) "two"
}
array(4) {
  [0]=>
  string(4) "zero"
  [1]=>
  string(3) "one"
  [2]=>
  string(3) "two"
  [3]=>
  &string(3) "two"
}
Chang
  • 1,163
  • 9
  • 14
14

This question has a lot of explanations provided, but no clear examples of how to solve the problem that this behavior causes. In most cases, you'll probably want the following code in your pass by reference foreach.

foreach ($array as &$row) {
    // Do stuff
}
// Unset to remove the reference
unset($row);
Goose
  • 4,764
  • 5
  • 45
  • 84
5

This :

$a = array ('zero','one','two', 'three');

foreach ($a as &$v) {

}

foreach ($a as $v) {
    echo $v.PHP_EOL;
}

is the same as

$a = array ('zero','one','two', 'three');

$v = &$a[3];

for ($i = 0; $i < 4; $i++) {
    $v = $a[$i];
    echo $v.PHP_EOL; 
}

OR

$a = array ('zero','one','two', 'three');

for ($i = 0; $i < 4; $i++) {
    $a[3] = $a[$i];
    echo $a[3].PHP_EOL; 
}

OR

$a = array ('zero','one','two', 'three');

$a[3] = $a[0];
echo $a[3].PHP_EOL;

$a[3] = $a[1]; 
echo $a[3].PHP_EOL;

$a[3] = $a[2];
echo $a[3].PHP_EOL;

$a[3] = $a[3]; 
echo $a[3].PHP_EOL;
TTT
  • 51
  • 1
  • 2
1

I found this example also tricky. Why that in the 2nd loop at the last iteration nothing happens ($v stays 'two'), is that $v points to $a[3] (and vice versa), so it cannot assign value to itself, so it keeps the previous assigned value :)

bpile
  • 359
  • 3
  • 10
-1

Because if you create a reference to a variable, all names for that variable (including the original) BECOME REFERENCES.

Ahmed Aman
  • 2,373
  • 1
  • 19
  • 33
  • If this is the case then why if you change $v to &$v in the second loop does it change the result i.e. it echoes zero, one, two, three? – Rupert Madden-Abbott Jul 22 '10 at 09:43
  • because when you use &$v in the second loop, you are changing the reference of the variable. Thus, the problem is eliminated. – Ahmed Aman Jul 22 '10 at 09:59