L'abstraction procédurale et les fonctions
L'abstraction procédurale est un principe de programmation qui consiste à structurer un programme en un ensemble de fonctions (ou procédures) qui sont des séquences d'instructions qui permettent de produire des résultats ou des effets sans exposer leurs implémentations. L'objectif de ce principe est que chaque fonction accomplisse une tâche spécifique et généralement simple pour qu'en les combinant, elles résolvent un problème global. Cela permet de rendre le code plus lisible et de réutiliser des fragments de code dans différents contextes au lieu de juste dupliquer ces fragments.
Pour créer une fonction en C, on va faire ce qu'on appelle une définition de fonction. Elle est composée de plusieurs éléments :
- une précondition qui est une condition devant être vraie lors de l'appel de la fonction (similaire à la précondition des boucles).
- une postcondition qui est une condition devant être vraie lors de la sortie de la fonction (similaire à la postcondition des boucles).
- un type de retour qui indique le type de valeur que la fonction renvoie (ou
voidsi elle ne renvoie rien). - un nom (ou identifiant) qui identifie la fonction.
- une liste de paramètres (ou arguments formels) entre parenthèses qui spécifie les entrées (et parfois les sorties) de la fonction (ou
voidsi elle n'a pas de paramètres). - un corps entre accolades qui contient les instructions que la fonction exécute.
Certains de ces éléments peuvent être regroupés pour former des concepts plus larges :
- les spécifications de la fonction qui comprennent la précondition et la postcondition.
- l'en-tête (ou signature) de la fonction qui comprend le type de retour, le nom et la liste des paramètres.
Exemple
Un exemple de fonction que vous avez déjà vue est la fonction main qui est le point d'entrée de tout programme C, par exemple :
int main(void) {
int a = 5;
return 0;
}
Comme vous pouvez le voir, la fonction main a un type de retour int, pas de paramètres (indiqué par void) et son corps contient deux instructions :
la déclaration et l'initialisation d'une variable entière a avec la valeur 5 et une instruction return pour renvoyer la valeur 0 qui indique que le programme
s'est terminé avec succès à l'OS. Dans le cas de la fonction main, on omet souvent de mentionner explicitement la précondition et la postcondition car elles sont implicites
mais c'est une bonne pratique de les inclure pour les autres fonctions.
Un autre exemple simple est la fonction max qui prend deux entiers en paramètres et renvoie le plus grand des deux :
// PRE: /
// POST: renvoie le plus grand des deux entiers a et b.
int max(int a, int b) {
if (a > b) {
return a;
} else {
return b;
}
}
Ici, la fonction max a un type de retour int, deux paramètres entiers a et b, et un corps qui utilise une sélection
pour déterminer lequel des deux entiers est le plus grand et le renvoyer. La précondition est vide (indiquée par /) car les types
des paramètres garantissent déjà que les entrées soient des entiers donc il n'y a pas de conditions supplémentaires à vérifier, et
la postcondition indique plus précisément ce que la fonction fait.
Les appels de fonctions
Pour utiliser une fonction, on va faire ce qu'on appelle un appel de fonction. Cela consiste à écrire le nom de la fonction suivi de parenthèses
contenant les arguments (ou arguments effectifs) de la fonction. Voici un exemple d'appel de la fonction max définie précédemment :
Dans cet exemple, vous pouvez voir l'intérêt des fonctions pour réutiliser des fragments de code car, plutôt que de dupliquer le if qui compare les deux entiers,
on peut simplement appeler la fonction max chaque fois qu'on a besoin de trouver le maximum entre deux entiers.
Vous pouvez également voir un comportement important des fonctions : elles créent un nouvel environnement d'exécution. Quand l'on passe des arguments à une fonction, ces arguments sont copiés dans les paramètres de la fonction. Cela signifie que les modifications apportées aux paramètres à l'intérieur de la fonction n'affectent pas les arguments originaux dans l'environnement d'appel (même si les paramètres et les arguments ont le même nom). C'est ce qu'on appelle le passage par valeur qui est le mode de passage des arguments en C. Cependant, en C, on peut faire passer par valeur une référence à une variable grâce à l'utilisation de pointeurs. Dans ce cas, on appelle plutôt cela le passage par référence et cela permet à la fonction de modifier la variable originale. Voici un exemple pour illustrer cela :
Grâce à cet exemple, vous avez normalement mieux compris ce qu'il se passe en réalité quand vous appelez les fonctions printf et scanf que vous avez utilisées
dans les exercices précédents. Si vous vous souvenez bien, on avait dit qu'on allait voir plus tard comment fonctionnaient ces fonctions
et c'est ce que nous venons de faire.
Cet exemple permet aussi de rappeler le concept d'argument de sortie qui permet de retourner une valeur via une référence passée en argument. Voici un petit récapitulatif des différents types d'arguments :
- Argument d'entrée : un argument qui est utilisé pour fournir des valeurs à la fonction.
- Argument de sortie : un argument qui est utilisé pour renvoyer des valeurs de la fonction vers l'appelant via une référence.
- Argument d'entrée-sortie : un argument qui est utilisé à la fois pour fournir des valeurs à la fonction et pour renvoyer des valeurs de la fonction vers l'appelant via une référence.
Les fichiers d'en-tête
Comme dit juste avant, vous avez utilisé les fonctions printf et scanf mais comment est-ce que cela est possible alors vous ne les avez pas définies ?
En réalité, la majorité des fonctions qu'on utilise quand on programme ont déjà été écrites par d'autres personnes qui avaient le même besoin que nous.
Ces fonctions sont définies dans des modules regroupés dans des bibliothèques (ou librairies) comme le module stdio de la bibliothèque standard qui contient les fonctions printf et scanf.
Pour mettre à disposition ces fonctions, on utilise des fichiers d'en-tête qui contiennent les déclarations des fonctions présentes dans ces modules.
Une déclaration de fonction est similaire à une définition de fonction mais elle ne contient pas le corps de la fonction
car la fonction a déjà été compilée et donc on a juste besoin d'avoir suffisamment d'informations pour savoir comment l'appeler correctement.
Par exemple, la déclaration de la fonction max serait juste :
// PRE: n >= 0 // POST: renvoie le plus grand des deux entiers a et b. int max(int a, int b);
Pour que le compilateur comprenne que l'on veut utiliser un module, on utilise la macro #include suivie du nom de son fichier d'en-tête entre "" pour les modules locaux
et entre <> pour les modules globaux. Par exemple, pour utiliser les fonctions printf et scanf du module stdio, on va inclure le fichier d'en-tête stdio.h
comme vous l'avez déjà fait dans les exercices précédents :
#include <stdio.h>
int main(void) {
printf("Hello, World!\n");
return 0;
}
Par contre, si on avait créé un module local pour la fonction max et qu'on avait mis sa déclaration dans un fichier d'en-tête max.h, on l'inclurait comme ceci :
#include "max.h"
int main(void) {
int m = max(3, 5);
return 0;
}
Nous verrons dans la section suivante comment créer nos propres fichiers d'en-tête, modules et bibliothèques mais pour l'instant, nous allons nous concentrer sur les appels de fonctions déjà définies.
INGInious