Passage par valeur

Il est important de bien comprendre la différence entre les arguments formels d’une fonction et ses paramètres réels (ou effectifs). Les arguments formels sont ceux déclarés avec la fonction :

void f(int i, int j, double x)

Les paramètres réels sont ceux qui sont envoyés au moment de l’appel de la fonction :

int i, k, l;
f(i+j, i, k+l);

On notera que ces paramètres peuvent parfaitement avoir des noms différents, ou identiques à ceux des arguments ; cela n’a aucune espèce d’importance, car les noms des arguments n’ont de signification qu’à l’intérieur de la définition de la fonction, et sont oubliés sitôt celle-ci compilée. Le compilateur ne peut donc même pas savoir que l’on a passé un paramètre nommé i à la place d’un argument nommé j ; il peut donc encore moins nous le reprocher.

La seule chose qui compte, c’est la coïncidence des types. On voit que dans notre exemple cette coïncidence n’est pas parfaite ; en effet, si i+j et i sont bien de type int, k+l est aussi entier, et non un double. Dans ce cas, le compilateur tente de faire coïncider les deux types : ici, c’est possible puisque les types entiers et décimaux sont compatibles (imaginer une assignation d = k+l, parfaitement possible). De même, une fonction ayant pour paramètre un pointeur void* pourra accepter n’importe quel pointeur :

void g(void* p)
....
char *s;
g(s)            // ok char* -> void* possible

Par contre, si l’on écrit g(d), où d est de type double, le compilateur protestera, car il n’existe pas de changement de type de double vers void*.

Ces paramètres sont dit passés par valeur, c’est-à-dire que seule leur valeur est connue de la fonction, non leur adresse. En conséquence, il n’est pas possible à une fonction de modifier ses paramètres, même en changeant la valeur des arguments. Par exemple, la fonction d’échange suivant ne marche pas :

void echange(int a, int b)
{
     int c = a;
     a = b; b = c; // tout à fait permis
}                 // ne marche pas : aucun effet

Il est d’ailleurs facile de comprendre pourquoi, puisqu’on peut appeler echange(1,2) ; il serait gênant que 1 et 2 soient échangés !

Que se passe-t-il en réalité ? Le programme dispose d’un espace mémoire spécial appelé pile. Il s’agit d’une structure LIFO (Last In, First Out c’est-à-dire dernier entré, premier sorti). Lorsqu’on appelle une fonction, les arguments sont calculés et placés dans des cases mémoire provisoires situées dans la pile ; ces cases mémoire peuvent être utilisées comme n’importe quelle variable, et en particulier leur contenu peut être modifié. Cependant, à la fin de la fonction, toutes ces cases sont détruites, en ce sens que l’espace qui leur est réservé est remis à la disposition du programme (pour le prochain appel de fonction).

Les variables créées à l’intérieur d’une fonction (comme c dans echange) sont également créées dans la pile au moment de leur déclaration, et détruites après. On dit qu’il s’agit de variables automatiques, en ce sens qu’elles sont gérées entièrement par le compilateur (voir le paragraphe sur les variables et leur visibilité).

Cela peut poser des problèmes avec certaines fonctions qui renvoient un pointeur, par exemple. Imaginons une fonction qui lit une chaîne de caractères particulière sur un périphérique. L’implantation suivante est incorrecte :

char *lecture(void)
{
     char tampon[256];
     // ...lecture de la chaîne dans le tampon
     return tampon;        // NON ! variable détruite !
}

En effet, au retour de la fonction, le tableau tampon est détruit, et le pointeur n’a donc plus aucun sens. Il faut déclarer le tampon en variable statique, indiquant par là au compilateur que cette variable ne doit pas être détruite :

char *lecture(void)
{
     static char tampon[256];
     // ...lecture de la chaîne dans le tampon
     return tampon;        // Ok variable conservée
}

Les variables statiques ne se trouvent pas sur la pile, mais dans le segment de données, dans une partie spéciale. Leur « durée de vie » est celle du programme, et la place mémoire qu’elles occupent ne peut jamais être libérée.

Il faut bien comprendre qu’une variable statique est unique, elle n’est pas recréée à chaque appel de la fonction. En conséquence, si l’on écrit :

char *s1 = lecture(), *s2 = lecture();

les deux pointeurs seront en fait égaux, et le tampon ne contient que la dernière chaîne lue, non la première qui est perdue. Pour éviter ce problème, on peut modifier ainsi la fonction lecture :

char *lecture(void)
{
     char tampon[256];
     // ...lecture de la chaîne dans le tampon
     return strdup(tampon);    // dupliquer
}

La fonction strdup se charge de créer la mémoire et de dupliquer la chaîne qui est son argument. Avec une telle écriture, les pointeurs s1 et s2 seront cette fois différents, et ne pointeront pas sur l’adresse de tampon. On note que dans ce cas, le compilateur ne peut pas savoir combien de fois dans le programme la fonction de lecture sera appelée, donc combien de duplications de chaîne seront faites, donc combien de mémoire il faudra ; à cause de cela, les chaînes créées par strdup sont nécessairement des variables dynamiques placées dans le tas au moment de l’exécution, par un appel à malloc (voir chapitre 3). D’autre part, le programmeur qui appelle la fonction lecture devra penser à libérer la mémoire occupée par s1 et s2 lorsqu’il n’en aura plus besoin.

Précédent Précédent Sommaire Sommaire Suivant Suivant