précédent  index  suivant

9. Fonctions et prototypes


9.1 Pour commencer ...

Il y a trois notions :

La déclaration d'une fonction, c'est annoncer que tel identificateur correspond à une fonction, qui renvoie tel type. La définition d'une fonction est une déclaration où, en plus, on donne le code de la fonction elle-même. Le prototype est une déclaration de fonction où le type des arguments est également donné.

Par exemple :

	int f();    /* declaration de f(), renvoyant un int, pas de prototype  */

	int f(void);/* declaration de f(), renvoyant un int, prototype (0 arg) */

	int f(void) /* definition de f() avec declaration avec prototype       */
	{
		return 42;
	}

	int f(x)    /* definition de f() avec declaration sans prototype       */
	int x;
	{
		return x;
	}

	int f()     /* definition de f() avec declaration sans prototype       */
	{
		return 42;
	}
		

Ce qui n'est pas possible :

Ce qui est autorisé :

Ce qui était autorisé en C90 mais ne l'est plus en C99 :

Ce qui est encore autorisé en C99 mais disparaîtra bientôt :

Ce qu'il faut faire quand on veut programmer lisiblement, en détectant les bugs et en gardant du code maintenable et compatible avec le futur :

9.2 Qu'est-ce qu'un prototype ?

Un prototype est une signature de fonction. Comme tout objet en C, une fonction doit être déclarée avant son utilisation. Cette déclaration est le prototype de la fonction. Le prototype doit indiquer au compilateur le nom de la fonction, le type de la valeur de retour et le type des paramètres (sauf pour les fonctions à arguments variables, comme printf(). (cf. 9.6).

	int fa(int a, char const * const b);
	int fb(int, char const * const);
		

Les noms de paramètre sont optionnels, mais il est fortement conseillé de les laisser. Cela donne une bonne indication sur leurs rôles.

Les fonctions de la bibliothèque ont également leur prototype. Avant l'utilisation de celles-ci, il faut inclure les fichiers d'en-tête contenant les prototypes. Par exemple, le prototype de malloc() se trouve dans stdlib.h.

Certains préfèrent ajouter le mot clé extern au prototype, afin de rester cohérent avec la déclaration des variables globales.

Voir aussi les questions 9.3, 12.1, 13.5 et 14.17.

9.3 Où déclarer les prototypes ?

Un prototype de fonction doit être déclaré avant l'utilisation de la fonction. Pour une plus grande lisibilité, mais aussi pour simplifier la maintenance du code, il est conseillé de regrouper tous les prototypes d'un module (fichier xxx.c) dans un en-tête (<xxx.h>). Ce dernier n'a plus alors qu'à être inclus dans le code qui utilise ces fonctions. C'est le cas des fonctions de la bibliothèque standard.

Voir aussi les questions 13.5 et 9.10.

9.4 Quels sont les prototypes valides de main() ?

La fonction main() renvoie toujours un int. Les prototypes valides sont :

	int main(void);
	int main(int argc, char * argv[]);
		

Tout autre prototype n'est pas du tout portable et ne doit jamais être utilisé (même s'il est accepté par votre compilateur).

En particulier, vous ne devez pas terminer la fontion main() sans retourner une valeur positive (non nulle en cas d'erreur). Les valeurs de retour peuvent être 0, EXIT_SUCCESS ou EXIT_FAILURE.

On pourra aussi rencontrer (sous Unix) le prototype suivant :

	int main(int argc, char* argv[], char** arge);
		

dans le but d'utiliser les variables d'environnement du shell actif. Ce n'est ni portable ni standard, d'autant plus que les fonctions getenv(), setenv() et putenv() le sont et suffisent largement.

Enfin, rappelons que le prototype suivant

	int main();
		

est parfaitement valide en C++ (et est synonyme du premier présenté ici), mais ne l'est pas en C.

9.5 Comment printf() peut recevoir différents types d'arguments ?

printf() est une fonction à nombre variable de paramètres. Son prototype est le suivant :

	int printf(const char * format, ...); /* C 90 */
	int printf(const char * restrict format, ...); /* C 99 */
		

Le type et le nombre des paramètres n'est pas défini dans le prototype, c'est le traitement effectué dans la fonction qui doit les vérifier.

Pour utiliser cette fonction, il est donc impératif d'inclure l'en-tête <stdio.h>.

Pour écrire une fonction de ce type, lire la question suivante (9.6).

9.6 Comment écrire une fonction à un nombre variable de paramètres ?

La bibliothèque standard fournit des outils pour faciliter la gestion de ce type de fonctions. On les trouve dans l'en-tête <stdarg.h>.

Le prototype d'une fonction à nombre variable de paramètres doit contenir au moins un paramètre explicite, puis se termine par ... Exemple :

	int f(int nombre, ...);
		

Il faut, d'une façon ou d'une autre, passer dans les paramètres le nombre d'arguments réellement transmis. On peut le faire en donnant ce nombre explicitement (comme printf()), ou passer la valeur NULL en dernier.

Attention toutefois avec la valeur NULL dans ce cas. En effet, NULL n'est pas nécessairement une valeur du type pointeur mais une valeur qui donne un pointeur nul si elle est affectée ou passée ou comparée à un type pointeur. Le passage d'une valeur à un paramètre n'est pas une affectation à un pointeur mais une affectation qui obéit aux lois spéciales pour les paramètres à nombre variable (ou pour les paramètres d'une fonction sans prototype). Les lois de promotion pour les types arithmétiques sont appliquées). Si NULL est défini par

	#define NULL 0
		

alors (int)0 est passé à la fonction. Si un pointeur n'a pas la même taille qu'un int ou si un pointeur nul n'est pas représenté par « tous les bits 0 » le passage d'un 0 ne passe donc pas de pointeur nul. La méthode portable est donc

	f(toto, titi, (void *)NULL);
		

ou

	f(toto, titi, (void *)0);
		

C'est le seul cas où il faut caster NULL parce qu'il ne s'agit pas d'un contexte syntactique « de pointeur », seulement d'un contexte « de pointeur par contrat ».

Après cela, les fonctions va_start(), va_arg() et va_end() permettent de parcourir la liste des paramètres.

Voici un petit exemple :

	#include <stdarg.h>

	int vexemple(int nombre, ...)
	{
		va_list argp;
		int i;
		int total = O;

		if (nombre < 1)
			return 0;

		va_start(argp, nombre);
		for (i = 0; i < nombre; i++) {
			total += va_arg(argp, int);
		}
		va_end(argp);

		return total;
	}
		

Merci à Horst Kraemer pour ces remarques.

Pour les fonctions utilisant des paramètres variables du type printf(), GCC fournit une extension lors du prototypage pour que le compilateur fournisse des avertissements si les paramètres variables ne sont pas conformes au standard printf.

Voici un petit exemple :

	#include <stdarg.h>

	extern void my_trace(int, const char *, ...) __attribute__ ((format (printf, 2, 3)));

	void my_trace(int type, const char *fmt, ...)
	{
		va_list argp;

		if (type) {
			va_start(argp, fmt);
			vfprintf(stderr, fmt, argp);
			va_end(argp);
		}
	}
	

L'attribut __attribute__ indique que la fonction utilise des arguments du type printf, dont le format (chaîne avec %) se trouve en position 2, et les arguments du printf se trouvent à partir de la position 3.

D'autres types que printf sont connus, comme scanf, strftime et strfmon. Voir la documentation de GCC sur http://gcc.gnu.org/onlinedocs/gcc/Function-Attributes.html.

9.7 Comment modifier la valeur des paramètres d'une fonction ?

En C, les paramètres sont passés par valeur. Dans la plupart des implémentations, cela se fait par une copie dans la pile. Lors du retour de la fonction, ces valeurs sont simplement dépilées, et les modifications éventuelles sont perdues. Pour pallier cela, il faut simuler un passage des paramètres par référence, en passant un pointeur sur les variables à modifier. Voici l'exemple classique de l'échange des valeurs entre deux entiers :

	void echange(int * a, int * b)
	{
		int tmp = *a;
		*a = *b;
		*b = tmp;
	}
		

9.8 Comment retourner plusieurs valeurs ?

Le langage C ne permet pas aux fonctions de renvoyer plusieurs objets. Une solution consiste à passer l'adresse des objets à modifier en paramètre. Une autre solution consiste à renvoyer une structure, ou un pointeur sur une structure qui contient l'ensemble des valeurs. Généralement, quand on a ce genre de choses à faire, c'est qu'il se cache une structure de données que l'on n'a pas identifiée. La pire des solutions est d'utiliser des variables globales.

9.9 Peut-on, en C, imbriquer des fonctions ?

Non, on ne peut pas. Les concepteurs ont jugé cela trop compliqué à mettre en oeuvre (portée des variables, gestion de la pile etc.). Certaines implémentations, comme GNU CC le supportent toutefois. Ceci dit, on peut très bien s'en passer, en utilisant des pointeurs sur les structures de données à partager, ou en utilisant des pointeurs de fonctions.

9.10 Qu'est-ce qu'un en-tête ?

Un en-tête est un ensemble de déclarations, définitions et prototypes nécessaires pour compiler et pour utiliser un module. Par exemple, pour utiliser les fonctions d'entrées/sorties de la bibliothèque standard, il est nécessaire d'inclure dans son programme l'en-tête <stdio.h>.

Par abus, on parle souvent de fichier d'en-tête, car historiquement, et encore aujourd'hui pour de nombreuses implémentations, ces en-têtes sont des fichiers. C'est également le cas pour les en-têtes personnels. Toutefois, la norme n'exige pas que les en-têtes standards soient des fichiers à proprement parlé.


précédent  index  suivant