Cours sur les bases de C/C++ par la pratique
...
1.6. Les fonctions
Le C++ ne permet de faire que des fonctions, pas de procédures. Une procédure peut être faite en utilisant une fonction ne renvoyant pas de valeur ou en ignorant la valeur retournée.
1.6.1. Définition des fonctions
La définition des fonctions se fait comme suit :
type identificateur(paramètres)
{
... /* Instructions de la fonction. */
}
type est le type de la valeur renvoyée, identificateur est le nom de la fonction, et paramètres
est une liste de paramètres. La syntaxe de la liste de paramètres est la suivante :
type variable [= valeur] [, type variable [= valeur] [...]]
où type est le type du paramètre variable qui le suit et valeur sa valeur par défaut. La valeur par défaut d'un paramètre est la valeur que ce paramètre prend lors de l'appel de la fonction si au- cune autre valeur n'est fournie.
Note : L'initialisation des paramètres de fonctions n'est possible qu'en C++, le C n'accepte pas cette syntaxe.
La valeur de la fonction à renvoyer est spécifiée en utilisant la commande return, dont la syntaxe est :
return valeur;
Exemple 1-13. Définition de fonction
int somme(int i, int j)
{
return i+j;
}
Si une fonction ne renvoie pas de valeur, on lui donnera le type void. Si elle n'attend pas de para- mètres, sa liste de paramètres sera void ou n'existera pas. Il n'est pas nécessaire de mettre une ins- truction return à la fin d'une fonction qui ne renvoie pas de valeur.
Exemple 1-14. Définition de procédure
void rien() /* Fonction n'attendant pas de paramètres */
{ /* et ne renvoyant pas de valeur. */ return; /* Cette ligne est facultative. */
}
1.6.2. Appel des fonctions
L'appel d'une fonction se fait en donnant son nom, puis les valeurs de ses paramètres entre paren- thèses. Attention ! S'il n'y a pas de paramètres, il faut quand même mettre les parenthèses, sinon la fonction n'est pas appelée.
Exemple 1-15. Appel de fonction
int i=somme(2,3); rien();
Si la déclaration comprend des valeurs par défaut pour des paramètres (C++ seulement), ces va- leurs sont utilisées lorsque ces paramètres ne sont pas fournis lors de l'appel. Si un paramètre est manquant, alors tous les paramètres qui le suivent doivent être eux aussi manquants. Il en résulte que seuls les derniers paramètres d'une fonction peuvent avoir des valeurs par défaut. Par exemple :
int test(int i = 0, int j = 2)
{
return i/j;
}
L'appel de la fonction test(8) est valide. Comme on ne précise pas le dernier paramètre, j est initialisé à 2. Le résultat obtenu est donc 4. De même, l'appel test() est valide : dans ce cas i vaut 0 et j vaut 2. En revanche, il est impossible d'appeler la fonction test en ne précisant que la valeur de j. Enfin, l'expression “ int test(int i=0, int j) {...} ” serait invalide, car si on ne passait pas deux paramètres, j ne serait pas initialisé.
1.6.3. Déclaration des fonctions
Toute fonction doit être déclarée avant d'être appelée pour la première fois. La définition d'une fonction peut faire office de déclaration.
Il peut se trouver des situations où une fonction doit être appelée dans une autre fonction définie avant elle. Comme cette fonction n'est pas définie au moment de l'appel, elle doit être déclarée. De même, il est courant d'avoir à appeler une fonction définie dans un autre fichier que le fichier d'où se fait l'appel. Encore une fois, il est nécessaire de déclarer ces fonctions.
Le rôle des déclarations est donc de signaler l'existence des fonctions aux compilateurs afin de les utiliser, tout en reportant leur définition de ces fonctions plus loin ou dans un autre fichier.
La syntaxe de la déclaration d'une fonction est la suivante :
type identificateur(paramètres);
où type est le type de la valeur renvoyée par la fonction, identificateur est son nom et para- mètres la liste des types des paramètres que la fonction admet, séparés par des virgules.
Exemple 1-16. Déclaration de fonction
int Min(int, int); /* Déclaration de la fonction minimum */
/* définie plus loin. */
/* Fonction principale. */ int main(void)
{
int i = Min(2,3); /* Appel à la fonction Min, déjà
déclarée. */
return 0;
}
/* Définition de la fonction min. */ int Min(int i, int j)
{
if (i<j) return i;
else return j;
}
En C++, il est possible de donner des valeurs par défaut aux paramètres dans une déclaration, et ces valeurs peuvent être différentes de celles que l'on peut trouver dans une autre déclaration. Dans ce cas, les valeurs par défaut utilisées sont celles de la déclaration visible lors de l'appel de la fonction.
1.6.4. Surcharge des fonctions
Il est interdit en C de définir plusieurs fonctions qui portent le même nom. En C++, cette interdic- tion est levée, moyennant quelques précautions. Le compilateur peut différencier deux fonctions en regardant le type des paramètres qu'elle reçoit. La liste de ces types s'appelle la signature de la fonc- tion. En revanche, le type du résultat de la fonction ne permet pas de l'identifier, car le résultat peut ne pas être utilisé ou peut être converti en une valeur d'un autre type avant d'être utilisé après l'appel de cette fonction.
Il est donc possible de faire des fonctions de même nom (on dit que ce sont des fonctions surchar- gées) si et seulement si toutes les fonctions portant ce nom peuvent être distinguées par leurs signa- tures. La fonction qui sera appelée sera choisie parmi les fonctions de même nom, et ce sera celle dont la signature est la plus proche des valeurs passées en paramètre lors de l'appel.
Exemple 1-17. Surcharge de fonctions
float test(int i, int j)
{
return (float) i+j;
}
float test(float i, float j)
{
return i*j;
}
On veillera à ne pas utiliser des fonctions surchargées dont les paramètres ont des valeurs par dé- faut, car le compilateur ne pourrait pas faire la distinction entre ces fonctions. D'une manière géné- rale, le compilateur dispose d'un ensemble de règles (dont la présentation dépasse le cadre de ce cours) qui lui permettent de déterminer la meilleure fonction à appeler étant donné un jeu de para- mètres. Si, lors de la recherche de la fonction à utiliser, le compilateur trouve des ambiguïtés, il gé- nérera une erreur.
1.6.5. Fonctions inline
Le C++ dispose du mot-clé inline, qui permet de modifier la méthode d'implémentation des fonctions. Placé devant la déclaration d'une fonction, il propose au compilateur de ne pas instancier cette fonction. Cela signifie que l'on désirerait que le compilateur remplace l'appel d'une fonction par le code correspondant. Si la fonction est grosse ou si elle est appelée souvent, le programme de- vient plus gros, puisque la fonction est réécrite à chaque fois qu'elle est appelée. En revanche, il de- vient nettement plus rapide, puisque les mécanismes d'appel de fonctions, de passage des paramètres et de la valeur de retour sont ainsi évités. De plus, le compilateur peut effectuer des optimisations additionnelles qu'il n'aurait pas pu faire si la fonction n'était pas inlinée. En pratique, on réservera cette technique pour les petites fonctions appelées dans du code devant être rapide (à l'intérieur des boucles par exemple), ou pour les fonctions permettant de lire des valeurs dans des variables.
De plus, il faut connaître les restrictions des fonctions inline :
Si l'une de ces deux conditions n'est pas vérifiée pour une fonction, le compilateur l'implémentera classiquement (elle ne sera donc pas inline).
Enfin, du fait que les fonctions inline sont insérées telles quelles aux endroits où elles sont appe- lées, il est nécessaire qu'elles soient complètement définies avant leur appel. Cela signifie que, con- trairement aux fonctions classiques, il n'est pas possible de se contenter de les déclarer pour les ap- peler, et de fournir leur définition dans un fichier séparé. Dans ce cas en effet, le compilateur géné- rerait des références externes sur ces fonctions, et n'insérerait pas leur code. Ces références ne se- raient pas résolues à l'édition de lien, car il ne génère également pas les fonctions inline, puis- qu'elles sont supposées être insérées sur place lorsqu'on les utilise. Les notions de compilation dans des fichiers séparés et d'édition de liens seront présentées en détail dans le Chapitre 6.
Exemple 1-18. Fonction inline
inline int Max(int i, int j)
{
if (i>j)
return i; else
return j;
}
Pour ce type de fonction, il est tout à fait justifié d'utiliser le mot-clé inline.
1.6.6. Fonctions statiques
Exemple 1-19. Fonction statique
// Déclaration de fonction statique : static int locale1(void);
/* Définition de fonction statique : */ static int locale2(int i, float j)
{
return i*i+j;
}
Les techniques permettant de découper un programme en plusieurs fichiers source et de générer les fichiers binaires à partir de ces fichiers seront décrites dans le chapitre traitant de la modularité des programmes.
1.6.7. Fonctions prenant un nombre variable de paramètres
En général, les fonctions ont un nombre constant de paramètres. Pour les fonctions qui ont des pa- ramètres par défaut en C++, le nombre de paramètres peut apparaître variable à l'appel de la fonc- tion, mais en réalité, la fonction utilise toujours le même nombre de paramètres.
Cependant, le C et le C++ disposent d'un mécanisme qui permet au programmeur de réaliser des fonctions dont le nombre et le type des paramètres sont variables. Nous verrons plus loin que les fonctions d'entrée / sortie du C sont des fonctions dont la liste des arguments n'est pas fixée, cela afin de pouvoir réaliser un nombre arbitraire d'entrées / sorties, et ce sur n'importe quel type prédé- fini.
En général, les fonctions dont la liste des paramètres est arbitrairement longue disposent d'un cri- tère pour savoir quel est le dernier paramètre. Ce critère peut être le nombre de paramètres, qui peut être fourni en premier paramètre à la fonction, ou une valeur de paramètre particulière qui détermine la fin de la liste par exemple. On peut aussi définir les paramètres qui suivent le premier paramètre à l'aide d'une chaîne de caractères.
Pour indiquer au compilateur qu'une fonction peut accepter une liste de paramètres variable, il faut simplement utiliser des points de suspensions dans la liste des paramètres :
type identificateur(paramètres, ...)
La difficulté apparaît en fait dans la manière de récupérer les paramètres de la liste de paramètres dans la définition de la fonction. Les mécanismes de passage des paramètres étant très dépendants de la machine (et du compilateur), un jeu de macros a été défini dans le fichier d'en-tête stdarg.h pour faciliter l'accès aux paramètres de la liste. Pour en savoir plus sur les macros et les fichiers d'en-tête, consulter le Chapitre 5. Pour l'instant, sachez seulement qu'il faut ajouter la ligne sui- vante :
#include <stdarg.h>
au début de votre programme. Cela permet d'utiliser le type va_list et les expressions va_start, va_arg et va_end pour récupérer les arguments de la liste de paramètres variable, un à un.
Le principe est simple. Dans la fonction, vous devez déclarer une variable de type va_list. Puis, vous devez initialiser cette variable avec la syntaxe suivante :
va_start(variable, paramètre);
où variable est le nom de la variable de type va_list que vous venez de créer, et paramètre est le dernier paramètre classique de la fonction. Dès que variable est initialisée, vous pouvez récu- pérer un à un les paramètres à l'aide de l'expressions suivantes :
va_arg(variable, type)
qui renvoie le paramètre en cours avec le type type et met à jour variable pour passer au para- mètre suivant. Vous pouvez utiliser cette expression autant de fois que vous le désirez, elle retourne à chaque fois un nouveau paramètre. Lorsque le nombre de paramètres correct a été récupéré, vous devez détruire la variable variable à l'aide de la syntaxe suivante :
va_end(variable);
Il est possible de recommencer les étapes suivantes autant de fois que l'on veut, la seule chose qui compte est de bien faire l'initialisation avec va_start et de bien terminer la procédure avec va_end à chaque fois.
Exemple 1-20. Fonction à nombre de paramètres variable
#include <stdarg.h>
/* Fonction effectuant la somme de "compte" paramètres : */ double somme(int compte, ...)
{
double resultat=0; /* Variable stockant la somme. */ va_list varg; /* Variable identifiant le prochain
paramètre. */
va_start(varg, compte); /* Initialisation de la liste. */ do /* Parcours de la liste. */
{
resultat=resultat+va_arg(varg, double); compte=compte-1;
} while (compte!=0);
va_end(varg); /* Terminaison. */ return resultat;
}
La fonction somme effectue la somme de compte flottants (float ou double) et la renvoie dans un double. Pour plus de détails sur la structure de contrôle do ... while, voir Section 2.4.
1.7. La fonction main
Lorsqu'un programme est chargé, son exécution commence par l'appel d'une fonction spéciale du programme. Cette fonction doit impérativement s'appeler “ main ” (principal en anglais) pour que le compilateur puisse savoir que c'est cette fonction qui marque le début du programme. La fonction main est appelée par le système d'exploitation, elle ne peut pas être appelée par le programme, c'est-à-dire qu'elle ne peut pas être récursive.
Exemple 1-21. Programme minimal
int main() /* Plus petit programme C/C++. */
{
}
La fonction main doit renvoyer un code d'erreur d'exécution du programme, le type de ce code est int. Elle peut aussi recevoir des paramètres du système d'exploitation. Ceci sera expliqué plus loin. Pour l'instant, on se contentera d'une fonction main ne prenant pas de paramètres.
Note : Il est spécifié dans la norme du C++ que la fonction main ne doit pas renvoyer le type void. En pratique cependant, beaucoup de compilateurs l'acceptent également.
La valeur 0 retournée par la fonction main indique que tout s'est déroulé correctement. En réalité, la valeur du code de retour peut être interprétée différemment selon le système d'exploitation utilisé. La librairie C définit donc les constantes EXIT_SUCCESS et EXIT_FAILURE, qui permettent de supprimer l'hypothèse sur la valeur à utiliser respectivement en cas de succès et en cas d'erreur.
1.8. Les fonctions d'entrée / sortie de base
Nous avons distingué au début de ce chapitre les programmes graphiques, qui traitent les événe- ments qu'ils reçoivent du système sous la forme de message, des autres programmes, qui reçoivent les données à traiter et écrivent leurs résultats sur les flux d'entrée / sortie standards. Les notions de flux d'entrée / sortie standards n'ont pas été définies plus en détail à ce moment, et il est temps à présent de pallier cette lacune.
1.8.1. Généralités sur les flux d'entrée / sortie en C
Sur quasiment tous les systèmes d'exploitation, les programmes disposent dès leur lancement de trois flux d'entrée / sortie standards. Généralement, le flux d'entrée standard est associé au flux de données provenant d'un terminal, et le flux de sortie standard à la console de ce terminal. Ainsi, les données que l'utilisateur saisit au clavier peuvent être lues par les programmes sur leur flux d'entrée standard, et ils peuvent afficher leurs résultats à l'écran en écrivant simplement sur leur flux de sortie standard. Le troisième flux standard est le flux d'erreur standard, qui, par défaut, est également asso- cié à l'écran, et sur lequel le programme peut écrire tous les messages d'erreurs qu'il désire.
Note : La plupart des systèmes permettent de rediriger les flux standards des programmes afin de les faire travailler sur des données provenant d'une autre source de données que le clavier, ou, par exemple, de leur faire enregistrer leurs résultats dans un fichier. Il est même courant de réaliser des “ pipelines ” de programmes, où les résultats de l'un sont envoyés dans le flux d'entrée standard de l'autre, et ainsi de suite. Ces suites de programmes sont également appelés des tubes en français.
La manière de réaliser les redirections des flux standards dépend des systèmes d'exploitation et de leurs interfaces utilisateurs. De plus, les programmes doivent être capables de travailler avec leurs flux d'entrée / sortie standards de manière générique, que ceux-ci soient redirigés ou non. Les techniques de redirection ne seront donc pas décrites plus en détail ici.
On pourrait penser que les programmes graphiques ne disposent pas de flux d'entrée / sortie standards. Pourtant, c'est généralement le cas. Les événements traités par les programmes graphiques dans leur boucle de messages ne proviennent généralement pas du flux d'entrée standard, mais d'une autre source de données spécifique à chaque système. En conséquence, les programmes graphiques peuvent toujours utiliser les flux d'entrée / sortie standard si cela leur est nécessaire.
Afin de permettre aux programmes d'écrire sur leurs flux d'entrée / sortie standards, la librairie C définit plusieurs fonctions extrêmement utiles. Les deux principales fonctions sont sans doute les fonctions printf et scanf. La fonction printf (“ print formatted ” en anglais) permet d'afficher des données à l'écran, et scanf (“ scan formatted ”) permet de les lire à partir du clavier.
En réalité, ces fonctions ne font rien d'autre que d'appeler deux autres fonctions permettant d'écrire et de lire des données sur un fichier : les fonctions fprintf et fscanf. Ces fonctions s'utilisent exactement de la même manière que les fonctions printf et scanf, à ceci près qu'elles prennent en premier paramètre une structure décrivant le fichier sur lequel elles travaillent. Pour les flux d'entrée
/ sortie standards, la librairie C définit les pseudo-fichiers stdin, stdout et stderr, qui corres- pondent respectivement aux flux d'entrée, au flux de sortie et au flux d'erreur standards. Ainsi, tout appel à scanf se traduit par un appel à fscanf sur le pseudo-fichier stdin, et tout appel à printf par un appel à fprintf sur le pseudo-fichier stdout.
Note : Il n'existe pas de fonction permettant d'écrire directement sur le flux d'erreur standard. Par conséquent, pour effectuer de telles écritures, il faut impérativement passer par la fonction fprintf, en lui fournissant en paramètre le pseudo-fichier stderr.
Les fonctions printf et scanf sont toutes deux des fonctions à nombre de paramètres variables. Elles peuvent donc être utilisées pour effectuer des écritures et des lectures multiples en un seul ap- pel. Afin de leur permettre de déterminer la nature des données passées dans les arguments va- riables, elles attendent toutes les deux en premier paramètre une chaîne de caractères descriptive des arguments suivants. Cette chaîne est appelée chaîne de format, et elle permet de spécifier avec pré- cision le type, la position et les options de format (précision, etc.) des données à traiter. Les deux sections suivantes décrivent la manière d'utiliser ces chaînes de format pour chacune des deux fonc- tions printf et scanf.
1.8.2. La fonction printf
La fonction printf s'emploie comme suit :
printf(chaîne de format [, valeur [, valeur [...]]])
On peut passer autant de valeurs que l'on veut, pour peu qu'elles soient toutes référencées dans la chaîne de format. Elle renvoie le nombre de caractères affichés.
La chaîne de format peut contenir du texte, mais surtout elle doit contenir autant de formateurs que de variables à afficher. Si ce n'est pas le cas, le programme plantera. Les formateurs sont placés dans le texte là où les valeurs des variables doivent être affichées.
La syntaxe des formateurs est la suivante :
%[[indicateur]...][largeur][.précision][taille] type
Un formateur commence donc toujours par le caractère %. Pour afficher ce caractère sans faire un formateur, il faut le dédoubler (%%).