.. _07-fonctions: Les fonctions ============= Nous avons eu un premier aperçu des fonctions dans le chapitre :ref:`03-programme`. 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 :func:`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 :func:`sqrt` est également pure puisqu'elle donne systématiquement le même résultat pour le même paramètre ; - la fonction :func:`printf` a des effets de bords (elle modifie l'affichage, donc une autre zone mémoire). Elle est donc impure ; - la fonction :func:`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 :ref:`08-arrays` avec la fonction :func:`rand` ; - modifier le flux d'entrée/sortie avec :func:`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: .. code-block:: c :linenos: // swap1.c #include void swap1(int x, int y) { int tmp = 0; tmp = x; x = y; y = tmp; } int main() { int a = 1; int b = 2; printf("AVANT : a = %d, b = %d\n", a, b); swap1(a, b); printf("APRES : a = %d, b = %d\n", a, b); return 0; } .. important:: La fonction :func:`swap1` ne retourne aucune valeur. Une telle fonction est de type ``void``. .. admonition:: 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 :func:`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 :func:`main`. De façon plus détaillée, dans la fonction principale :func:`main`, observons l'état de la mémoire avant l'appel à la fonction :func:`swap1` : .. image:: ../images/swap1.png :scale: 100% :align: center A l'intérieur de la fonction :func:`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 :func:`swap1`. .. image:: ../images/swap2.png :scale: 100% :align: center 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. .. image:: ../images/swap3.png :scale: 100% :align: center 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. | .. image:: ../images/07-memory-layout.drawio.png :scale: 100% :align: center | 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 :ref:`11-allocdyn`). 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. | .. image:: ../images/07-stack-swap1.drawio.png :scale: 100% :align: center | Lors de l'appel à la fonction :func:`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 :func:`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, :func:`swap1` ne peut pas accéder à la mémoire réservée pour :func:`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 :func:`swap2` utilise leur adresse passée sous forme de pointeurs. Les pointeurs **localisent** la zone mémoire à modifier. .. code-block:: c :linenos: // swap2.c #include void swap2(int *x, int *y) { int tmp=0; tmp = *x; *x = *y; *y = tmp; } int main() { int a = 1; int b = 2; printf("AVANT : a = %d, b = %d\n", a, b); swap2(&a, &b); printf("APRES : a = %d, b = %d\n", a, b); return 0; } .. admonition:: 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 :func:`main`, observons l'état de la mémoire avant l'appel à la fonction :func:`swap2` : .. image:: ../images/swap4.png :scale: 100% :align: center A l'intérieur de la fonction :func:`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 ``*``. .. image:: ../images/swap5.png :scale: 100% :align: center 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``. .. image:: ../images/swap6.png :scale: 100% :align: center Après exécution de la fonction :func:`swap2`, la permutation est opérationnelle. .. image:: ../images/swap7.png :scale: 100% :align: center Pour le code ci dessus, la mémoire est organisée comme indiqué dans la figure ci dessous. | .. image:: ../images/07-stack-swap2.drawio.png :scale: 100% :align: center | Exercice -------- Ecrire une fonction :func:`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 :func:`sumult` dans la fonction :func:`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 .. #include void sumult(int a, int b, int* sum, int* mult){ *sum = a + b; *mult = a * b; } int main(){ int a = 3; int b = 4; int sum, mult; sumult(a, b, &sum, &mult); printf("%d + %d = %d\n", a, b, sum); printf("%d x %d = %d\n", a, b, mult); }