Les fonctions

Nous avons eu un premier aperçu des fonctions dans le chapitre Comparaison avec Python. Allons plus loin…

En programmation, une fonction est une boite noire :

  • à laquelle on passe entre 0 et n arguments ;

  • et qui retourne entre 0 et 1 valeur.

Note

Certains langages un peu anciens (Pascal, Visual Basic, etc.) font la différence entre :

  • une fonction (au sens mathématique du terme) qui associe une valeur de retour à une ou plusieurs valeurs d’entrée ;

  • et une procédure qui est un regroupement d’instructions ne produisant aucune valeur de retour.

Dans l’informatique moderne, tout est fonction.

Une fonction est dite sans effet de bord, si la seule interaction observable avec le monde extérieur est sa valeur de retour.

Une fonction est dite pure si elle est sans effet de bord et qu’en plus elle produit la même valeur de retour lors de plusieurs exécutions avec les mêmes arguments.

Pour illustrer ces deux concepts, quelques exemples :

  • la fonction sqrt() est sans effet de bord puisqu’elle ne fait que retourner une valeur, sans modifier quoi que ce soit d’autre dans la mémoire ;

  • la fonction sqrt() est également pure puisqu’elle donne systématiquement le même résultat pour le même paramètre ;

  • la fonction printf() a des effets de bords (elle modifie l’affichage, donc une autre zone mémoire). Elle est donc impure ;

  • la fonction gettimeofday() récupère l’heure courante et sa valeur de retour change donc à chaque appel. Elle est sans effet de bord mais impure.

Les fonctions pures sont intéressantes car elles ne modifient pas l’état du programme à l’extérieur de leur corps. Ce qui rend leur maintenance aisée.

Comme on vient de le voir, capturer l’état d’une horloge nécessite une fonction impure. D’autres exemples de fonctions impures nécessaires pour :

  • simuler des phénomènes aléatoires comme on le verra dans le chapitre Les tableaux avec la fonction rand() ;

  • modifier le flux d’entrée/sortie avec printf() par exemple.

On aura besoin également des fonctions impures en C pour d’autres opérations, à cause de limitations du langage. Python ne possède pas ces limitations.

Passage par valeur

On l’a vu, une première différence avec les fonctions de Python est qu’une fonction C doit déclarer son type de retour, et le type de ses paramètres dans sa signature.

Il existe une deuxième différence majeure, pas directement visible à l’examen du code. Rappelons nous qu’une variable C est définie par 4 paramètres :

  • son nom ;

  • son type (qui fixe la taille réservée en mémoire) ;

  • son adresse ;

  • et sa valeur.

Les fonctions C utilisent un passage par copie de valeur. A l’intérieur de la fonction C, le lien avec le nom et l’adresse de la variable passée en argument est perdu.

Pour illustrer ça, implémentons un programme « naïf » d’échange de variable:

 1// swap1.c
 2
 3#include <stdio.h>
 4
 5void swap1(int x, int y)
 6{
 7    int tmp = 0;
 8    tmp = x;
 9    x = y;
10    y = tmp;
11}
12
13int main()
14{
15    int a = 1;
16    int b = 2;
17
18    printf("AVANT : a = %d, b = %d\n", a, b);
19    swap1(a, b);
20    printf("APRES : a = %d, b = %d\n", a, b);
21    return 0;
22}

Important

La fonction swap1() ne retourne aucune valeur. Une telle fonction est de type void.

Exercice

Compiler et exécuter le programme ci-dessus. Quel est le résultat attendu ? Quel est le résultat obtenu ? Pourquoi ? Pour comprendre, coller le code ci dessus dans Python Tutor et observer l’évolution pas à pas de la mémoire au cours de l’exécution.

Les valeurs passées en paramètres à la fonction sont affectées à des variables locales à la fonction, et la fonction swap1() ne peut agir que sur ces variables locales et ne peut donc pas modifier les variables qui appartiennent au segment mémoire de la fonction main().

De façon plus détaillée, dans la fonction principale main(), observons l’état de la mémoire avant l’appel à la fonction swap1() :

../_images/swap1.png

A l’intérieur de la fonction swap1() les variables x, y et tmp sont créées en mémoire (observer le plan d’adressage). x et y sont initialisées avec les valeurs de a et b.

Observons l’évolution de la mémoire durant l’exécution de swap1().

../_images/swap2.png

La permutation a bien lieu à l’intérieur de la fonction mais sur des variables différentes de celles des variables initiales. La zone mémoire qui nous intéresse n’ayant pas été affectée, la permutation attendue n’a pas eu lieu.

../_images/swap3.png

On va voir que l’utilisation de pointeurs apporte une solution élégante à ce problème. Mais avant cela, un petit détour par la mémoire s’impose.

La mémoire

Il est maintenant temps de jeter un oeil à la façon dont la mémoire est organisée.

La mémoire est divisée en plusieurs segments :

  • le segment de code qui contient le code exécutable ;

  • le segment de données qui contient les variables globales ;

  • le segment de pile (stack) qui contient les variables locales et les adresses de retour des fonctions ;

  • le segment de tas (heap) qui contient les variables allouées dynamiquement.


../_images/07-memory-layout.drawio.png

La mémoire statique est utilisée pour stocker le code exécutable et les variables globales (variables déclarées en dehors de toute fonction). La mémoire de tas (heap) est utilisée pour stocker les variables allouées dynamiquement (ce qui sera abordé dans le chapitre Allocation dynamique). La mémoire de pile (stack) est utilisée pour stocker les variables locales et les adresses de retour des fonctions.

Pour le code ci dessus, la mémoire est organisée comme indiqué dans la figure ci dessous.


../_images/07-stack-swap1.drawio.png

Lors de l’appel à la fonction main(), une zone mémoire spécifique et exclusive à la fonction est réservée, dans laquelle on crée les variables a et b.

Lors de l’appel à la fonction swap1(), une autre zone mémoire exclusive (distincte de la précédente) est réservée, dans laquelle les variables x et y sont créées.

Le fait que les zones mémoire sont exclusives à chacune des fonctions, interdit à chacune des fonctions d’accéder à la zone mémoire de l’autre. En particulier, swap1() ne peut pas accéder à la mémoire réservée pour main() et donc pas modifier les variables a et b.

Passage par adresse (référence)

On vient de voir que pour permuter les variables, il fallait identifier sans ambiguité la zone mémoire des variables concernées. C’est ici que les pointeurs s’avèrent indispensables. Plutôt que d’utiliser une copie de la valeur, la fonction swap2() utilise leur adresse passée sous forme de pointeurs. Les pointeurs localisent la zone mémoire à modifier.

 1// swap2.c
 2
 3#include <stdio.h>
 4
 5void swap2(int *x, int *y)
 6{
 7    int tmp=0;
 8    tmp = *x;
 9    *x = *y;
10    *y = tmp;
11}
12
13int main()
14{
15    int a = 1;
16    int b = 2;
17
18    printf("AVANT : a = %d, b = %d\n", a, b);
19    swap2(&a, &b);
20    printf("APRES : a = %d, b = %d\n", a, b);
21    return 0;
22}

Exercice

Compiler et exécuter le programme ci-dessus. Répond il correctement au problème posé ? Pourquoi ? Pour comprendre, coller le code ci dessus dans Python Tutor et observer l’évolution pas à pas de la mémoire au cours de l’exécution.

Effectivement, à l’exécution on obtient bien le résultat attendu

$ ./swap2
AVANT : a = 1, b = 2
APRES : a = 2, b = 1

Observons maintenant en détail la zone mémoire lors de l’exécution du programme.

Dans la fonction principale main(), observons l’état de la mémoire avant l’appel à la fonction swap2() :

../_images/swap4.png

A l’intérieur de la fonction swap() les variables x, y et tmp sont créées en mémoire (observer le plan d’adressage). x et y sont initialisées avec les adresses de a et b. On peut accéder à leur contenu avec l’opérateur de déréférencement *.

../_images/swap5.png

Les valeurs de x et y ne sont pas modifiées. On les utilise pour accéder à la zone mémoire de a et b.

../_images/swap6.png

Après exécution de la fonction swap2(), la permutation est opérationnelle.

../_images/swap7.png

Pour le code ci dessus, la mémoire est organisée comme indiqué dans la figure ci dessous.


../_images/07-stack-swap2.drawio.png

Exercice

Ecrire une fonction sumult() qui calcule le produit et la somme de 2 entiers.

Puisqu’une fonction C ne peut retourner qu’une seule valeur, quel mécanisme doit on mettre en oeuvre pour atteindre l’objectif ? Quelle conséquence pour les arguments ? Combien au total ?

Utiliser la fonction sumult() dans la fonction main() pour produire l’affichage ci dessous.

Avec les valeurs 3 et 4 définies à l’intérieur du programme, on doit obtenir

$ gcc -std=c99 -Wall -Wextra sumult.c -o sumult
$ ./sumult
3 + 4 = 7
3 x 4 = 12