Les références en PHP sont-elles des pointeurs en C/C++ ?

Les références en PHP peuvent sembler assez déroutantes au développeur C/C++. S’agit-il de pointeurs ? La documentation de PHP prétend que non, mais la lecture des commentaires qui y ont été annexés laisse sur une impression bien moins catégorique.
En fait, tout dépend d’où l’on se place : veut-on chercher à comprendre comment les références fonctionnent, ou veut-on simplement déjà parvenir à décrypter le code où il en figure ?
Quelle que soit la manière dont les références fonctionnent réellement, ce qui sème le trouve dans l’esprit du développeur C/C++, c’est qu’il semble que ces références ne sont pas des pointeurs, du fait de la syntaxe.
En effet, soit le code C++ suivant :
int i, *p;
i = 0;
p = &i;
std::cout << *p;	// On écrit *p et non p (comme on a écrit i)
Le code équivalent en PHP serait :
$i = 0;
$p = &$i;
echo ($p);	// Surprise! on écrit $p comme on a écrit $i
Alors, comment le développeur C/C++ doit-il décrypter les références aux références dans le code PHP ? L'objet de cet article est d'éclairer sa lanterne. Plus généralement, tous ceux qui souhaitent utiliser des références en PHP devraient trouver un intérêt à le lire - tout particulièrement pour clarifier la relation critique entre référence et unset ().
Click here to read this article in english.

Les variables en PHP sont (comme) des pointeurs...

En fait, chacun sait que le code PHP qui vient d'être présenté peut être rendu bien plus lisible pour le développeur C/C++ en écrivant le & au = et non au $ de la variable :
$i = 0;
$p =& $i;
echo ($p);
En effet, le développeur C/C++ comprend alors que le & n'est pas le & du C/C++, mais un élément d'un opérateur particulier. Partant, ce développeur peut considérer que les variables PHP ne sont rien de moins que... des pointeurs ! Ainsi, voici comment il pourra traduire mentalement le code PHP précédent :
$a = 0;		// *a = 0
$b =& $a;	// b = a
$b = 1;		// *b = 1 si bien que *a = 1

...ou des pointeurs de pointeurs

La situation est un peu différente pour les objets. En effet, une variable PHP ne contient pas l'objet lui-même, mais un identifiant de cet objet. C'est pourquoi :
class Bird {
	public $name = 'The Great Eagle';
}
$b0 = new Bird ();
$b1 =& $b0;
echo ($b1->name);	// Succès
$b0 = NULL;
echo ($b1->name);	// Echec
Le développeur C/C++ peut décrypter en considérant que la variable est un pointeur de pointeur, une construction très commune dans ces langages :
#include <iostream>

class Bird {
	public :
		char name[16] = "The Great Eagle";
};

int main () {
	Bird *_b0, **b0, **b1;
	_b0 = new Bird ();
	b0 = &_b0;
	std::cout << (*b0)->name;	// Succès
	b1 = b0;
	*b0 = NULL;
	std::cout << (*b1)->name;	// Echec (à l'exécution)
}

Apprenez à penser en PHP !

La grosse suprise, c'est que cette manière de lire les références en PHP du point de vue du développeur C/C++ ne conduit pas à se demander si les références en PHP sont des pointeurs en C/C++, mais si les variables en PHP sont des pointeurs en C/C++, voire des pointeurs de pointeurs.
Et de fait, comme l'explique la documentation de PHP "le nom d'une variable et son contenu sont deux notions distinctes, ce qui fait que l'on peut donner plusieurs noms au même contenu".
L'idée compatible développée dans cet article, c'est qu'un nom de variable peut être vu comme un pointeur intelligent (tant qu'un nom référence un contenu, ce contenu reste alloué). C'est pourquoi le code suivant fonctionne, quelle que soit la nature de la variable (nombre, chaîne, tableau, objet) :
$a = 'Hello, world:';
$b =& $a;
$b = 'Bye-bye, world!';
echo ($a);
unset ($a);				// La chaîne existe toujours, car référencée par le nom "b"
echo ($b);
unset ($b);				// La chaîne n'existe plus, car plus aucun nom ne la référence
Bye-bye, world!
Bye-bye, world!
Tout cela peut aider, mais au final, le développeur C/C++ ne doit pas oublier que traduire du code mentalement ne permet pas d'apprendre comment les références fonctionnenent réellement en PHP. Ce ne sont pas des pointeurs, mais des alias, si bien que les considérer comme tels peut déboucher à terme sur des erreurs d'interprétation.
Le fait est que si vous souhaitez apprendre une langue étrangère, il vous faut parvenir à penser dans cette dernière. Or cela peut s'avérer difficile, car il vous faut alors parvenir à ne plus penser dans votre langue maternelle.

Complément sur les fonctions

Une variable peut être passée par référence à une fonction en précédant le nom de l'argument de & dans la signature (et non dans l'appel !) :
function f (&$value) {
	global $speech;
	$value = 'Bye-bye, world!';
	$value =& $speech;
}
$speech = 'Ich bin ein Berliner';
$message = 'Hello, world!';
f ($message);
echo ($message);
Dans cet exemple, $message n'est pas modifiée par l'affectation par =& de $speech à $value. C'est logique, car c'est le nom "value" qui s'est trouvé lié au contenu auquel le nom "speech" est lié : le nom "message" n'a pas été impliqué.
Notez que toute variable contenant l'instance d'une classe est par défaut passée par référence à une fonction. Pour le comprendre, il faut l'exprimer de manière plus rigoureuse, comme expliqué dans la documentation de PHP. En effet, comme il a été dit plus tôt, dans le cas d'une instance de classe, la variable contient un identifiant permettant d'accéder à l'instance et non l'instance elle-même. Partant, lors d'un appel normal, l'identifiant de l'instance est naturellement copié dans l'argument, si bien que tout se passe comme si la variable présumée contenir l'instance était passée par référence :
class Bird {
	public $name='The Great Eagle';
}

function f ($value) {
	$value->name = 'The Crow';
}

$b = new Bird ();
f ($b);
echo ($b->name);
The Crow
Par ailleurs, une fonction peut retourner une référence. Le nom de la fonction doit alors être précédé de &, et la valeur retournée doit être affectée avec l'opérateur =& pour l'utiliser en tant que référence (autrement, le contenu sera donc dupliqué) :
$speech = 'Ich bin ein Berliner';

function &f () {
	global $speech;
	$value =& $speech;
	return ($value);
}

echo ($speech);
$message =& f ();				// Le contenu lié à "speech" est lié à "message"
$message = 'Hello, world!';		// Le contenu lié à "speech" est modifié
echo ($speech);
unset ($message);				// Délier le nom "message" sinon l'affectation suivante va impacter le contenu lié à "speech" !
$message = f ();				// Le contenu lié à "speech" est dupliqué et lié à "message"
$message = 'Bye-bye, world!';	// Le contenu de "speech" n'est pas modifié
echo ($speech);
Ich bin ein Berliner
Hello, world
Hello, world
L'exemple précédent pointe la vigilance qu'il faut exercer après avoir affecté une référence. La documentation de PHP en souligne l'importance dans le cas de foreach. Au sortir d'un foreach, il faut veiller à utiliser unset () pour briser le lien entre le nom et le dernier élément du tableau si l'on souhaite réutiliser ce nom :
$values = [0, 1, 2];
foreach ($values as $key => &$value) {}
$value = 3;						// Vient modifier $values[2], contenu auquel "value" est encore lié !
echo ($values[3]);
3

Ce que le développeur C/C++ doit assimiler

Pour bien comprendre la notion de référence en PHP, le développeur C/C++ doit faire l'effort d'assimiler deux choses :
  • Il existe une distinction entre le nom d'une variable et son contenu. Plusieurs noms peuvent renvoyer sur un même contenu auquel ils sont liés, et ce contenu existe tant qu'un nom lui est lié, un lien pouvant être brisé par un appel à unset ().
  • Dans le cas d'un objet (l'instance d'une classe), la variable n'est pas liée à l'objet lui-même, mais à un identifiant de cet objet.
Au-delà de tout ce qui a été dit, il convient de faire la lecture de la documentation de PHP pour en apprendre plus sur les références. Ne pas oublier de lire les commentaires, toujours très utiles.