Les pointeurs sont indispensables pour écrire certaines fonctions. Cette fiche précise pourquoi, comment et quand déclarer un paramètre pointeur lorsqu'on écrit une fonction.

1. Rappel : mécanisme d'appel de fonction en C

Rappel - rule of thumb : le mécanisme d'appel de fonction en C repose TOUJOURS sur un passage par valeur.

Supposons qu'on veuille une fonction qui échange le contenu de deux variables entières passées en paramètres. Naïvement on pourrait écrire le code suivant. Mais détaillons ce qui se passe...

Echange du contenu de deux variables entières (version incorrecte)


// la fonction qui échange le contenu de deux variables
void echanger_KO(int a, int b) {
  int c; // variable pivot
  printf("  FONCTION AVANT : a=%d et b=%d\n", a, b);
  c = a;
  a = b;
  b = c;
  printf("  FONCTION APRES : a=%d et b=%d\n", a, b);
}

int main() {
  int a, z;
  a = 1;
  z = 2;
  printf("AVANT : a=%d et z=%d\n", a, z);
  echange_KO(a, z); // on appelle la fonction
  printf("APRES : a=%d et z=%d\n", a, z);
  return EXIT_SUCCES;
}
               

A l'exécution, ce code affiche :


              AVANT : a=1 et z=2
                FONCTION AVANT : a=1 et b=2
                FONCTION AVANT : a=2 et b=1
              APRES : a=1 et z=2
            

Ainsi : la fonction echanger_KO() échange bien les valeurs des variables paramètre a et b, mais les variables a et z de la fonction principale main() ne sont PAS échangées.

Pourquoi ? Ce slide rappelle ce qui se passe lors de l'appel de la fonction.

Lorsque la ligne 15 est exécutée, il existe deux variables en mémoire :

TODO image

La ligne 15 affiche donc :


                     AVANT : a=1 et z=2
                   

La ligne 16 lance l'exécution de la fonction, en passant en paramètre à la fonction les valeurs des variables a et z du main(), c'est à dire les valeurs entières 1 et 2.

Durant l'exécution d'une fonction, de nouvelles variables sont créées en mémoire : une pour chaque paramètre de la fonction, et une pour chaque variable locale à la fonction.

Dans notre cas, pendant l'exécution de la fonction, il existe donc 3 nouvelles variables en mémoire : a, b et z.

TODO Image

[En savoir plus] ces nouvelles variables sont créés dans une zone mémoire particulière appéle "pile d'appel de fonction".

Pour le moment, c n'a pas été initialisé.

Les variables-paramètre a et b de la fonction ont elles pour valeur les valeurs passées lors de l'appel de la fonction par le main(), c'est à dire 1 et 2.

On peut remarquer que durant l'exécution de notre fonction il existe en mémoire DEUX variables nommées a en mémoire : celle crée par la fonction main() et celle créée par la fonction echanger_KO(). D'ailleurs, bientôt, elles n'auront plus les mêmes valeurs !

Ce n'est pas un problème. Ce ne sont pas les mêmes variables.

Reprenons le fil. La ligne 4 affiche :


                      FONCTON AVANT : a=1 et b=2
                    

La ligne 5 initialise la valeur de c :

TODO

La ligne 6 affecte la valeur de b à la variable a :

TODO

TODO La ligne 7 change la valeur de b, en lui affectant l'ancienne valeur de a, qu'on avait pris soin de stocker dans la variable "pivot" c.

TODO

En résumé, le code des lignes 5 à 7 de la fonction échange donc bien le contenu des variables a et b de la fonction.

Et la ligne 8 affiche donc :


                      FONCTON APRES : a=2 et b=1
                    

Lorsque l'exécution d'une fonction se termine, toutes les variables nécessaire à son exécution sont détruites en mémoire.

Dans notre cas, les variables a b et c de la fonction sont détruites :

TODO

Puis on retourne au main(), et on passe à l'instruction qui suit l'appel de la fonction.

Notez que les variables du main() n'ont pas été modifiées par l'appel de la fonction !

Le dernier printf() affiche donc :


                      APRES : a=1 et z=2
                    

Ainsi donc, la fonction a bien échangé ses variables-paramètre ... Mais n'a pas échangé les variables du main !

En résumé, ligne 16, lorsqu'on a appellé la fonction, on ne passe lui pas les variables a et z du main(), mais les valeurs de ces variables.

En fait, l'appel de fonction ligne 16 aurait pu tout aussi bien être fait directement avec ces valeurs comme ceci :


                      echange_KO(1, 2);
                    

Cela n'aurait rien changé du tout !

Le programme précédent ne fait donc pas ce qu'on veut : il n'échange pas les valeurs des variables...

Mais alors, comment faire pour qu'une fonction change la valeur d'une variable qu'on lui indique ? Eh bien : on a recours à un passage "par adresse", au moyen d'un paramètre pointeur.

2. Exemple de passage par adresse

Pour que notre fonction echanger(...) puisse échanger les variables du main qu'on veut échanger, il faut qu'elle ait accès à ces variables.

L'idée est, tout simplement, de ne pas passer à la fonction la valeur de ces variables, mais leur adresse. Ainsi, la fonction pourra accéder à la case mémoire de ces variables, et échanger les valeurs qui y sont stockées.

Pour cela, on a recours à des paramètres pointeurs. Voila comment on fait :

Echange du contenu de deux variables entières (version incorrecte)


  // Cette fonction qui échange le contenu de deux variables
  void echanger_OK(int * p1, int * p2) {
    int c; // variable pivot
    printf("  FONCTION AVANT : *p1=%d et *p1=%d\n", *p1, *p2);
    c = *p1;  // on stocke temporairement dans c la valeur entière
              // de la variable pointée par p1.
    *p1 = *p2;
    *p2 = c;  // on met dans la variable pointée par p2
              // la valeur temporaire stockée dans c,
              // ancienne valeur de *p1
    printf("  FONCTION APRES : *p1=%d et *p1=%d\n", *p1, *p2);
  }

  int main() {
    int a, z;
    a = 1;
    z = 2;
    printf("AVANT : a=%d et z=%d\n", a, z);
    echange_OK(&a, &z); // on passe les ADRESSES de a et z à la fonction
    printf("APRES : a=%d et z=%d\n", a, z);
    return EXIT_SUCCES;
  }
                 

Notez que la fonction prend 2 paramètres pointeur.

C'est cela qui permet, lorsqu'on l'exécute, qu'elle pointe des variables de la fonction appelante (ici : "du main()"), et ainsi qu'elle modifie les valeurs de ces variables.

Dans le main, on crée 2 variables entières. Voici la représentation de la mémoire à cette étape :

****TODO image

La ligne 18 affiche donc :


                       AVANT : a=1 et z=2
                     

A la ligne 19, on lance l'exécution de la fonction.

Notez qu'on passe à la fonction les adresses des variables a et z : &a et &z.

Ceci est conforme au prototype de la fonction (cohérence des types) : les paramètres p1 et p2 de la fonction sont de type pointeur-sur-int. Ils attendent donc des adresses de variables entières, et c'est bien ce qu'on leur donne.

Durant l'exécution de la fonction, de nouvelles variables sont créées en mémoire : une pour chaque paramètre de la fonction, et une pour chaque variable locale à la fonction.

Dans notre cas, pendant toute l'exécution de la fonction, il existe donc 3 nouvelles variables en mémoire : p1, p2 et c.

TODO Image

Les nouvelles variables p1 et p2 prennent pour valeur celles passées lors depuis le main(), c'est à dire les adresses des variables a et z du main().

Autrement dit, pendant l'exécution de notre fonction :

  • p1 pointe a
  • etp2 pointe z

Voici l'état de la mémoire au début de la fonction, après la ligne 3 :

TODO Image

La ligne 4 affiche donc les valeurs de a et z :


                            FONCTION AVANT : *p1=1 et *p1=2
                        

Puis ligne 5, on initialise c :

TODO Image

Puis après la ligne 7, notez que la valeur de la variable a a changé !

TODO Image

Puis après la ligne 8, notez que la valeur de la variable b a changé !

TODO Image

Ainsi donc, la fonction a échangé les valeurs des variables pointées par les paramètres p1 et p2.

La ligne 11 affiche :


                            FONCTION AVANT : *p1=2 et *p1=1
                        

L'exécution de la fonction est terminée.

Les variables nécessaires à son exécution sont détruites.

Les valeurs des variables a et z du main() ont bien été échangées !

Cette ligne affiche donc :


                        AVANT : a=2 et z=1
                      

Bravo !

4. Définir le prototype d'une fonction (... et son contrat)