Nous avons découvert beaucoup de nouveautés dans les chapitres précédents et nos programmes commencent à grossir. C’est pourquoi il est important d’apprendre à les découper en fonctions.
- Qu'est-ce qu'une fonction ?
- Définir et utiliser une fonction
- Les prototypes
- Variables globales et classes de stockage
- Exercices
Qu'est-ce qu'une fonction ?
Le concept de fonction ne vous est pas inconnu : printf()
, scanf()
, et main()
sont des fonctions.
Mais qu’est-ce qu’une fonction exactement et quel est leur rôle exactement ?
Une fonction est :
- une suite d’instructions ;
- marquée à l’aide d’un nom (comme une variable finalement) ;
- qui a vocation à être exécutée à plusieurs reprises ;
- qui rassemble des instructions qui permettent d’effectuer une tâche précise (comme afficher du texte à l’écran, calculer la racine carrée d’un nombre, etc).
Pour mieux saisir leur intérêt, prenons un exemple concret.
#include <stdio.h>
int main(void)
{
int a;
int b;
printf("Entrez deux nombres : ");
scanf("%d %d", &a, &b);
int min = (a < b) ? a : b;
for (int i = 2; i <= min; ++i)
if (a % i == 0 && b % i == 0)
{
printf("Le plus petit diviseur de %d et %d est %d\n", a, b, i);
break;
}
return 0;
}
Ce code, repris du chapitre précédent, permet de calculer le plus petit commun diviseur de deux nombres donnés. Imaginons à présent que nous souhaitions faire la même chose, mais avec deux paires de nombres. Le code ressemblerait alors à ceci.
#include <stdio.h>
int main(void)
{
int a;
int b;
printf("Entrez deux nombres : ");
scanf("%d %d", &a, &b);
int min = (a < b) ? a : b;
for (int i = 2; i <= min; ++i)
if (a % i == 0 && b % i == 0)
{
printf("Le plus petit diviseur de %d et %d est %d\n", a, b, i);
break;
}
printf("Entrez deux autres nombres : ");
scanf("%d %d", &a, &b);
min = (a < b) ? a : b;
for (int i = 2; i <= min; ++i)
if (a % i == 0 && b % i == 0)
{
printf("Le plus petit diviseur de %d et %d est %d\n", a, b, i);
break;
}
return 0;
}
Comme vous le voyez, ce n’est pas très pratique : nous devons recopier les instructions de calcul deux fois, ce qui est assez dommage et qui plus est source d’erreurs. C’est ici que les fonctions entrent en jeu en nous permettant par exemple de rassembler les instructions dédiées au calcul du plus petit diviseur commun en un seul point que nous solliciterons autant de fois que nécessaire.
Oui, il est aussi possible d’utiliser une boucle pour éviter la répétition, mais l’exemple aurait été moins parlant.
Définir et utiliser une fonction
Pour définir une fonction, nous allons devoir donner quatre informations sur celle-ci :
- son nom : les règles sont les mêmes que pour les variables ;
- son corps (son contenu) : le bloc d’instructions à exécuter ;
- son type de retour : le type du résultat de la fonction ;
- d’éventuels paramètres : des valeurs reçues par la fonction lors de l’appel.
La syntaxe est la suivante.
type nom(paramètres)
{
/* Corps de la fonction */
}
Prenons un exemple en créant une fonction qui affiche « bonjour ! » à l’écran.
#include <stdio.h>
void bonjour(void)
{
printf("Bonjour !\n");
}
int main(void)
{
bonjour();
return 0;
}
Comme vous le voyez, la fonction se nomme « bonjour » et est composée d’un appel à printf()
. Reste les deux mots-clés void
:
- dans le cas du type de retour, il spécifie que la fonction ne retourne rien ;
- dans le cas des paramètres, il spécifie que la fonction n’en reçoit aucun (cela se manifeste lors de l’appel : il n’y a rien entre les parenthèses).
Le type de retour
Le type de retour permet d’indiquer deux choses : si la fonction retourne une valeur et le type de cette valeur.
#include <stdio.h>
int deux(void)
{
return 2;
}
int main(void)
{
printf("Retour : %d\n", deux());
return 0;
}
Retour : 2
Dans l’exemple ci-dessus, la fonction deux()
est définie comme retournant une valeur de type int
. Vous retrouvez l’instruction return
, une instruction de saut (comme break
, continue
et goto
). Ce return
arrête l’exécution de la fonction courante et provoque un retour (techniquement, un saut) vers l’appel à cette fonction qui se voit alors attribuer la valeur de retour (s’il y en a une). Autrement dit, dans notre exemple, l’instruction return 2
stoppe l’exécution de la fonction deux()
et ramène l’exécution du programme à l’appel qui vaut désormais 2, ce qui donne finalement printf("Retour : %d\n", 2)
Les paramètres
Un paramètre sert à fournir des informations à la fonction lors de son exécution. La fonction printf()
par exemple récupère ce qu’elle doit afficher dans la console à l’aide de paramètres. Ceux-ci sont définis de la même manière que les variables si ce n’est que les définitions sont séparées par des virgules.
type nom(type paramètres1, type paramètres2, ...)
{
/* Corps de la fonction */
}
Vous pouvez utiliser un maximum de 127 paramètres1 (si, si ), toutefois nous vous conseillons de vous limiter à cinq afin de conserver un code concis et lisible.
Maintenant que nous savons tout cela, nous pouvons réaliser une fonction qui calcule le plus petit commun diviseur entre deux nombres et ainsi simplifier l’exemple du dessus.
#include <stdio.h>
int ppcd(int a, int b)
{
int min = (a < b) ? a : b;
for (int i = 2; i <= min; ++i)
if (a % i == 0 && b % i == 0)
return i;
return 0;
}
int main(void)
{
int a;
int b;
printf("Entrez deux nombres : ");
scanf("%d %d", &a, &b);
int resultat = ppcd(a, b);
if (resultat != 0)
printf("Le plus petit diviseur de %d et %d est %d\n", a, b, resultat);
printf("Entrez deux autres nombres : ");
scanf("%d %d", &a, &b);
resultat = ppcd(a, b);
if (resultat != 0)
printf("Le plus petit diviseur de %d et %d est %d\n", a, b, resultat);
return 0;
}
Plus simple et plus lisible, non ?
Remarquez la présence de deux instructions return
dans la fonction ppcd()
. La valeur zéro est retournée afin d’indiquer l’absence d’un diviseur commun.
Les arguments et les paramètres
À ce stade, il est important de préciser qu’un paramètre est propre à une fonction, il n’est pas utilisable en dehors de celle-ci. Par exemple, la variable a
de la fonction ppcd()
n’a aucun rapport avec la variable a
de la fonction main()
.
Voici un autre exemple plus explicite à ce sujet.
#include <stdio.h>
void fonction(int nombre)
{
++nombre;
printf("Variable nombre dans `fonction' : %d\n", nombre);
}
int main(void)
{
int nombre = 5;
fonction(nombre);
printf("Variable nombre dans `main' : %d\n", nombre);
return 0;
}
Variable nombre dans `fonction' : 6
Variable nombre dans `main' : 5
Comme vous le voyez, les deux variables nombre
sont bel et bien distinctes. En fait, lors d’un appel de fonction, vous spécifiez des arguments à la fonction appelée. Ces arguments ne sont rien d’autre que des expressions dont les résultats seront ensuite affectés aux différents paramètres de la fonction.
Notez bien cette différence car elle est très importante : un argument est une expression alors qu’un paramètre est une variable.
Ainsi, la valeur de la variable nombre
de la fonction main()
est passée en argument à la fonction fonction()
et est ensuite affectée au paramètre nombre
. La variable nombre
de la fonction main()
n’est donc en rien modifiée.
Les étiquettes et l’instruction goto
Nous vous avons présenté les étiquettes et l’instruction goto
au sein du chapitre précédent. À leur sujet, notez qu’une étiquette n’est utilisable qu’au sein d’une fonction. Autrement dit, il n’est pas possible de sauter d’une fonction à une autre à l’aide de l’instruction goto
.
int deux(void)
{
deux:
return 2;
}
int main(void)
{
goto deux; /* Interdit */
return 0;
}
- ISO/IEC 9899:201x, doc. N1570, § 5.2.4.1, Translation limits, p. 26↩
Les prototypes
Jusqu’à présent, nous avons toujours défini notre fonction avant la fonction main()
. Cela paraît de prime abord logique (nous définissons la fonction avant de l’utiliser), cependant cela est surtout indispensable. En effet, si nous déplaçons la définition après la fonction main()
, le compilateur se retrouve dans une situation délicate : il est face à un appel de fonction dont il ne sait rien (nombre d’arguments, type des arguments et type de retour). Que faire ? Hé bien, il serait possible de stopper la compilation, mais ce n’est pas ce qui a été retenu, le compilateur va considérer que la fonction retourne une valeur de type int
et qu’elle reçoit un nombre indéterminé d’arguments.
Toutefois, si cette décision a l’avantage d’éviter un arrêt de la compilation, elle peut en revanche conduire à des problèmes lors de l’exécution si cette supposition du compilateur s’avère inadéquate. Or, il serait pratique de pouvoir définir les fonctions dans l’ordre que nous souhaitons sans se soucier de qui doit être défini avant qui.
Pour résoudre ce problème, il est possible de déclarer une fonction à l’aide d’un prototype. Celui-ci permet de spécifier le type de retour de la fonction, son nombre d’arguments et leur type, mais ne comporte pas le corps de cette fonction. La syntaxe d’un prototype est la suivante.
type nom(paramètres);
Ce qui donne par exemple ceci.
#include <stdio.h>
void bonjour(void);
int main(void)
{
bonjour();
return 0;
}
void bonjour(void)
{
printf("Bonjour !\n");
}
Notez bien le point-virgule à la fin du prototype qui est obligatoire.
Étant donné qu’un prototype ne comprend pas le corps de la fonction qu’il déclare, il n’est pas obligatoire de préciser le nom des paramètres de celle-ci. Ainsi, le prototype suivant est parfaitement correct.
int ppcd(int, int);
Variables globales et classes de stockage
Les variables globales
Il arrive parfois que l’utilisation de paramètres ne soit pas adaptée et que des fonctions soient amenées à travailler sur des données qui doivent leur être communes. Prenons un exemple simple : vous souhaitez compter le nombre d’appels de fonction réalisé durant l’exécution de votre programme. Ceci est impossible à réaliser, sauf à définir une variable dans la fonction main()
, la passer en argument de chaque fonction et de faire en sorte que chaque fonction retourne sa valeur augmentée de un, ce qui est très peu pratique.
À la place, il est possible de définir une variable dite « globale » qui sera utilisable par toutes les fonctions. Pour définir une variable globale, il vous suffit de définir une variable en dehors de tout bloc, autrement dit en dehors de toute fonction.
#include <stdio.h>
void fonction(void);
int appels = 0;
void fonction(void)
{
++appels;
}
int main(void)
{
fonction();
fonction();
printf("Ce programme a réalisé %d appel(s) de fonction\n", appels);
return 0;
}
Ce programme a réalisé 2 appel(s) de fonction
Comme vous le voyez, nous avons simplement placé la définition de la variable appels en dehors de toute fonction et avant toute définition de fonction de sorte qu’elle soit partagée entre elles.
Le terme « global » est en fait un peu trompeur étant donné que la variable n’est pas globale au programme, mais tout simplement disponible pour toutes les fonctions du fichier dans lequel elle est située. Ce terme est utilisé en opposition aux paramètres et variables des fonctions qui sont dites « locales ».
N’utilisez les variables globales que lorsque cela vous paraît vraiment nécessaire. Ces dernières étant utilisables dans un fichier entier (voire dans plusieurs, nous le verrons un peu plus tard), elles ont tendances à rendre la lecture du code plus difficile.
Les classes de stockage
Les variables locales et les variables globales ont une autre différence de taille : leur classe de stockage. La classe de stockage détermine (entre autre) la durée de vie d’un objet, c’est-à-dire le temps durant lequel celui-ci existera en mémoire.
Classe de stockage automatique
Les variables locales sont par défaut de classe de stockage automatique. Cela signifie qu’elles sont allouées automatiquement à chaque fois que le bloc auquel elles appartiennent est exécuté et qu’elles sont détruites une fois son exécution terminée.
int ppcd(int a, int b)
{
int min = (a < b) ? a : b;
for (int i = 2; i <= min; ++i)
if (a % i == 0 && b % i == 0)
return i;
return 0;
}
Par exemple, à chaque fois que la fonction ppcd()
est appelée, les variables a
, b
, min
sont allouées en mémoire vive et détruites à la fin de l’exécution de la fonction. La variable i
quant à elle est allouée au début de la boucle for
et détruite à la fin de cette dernière.
Classe de stockage statique
Les variables globales sont toujours de classe de stockage statique. Ceci signifie qu’elles sont allouées au début de l’exécution du programme et sont détruites à la fin de l’exécution de celui-ci. En conséquence, elles conservent leur valeur tout au long de l’exécution du programme.
Également, à l’inverse des autres variables, celles-ci sont initialisées à zéro si elles ne font pas l’objet d’une initialisation1. L’exemple ci-dessous est donc correct et utilise deux variables valant zéro.
#include <stdio.h>
int a;
double b;
int main(void)
{
printf("%d, %f\n", a, b);
return 0;
}
0, 0.000000
Petit bémol tout de même : étant donné que ces variables sont créées au début du programme, elles ne peuvent être initialisées qu’à l’aide de constantes. La présence de variables au sein de l’expression d’initialisation est donc proscrite.
#include <stdio.h>
int a = 20; /* Correct */
double b = a; /* Incorrect */
int main(void)
{
printf("%d, %f\n", a, b);
return 0;
}
Modification de la classe de stockage
Il est possible de modifier la classe de stockage d’une variable automatique en précédant sa définition du mot-clé static
afin d’en faire une variable statique.
#include <stdio.h>
int compteur(void)
{
static int n;
return ++n;
}
int main(void)
{
compteur();
printf("n = %d\n", compteur());
return 0;
}
n = 2
- Dans les cas des pointeurs, que nous verrons plus tard dans ce cours, ils sont initialisés comme pointeurs nuls.↩
Exercices
Afficher un rectangle
Le premier exercice que nous vous proposons consiste à afficher un rectangle dans la console. Voici ce que devra donner l’exécution de votre programme.
Donnez la longueur : 5
Donnez la largeur : 3
***
***
***
***
***
Correction
#include <stdio.h>
void rectangle(int, int);
int main(void)
{
int longueur;
int largeur;
printf("Donnez la longueur : ");
scanf("%d", &longueur);
printf("Donnez la largeur : ");
scanf("%d", &largeur);
printf("\n");
rectangle(longueur, largeur);
return 0;
}
void rectangle(int longueur, int largeur)
{
for (int i = 0; i < longueur; i++)
{
for (int j = 0; j < largeur; j++)
printf("*");
printf("\n");
}
}
Vous pouvez aussi essayer d’afficher le rectangle dans l’autre sens.
Afficher un triangle
Même principe, mais cette fois-ci avec un triangle (rectangle). Le programme devra donner ceci.
Donnez un nombre : 5
*
**
***
****
*****
Bien entendu, la taille du triangle variera en fonction du nombre entré.
Correction
#include <stdio.h>
void triangle(int);
int main(void)
{
int nombre;
printf("Donnez un nombre : ");
scanf("%d", &nombre);
printf("\n");
triangle(nombre);
return 0;
}
void triangle(int nombre)
{
for (int i = 0; i < nombre; i++)
{
for (int j = 0; j <= i; j++)
printf("*");
printf("\n");
}
}
En petites coupures ?
Pour ce dernier exercice, vous allez devoir réaliser un programme qui reçoit en entrée une somme d’argent et donne en sortie la plus petite quantité de coupures nécessaires pour reconstituer cette somme.
Pour cet exercice, vous utiliserez les coupures suivantes :
- des billets de 100€ ;
- des billets de 50€ ;
- des billets de 20€ ;
- des billets de 10€ ;
- des billets de 5€ ;
- des pièces de 2€ ;
- des pièces de 1€ ;
Ci dessous un exemple de ce que devra donner votre programme une fois terminé.
Entrez une somme : 285
2 billet(s) de 100.
1 billet(s) de 50.
1 billet(s) de 20.
1 billet(s) de 10.
1 billet(s) de 5.
Correction
#include <stdio.h>
int coupure_inferieure(int valeur)
{
switch (valeur)
{
case 100:
return 50;
case 50:
return 20;
case 20:
return 10;
case 10:
return 5;
case 5:
return 2;
case 2:
return 1;
default:
return 0;
}
}
void coupure(int somme)
{
int valeur = 100;
while (valeur != 0)
{
int nb_coupure = somme / valeur;
if (nb_coupure > 0)
{
if (valeur >= 5)
printf("%d billet(s) de %d.\n", nb_coupure, valeur);
else
printf("%d pièce(s) de %d.\n", nb_coupure, valeur);
somme -= nb_coupure * valeur;
}
valeur = coupure_inferieure(valeur);
}
}
int main(void)
{
int somme;
printf("Entrez une somme : ");
scanf("%d", &somme);
coupure(somme);
return 0;
}
Le prochain chapitre sera l’occasion de mettre en pratique ce que nous venons de voir à l’aide d’un second TP.
En résumé
- Une fonction permet de rassembler une suite d’instructions qui est amenée à être utilisée plusieurs fois ;
- Une fonction peut retourner ou non une valeur et recevoir ou non des paramètres ;
- Un paramètre est une variable propre à une fonction, il n’est pas utilisable en dehors de celle-ci ;
- Un argument est une expression passée lors d’un l’appel de fonction et dont la valeur est affectée au paramètre correspondant ;
- Un prototype permet de déclarer une fonction sans spécifier son corps, il n’est ainsi plus nécessaire de se soucier de l’ordre de définition des fonctions ;
- Il est possible de définir une variable « globale » en plaçant sa définition en dehors de tout bloc (et donc de toute fonction) ;
- Une variable définie au sein d’un bloc est de classe de stockage automatique, alors qu’une variable définie en dehors de tout bloc est de classe de stockage statique ;
- Une variable de classe de stockage automatique est détruite une fois que l’exécution du bloc auquel elle appartient est terminée ;
- Une variable de classe de stockage statique existe durant toute l’exécution du programme ;
- Il est possible de modifier la classe de stockage d’une variable de classe de stockage automatique à l’aide du mot-clé
static
;