Passage par valeur et passage par référence

Difficulté Facile
Note
Thématiques
Mis à jour le mercredi 2 avril 2014

La fin de ce chapitre est consacrée à trois notions un peu plus avancées. Vous pourrez toujours y revenir plus tard si nécessaire.

Passage par valeur

La première des notions avancées dont je dois vous parler est la manière dont l'ordinateur gère la mémoire dans le cadre des fonctions.

Prenons une fonction simple qui ajoute simplement 2 à l'argument fourni. Vous commencez à bien la connaître. Je l'ai donc modifiée un poil.

int ajouteDeux(int a)
{
    a+=2;
    return a;
}

Testons donc cette fonction. Je pense ne rien vous apprendre en vous proposant le code suivant :

#include <iostream>
using namespace std;

int ajouteDeux(int a)
{
    a+=2;
    return a;
}

int main()
{
    int nombre(4), resultat;
    resultat = ajouteDeux(nombre);
    
    cout << "Le nombre original vaut : " << nombre << endl;
    cout << "Le resultat vaut : " << resultat << endl;
    return 0;
}

Cela donne sans surprise :

Le nombre original vaut : 4
Le resultat vaut : 6

L'étape intéressante est bien sûr ce qui se passe à la ligne resultat = ajouteDeux(nombre);. Vous vous rappelez les schémas de la mémoire ? Il est temps de les ressortir.

Lors de l'appel à la fonction, il se passe énormément de choses :

  1. Le programme évalue la valeur de nombre. Il trouve $$4$$.

  2. Le programme alloue un nouvel espace dans la mémoire et y écrit la valeur $$4$$. Cet espace mémoire possède l'étiquette a, le nom de la variable dans la fonction.

  3. Le programme entre dans la fonction.

  4. Le programme ajoute $$2$$ à la variable a.

  5. La valeur de a est ensuite copiée et affectée à la variable resultat, qui vaut donc maintenant $$6$$.

  6. On sort alors de la fonction.

Ce qui est important, c'est que la variable nombre est copiée dans une nouvelle case mémoire. On dit que l'argument a est passé par valeur. Lorsque le programme se situe dans la fonction, la mémoire ressemble donc à ce qui se trouve dans le schéma de la figure suivante.

État de la mémoire dans la fonction après un passage par valeur

On se retrouve donc avec trois cases dans la mémoire. L'autre élément important est que la variable nombre reste inchangée.

Si j'insiste sur ces points, c'est bien sûr parce que l'on peut faire autrement.

Passage par référence

Vous vous rappelez les références ? Oui, oui, ces choses bizarres dont je vous ai parlé il y a quelques chapitres. Si vous n'êtes pas sûrs de vous, n'hésitez-pas à vous rafraîchir la mémoire. C'est le moment de voir à quoi servent ces drôles de bêtes.

Plutôt que de copier la valeur de nombre dans la variable a, il est possible d'ajouter une « deuxième étiquette » à la variable nombre à l'intérieur de la fonction. Et c'est bien sûr une référence qu'il faut utiliser comme argument de la fonction.

int ajouteDeux(int& a) //Notez le petit & !!!
{
    a+=2;
    return a;
}

Lorsque l'on appelle la fonction, il n'y a plus de copie. Le programme donne simplement un alias à la variable nombre. Jetons un coup d'œil à la mémoire dans ce cas (figure suivante).

État de la mémoire dans la fonction après un passage par référence

Cette fois, la variable a et la variable nombre sont confondues. On dit que l'argument a est passé par référence.

Quel intérêt y a-t-il à faire un passage par référence ?

Cela permet à la fonction ajouteDeux() de modifier ses arguments ! Elle pourra ainsi avoir une influence durable sur le reste du programme. Essayons pour voir. Reprenons le programme précédent, mais avec une référence comme argument. On obtient cette fois :

Le nombre original vaut : 6
Le resultat vaut : 6

Que s'est-il passé ? C'est à la fois simple et compliqué.
Comme a et la variable nombre correspondent à la même case mémoire, faire a+=2 a modifié la valeur de nombre !
Utiliser des références peut donc être très dangereux. C'est pour cela qu'on ne les utilise que lorsqu'on en a réellement besoin.

Justement, est-ce qu'on pourrait avoir un exemple utile ?

J'y viens, j'y viens. Ne soyez pas trop pressés.
L'exemple classique est la fonction echange(). C'est une fonction qui échange les valeurs des deux arguments qu'on lui fournit :

void echange(double& a, double& b)
{
    double temporaire(a); //On sauvegarde la valeur de 'a'
    a = b;                //On remplace la valeur de 'a' par celle de 'b'
    b = temporaire;       //Et on utilise la valeur sauvegardée pour mettre l'ancienne valeur de 'a' dans 'b'
}

int main()
{
    double a(1.2), b(4.5);

    cout << "a vaut " << a << " et b vaut " << b << endl;

    echange(a,b);   //On utilise la fonction

    cout << "a vaut " << a << " et b vaut " << b << endl;
    return 0;
}

Ce code donne le résultat suivant :

a vaut 1.2 et b vaut 4.5
a vaut 4.5 et b vaut 1.2

Les valeurs des deux variables ont été échangées.

Si l'on n'utilisait pas un passage par référence, ce seraient alors des copies des arguments qui seraient échangées, et non les vrais arguments. Cette fonction serait donc inutile.
Je vous invite à tester cette fonction avec et sans les références. Vous verrez ainsi précisément ce qui se passe.

A priori, le passage par référence peut vous sembler obscur et compliqué. Vous verrez par la suite qu'il est souvent utilisé. Vous pourrez toujours revenir lire cette section plus tard si les choses ne sont pas encore vraiment claires dans votre esprit.

Avancé : Le passage par référence constante

Puisque nous parlons de références, il faut quand même que je vous présente une application bien pratique. En fait, cela nous sera surtout utile dans la suite de ce cours mais nous pouvons déjà prendre un peu d'avance.

Le passage par référence offre un gros avantage sur le passage par valeur : aucune copie n'est effectuée. Imaginez une fonction recevant en argument un string. Si votre chaîne de caractères contient un très long texte (la totalité de ce livre par exemple !), alors la copier va prendre du temps même si tout cela se passe uniquement dans la mémoire de l'ordinateur. Cette copie est totalement inutile et il serait donc bien de pouvoir l'éliminer pour améliorer les performances du programme.
Et c'est là que vous me dites : « utilisons un passage par référence ! ». Oui, c'est une bonne idée. En utilisant un passage par référence, aucune copie n'est effectuée. Seulement, cette manière de procéder a un petit défaut : elle autorise la modification de l'argument. Eh oui, c'est justement dans ce but que les références existent.

void f1(string texte);  //Implique une copie coûteuse de 'texte' 
{
}

void f2(string& texte);  //Implique que la fonction peut modifier 'texte' 
{
}

La solution est d'utiliser ce que l'on appelle un passage par référence constante. On évite la copie en utilisant une référence et l'on empêche la modification de l'argument en le déclarant constant.

void f1(string const& texte);  //Pas de copie et pas de modification possible
{
}

Pour l'instant, cela peut vous sembler obscur et plutôt inutile. Dans la partie II de ce cours, nous aborderons la programmation orientée objet et nous utiliserons très souvent cette technique. Vous pourrez toujours revenir lire ces lignes à ce moment-là.

Les auteurs