Programmation C/Fonctions et procédures

Définition

modifier

Le code suivant définit une fonction fonction renvoyant une valeur de type type_retour et prenant N arguments, par1 de type type1, par2 de type type2, etc.

type_retour fonction(type1 par1, type2 par2, /* ..., */ typeN parN) 
{
    /* Déclarations de variables ... */
    /* Instructions ... */
}

L'exécution d'une fonction se termine soit lorsque l'accolade fermante est atteinte, soit lorsque le mot clef return est rencontré. La valeur renvoyée par une fonction est donnée comme paramètre à return. Une procédure est une fonction renvoyant void, dans ce cas return est appelé sans paramètre.

Exemple :

int max(int a, int b) /* Retourne la valeur maximale */
{
    return a>b ? a : b; /* Retourne a si a plus grand que b, b sinon. */
}

Une fonction peut retourner directement le résultat d'une autre fonction retournant un type compatible :

int trancheImposition(int montant, int limite) /* Retourne un entier (int) */
{
    return max(montant, limite) - 5000; /* appelle la fonction int max(a, b) */
}

Ce qui n'est pas possible avec une fonction ne retournant rien :

void trancheA(int revenus) { /* ... */ }
void trancheB(int revenus) { /* ... */ }

void calculImpots(int revenu_a, int revenu_b)
{
    /* if (revenu_a<500) return trancheA(revenu_a); */ /* Erreur ici */
    if (revenu_a<500) { trancheA(revenu_a); return; }
    trancheB(revenu_b);
}

Les passages des arguments aux fonctions se font toujours par valeur. Si on veut modifier la valeur d'un argument passé en paramètre à une fonction, depuis cette fonction, il faut utiliser des pointeurs.

Exemple :

void correctionMontant(int* p_montant) /* Préfixe p_ par convention pour ne pas oublier qu'il s'agit d'un pointeur */
{
    if (*p_montant > 2000) *p_montant = 2000;
}

void calcul()
{
    int revenus = 2459;
    correctionMontant(&revenus);
    /* -> revenus == 2000 */
}

Déclaration par prototype

modifier

Le prototype d'une fonction correspond simplement à son en-tête (tout ce qui précède la première accolade ouvrante). C'est-à-dire son nom, son type de retour et les types des différents paramètres. Cela permet au compilateur de vérifier que la fonction est appelée avec le bon nombre de paramètres et surtout avec les bons types. La ligne suivante déclare la fonction fonction, mais sans la définir :

type_retour nom_fonction(type1, type2, /* ..., */ typeN);

À noter que les noms des paramètres peuvent être omis et que la déclaration doit se terminer par un point-virgule (;), sans quoi vous pourrez vous attendre à une cascade d'erreurs.

Absence des paramètres

modifier

Avant la normalisation par l'ANSI, il était possible de faire une déclaration partielle d'une fonction, en spécifiant son type de retour, mais pas ses paramètres:

int f();

Cette déclaration ne dit rien sur les éventuels paramètes de la fonction f, sur leur nombre ou leur type, au contraire de  :

int g(void);

qui précise que la fonction g ne prend aucun argument.

Cette déclaration partielle laissait au compilateur le soin de compléter la déclaration lors de l'appel de la fonction, ou de sa définition. On perd donc un grand intérêt des prototypes. Mais à cause de l'immense quantité de code existant qui se reposait sur ce comportement, l'ANSI (puis le WG14) n'ont pas interdit de tels programmes, mais ont déclaré dès le C90 que cette construction est obsolète.

Évaluation des arguments

modifier

La norme du langage ne spécifie pas l'ordre d'évaluation des arguments[1]. Il faut donc faire particulièrement attention aux effets de bords.

  Ce code contient une erreur volontaire !
#include <stdio.h>

int somme(int a, int b)
{
    return a + b;
}

int main(void)
{
    int i = 0;
    printf("%d\n",  somme(++i, i)  );
    return 0;
}

Voici un premier exemple. Lors de l'appel de la fonction somme, si l'expression ++i est évaluée avant l'expression i, alors le programme affichera 2. Si, au contraire, c'est l'expression i qui est évaluée avant l'expression ++i, alors le programme affichera 1.

  Ce code contient une erreur volontaire !
#include <stdio.h>
int fonction(int, int);
int g(void);
int h(void);

int test(void)
{
    return fonction(g(), h());
}

Dans cet autre exemple, les expressions g() et h() pouvant être évaluées dans n'importe quel ordre, on ne peut pas savoir laquelle des fonctions g et h sera appelée en premier. Si l'appel de ces fonctions provoque des effets de bord (affichage de messages, modification de variables globales...), alors le comportement du programme est imprévisible. Pour pallier à ce problème, il faut imposer l'ordre d'appel :

#include <stdio.h>
int fonction(int, int);
int g(void);
int h(void);

int test(void)
{
    int a,b;
    a = g();
    b = h();
    return fonction(a, b);
}

Nombre variable d'arguments

modifier

Une fonctionnalité assez utile est d'avoir une fonction avec un nombre variable d'arguments, comme la fameuse fonction printf(). Pour cela, il suffit de déclarer le prototype de la fonction de la manière suivante :

Déclaration

modifier
#include <stdarg.h>

void ma_fonction(type1 arg1, type2 arg2, ...)
{
}

Dans l'exemple ci-dessus, les points de suspension ne sont pas un abus d'écriture, mais bel et bien une notation C pour indiquer que la fonction accepte d'autres arguments. L'exemple est limité à deux arguments, mais il est bien sûr possible d'en spécifier autant qu'on veut. C'est dans l'unique but de ne pas rendre ambigüe la déclaration, qu'aucun abus d'écriture n'a été employé.

L'inclusion de l'en-tête <stdarg.h> n'est nécessaire que pour traiter les arguments à l'intérieur de la fonction. La première remarque que l'on peut faire est qu'une fonction à nombre variable d'arguments contient au moins un paramètre fixe. En effet la déclaration suivante est invalide :

  Ce code contient une erreur volontaire !
void ma_fonction(...);

Accès aux arguments

modifier

Pour accéder aux arguments situés après le dernier argument fixe, il faut utiliser certaines fonctions (ou plutôt macros) de l'en-tête <stdarg.h> :

void va_start (va_list ap, last);
type va_arg (va_list ap, type);
void va_end (va_list ap);

va_list est un type opaque dont on n'a pas à se soucier. On commence par l'initialiser avec va_start. Le paramètre last doit correspondre au nom du dernier argument fixe de la fonction, ou alors tout bon compilateur retournera au moins un avertissement.

Vient ensuite la collecte minutieuse des arguments. Il faut bien comprendre qu'à ce stade, le langage n'offre aucun moyen de savoir comment sont structurées les données (c'est à dire leur type). Il faut absolument définir une convention, laissée à l'imagination du programmeur, pour pouvoir extraire les données correctement.

Qui plus est, il faut être extrêmement vigilant lors de la récupération des paramètres, à cause de la promotion des types entiers ou réels. En effet, les entiers sont systématiquement promus en int, sauf si la taille du type est plus grande, auquel cas le type est inchangé. Pour les réels, le type float est promu en double, alors que le type long double est inchangé. C'est pourquoi ce genre d'instruction n'a aucun sens dans une fonction à nombre variable d'arguments :

  Ce code contient une erreur volontaire !
char caractere = va_arg(list, char);

Il faut obligatoirement récupérer un entier de type char, comme étant un entier de type int.

Exemple de convention

modifier

Un bon exemple de convention est la fonction printf() elle même. Elle utilise un spécificateur de format qui renseigne à la fois le nombre d'arguments qu'on s'attend à trouver mais aussi le type de chacun. D'un autre côté, analyser un spécificateur de format est relativement rébarbatif, et on n'a pas toujours besoin d'une artillerie aussi lourde.

Une autre façon de faire, relativement répandue, est de ne passer que des couples (type, objet), où type correspond à un code représentant un type (une énumération par exemple) et objet le contenu de l'objet lui-même (int, pointeur, double, etc.). On utilise alors un code spécial (généralement 0) pour indiquer la fin des arguments, ou alors un des paramètres pour indiquer combien il y en a. Un petit exemple complet :

#include <stdio.h>
#include <stdarg.h>

enum va_list{
        TYPE_FIN, TYPE_ENTIER, TYPE_REEL, TYPE_CHAINE
};

void affiche(FILE * out, ...)
{
        va_list list;
        int     type;

        va_start(list, out);

        while ((type = va_arg(list, int)))
        {
                switch (type)
                {
                case TYPE_ENTIER: fprintf(out, "%d", va_arg(list, int)); break;
                case TYPE_REEL:   fprintf(out, "%g", va_arg(list, double)); break;
                case TYPE_CHAINE: fprintf(out, "%s", va_arg(list, char *)); break;
                }
        }
        fprintf(out, "\n");
        va_end(list);
}

int main(int nb, char * argv[])
{
        affiche(stdout, TYPE_CHAINE, "Le code ascii de 'a' est ", TYPE_ENTIER, 'a', 0);
        affiche(stderr, TYPE_CHAINE, "2 * 3 / 5 = ", TYPE_REEL, 2. * 3 / 5, 0);

        return 0;
}

L'inconvénient de ce genre d'approche est de ne pas oublier le marqueur de fin. Dans les deux cas, il faut être vigilant avec les conversions implicites, notamment dans le second cas. À noter que la conversion (transtypage) explicite des types en une taille inférieure à celle par défaut (int pour les entiers ou double pour les réels) ne permet pas de contourner la promotion implicite. Même écrit de la sorte:

affiche(stderr, TYPE_CHAINE, "2 * 3 / 5 = ", TYPE_REEL, (float) (2. * 3 / 5), 0);

Le résultat transmis au cinquième paramètre sera quand même promu implicitement en type double.

Fonction inline

modifier

Il s'agit d'une extension ISO C99, qui à l'origine vient du C++. Ce mot clé doit se placer avant le type de retour de la fonction. Il ne s'agit que d'une indication, le compilateur peut ne pas honorer la demande, notamment si la fonction est récursive. Dans une certaine mesure, les fonctionnalités proposées par ce mot clé sont déjà prises en charge par les instructions du préprocesseur. Beaucoup préfèreront passer par une macro, essentiellement pour des raisons de compatibilité avec d'anciens compilateurs ne supportant pas ce mot clé, et quand bien même l'utilisation de macro est souvent très délicat.

Le mot clé inline permet de s'affranchir des nombreux défauts des macros, et de réellement les utiliser comme une fonction normale, c'est à dire surtout sans effets de bord. À noter qu'il est préférable de classifier les fonctions inline de manière statique. Dans le cas contraire, la fonction sera aussi déclarée comme étant accessible de l'extérieur, et donc définie comme une fonction normale.

En la déclarant static inline, un bon compilateur devrait supprimer toute trace de la fonction et seulement la mettre in extenso aux endroits où elle est utilisée. Ceci permettrait à la limite de déclarer la fonction dans un fichier en-tête, bien qu'il s'agisse d'une pratique assez rare et donc à éviter. Exemple de déclaration d'une fonction inline statique :

static inline int max(int a, int b)
{
	return (a > b) ? a : b;
}

La fonction main

modifier

Nous allons revenir ici sur la fonction main, présente dans chaque programme. Cette fonction est le point d'entrée du programme. La norme définit deux prototypes, qui sont donc portables:

int main(int argc, char * argv[]) { /* ... */ }
int main(void) { /* ... */ }

Le premier prototype est plus "général" : il permet de récupérer des paramètres au programme. Le deuxième existe pour des raisons de simplicité, quand on ne veut pas traiter ces arguments.

Si ces deux prototypes sont portables, une implémentation peut néanmoins définir un autre prototype pour main, ou spécifier une autre fonction pour le démarrage du programme. Cependant ces cas sont plus rares (et souvent spécifiques à du C embarqué).

Paramètres de ligne de commande

modifier

La fonction main prend deux paramètres qui permettent d'accéder aux paramètres passés au programme lors de son appel. Le premier, généralement appelé argc (argument count), est le nombre de paramètres qui ont été passés au programme. Le second, argv (argument vector), est la liste de ces paramètres. Les paramètres sont stockés sous forme de chaîne de caractères, argv est donc un tableau de chaînes de caractères, ou un pointeur sur un pointeur sur char. argc correspond au nombre d'éléments de ce tableau.

La première chaîne de caractères, dont l'adresse est dans argv[0], contient le nom du programme. Le premier paramètre est donc argv[1]. Le dernier élément du tableau, argv[argc], est un pointeur nul.

Valeur de retour

modifier

La fonction main retourne toujours une valeur de type entier. L'usage veut qu'on retourne 0 (ou EXIT_SUCCESS) si le programme s'est déroulé correctement, ou EXIT_FAILURE pour indiquer qu'il y a eu une erreur (Les macros EXIT_SUCCESS et EXIT_FAILURE étant définies dans l'en-tête <stdlib.h>). Il est possible par le programme appelant de récupérer ce code de retour, et de l'interpréter comme bon lui semble.

Particularités

modifier

Cette fonction ignore les prototypes, il n'est pas non plus nécessaire d'en écrire pour elle. On dit que l'environnement fournit ses prototypes mais cela n'est pas vraiment exact car une fonction en C ne peut avoir qu'un seul prototype. Cet exemple compilera

char main(int arg1, char arg2, double arg3) {
    return 0;
}

Elle ignore également le type de retour, le programme compilera si votre fonction main ne contient pas d'instruction return ou une instruction return qui ne correspond pas à son prototype. Par défaut dans un environnement UNIX une fonction main qui ne contient pas d'instruction return retournera 0, signifiant que le programme s'est exécuté correctement. Les deux programmes suivant sont équivalents

int main() {
    /* pas d'instruction return */
}
int main() {
    return 0;
}


Il est possible d'appeler la fonction main récursivement en C mais cela est interdit en C++, pour une portabilité maximale, il faut éviter de le faire.

int main(int argc, char** argv) {
    main(argc, argv); // interdit en c++
}

Exemple

modifier

Voici un petit programme très simple qui affiche la liste des paramètres passés au programme lorsqu'il est appelé:

#include <stdio.h>

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

	for (i = 0; i < argc; i++)
		printf("paramètre %i : %s\n", i, argv[i]);

	return 0;
}

On effectue une boucle sur argv à l'aide de argc. Enregistrez-le sous le nom params.c puis compilez-le (cc params.c -o params). Vous pouvez ensuite l'appeler ainsi:

./params hello world ! # sous Unix
params.exe hello world ! # sous MS-DOS ou Windows

Vous devriez voir en sortie quelque chose comme ceci (paramètre 0 varie selon le système d'exploitation):

paramètre 0 : ./params
paramètre 1 : hello
paramètre 2 : world
paramètre 3 : !

Fonctions en C pré-ANSI

modifier

Absence de prototype lors de l'appel d'une fonction

modifier

Avant la normalisation C89, on pouvait appeler une fonction sans disposer ni de sa définition, ni de sa déclaration. Dans ce cas, la fonction était implicitement déclarée comme retournant le type int, et prenant un nombre indéterminé de paramètres.

/* Aucune déclaration de g() n'est visible. */

void f(void)
{
    g();  /* Déclaration implicite: extern int g() */
}

À cause de la grande quantité de code existant à l'époque qui reposait sur ce comportement, le C90 a conservé cette possibilité. Cependant, elle a été retirée de la norme C99, et est à éviter même lorsqu'on travaille en C90.

En effet, c'est plus qu'une bonne habitude de programmation de s'assurer que chaque fonction utilisée dans un programme ait son prototype déclaré avant qu'elle ne soit définie ou utilisée. C'est d'autant plus indispensable lorsque les fonctions sont définies et utilisées dans des fichiers différents.

Ancienne notation

modifier

À titre anecdotique, ceci est la façon « historique » de définir une fonction, avant que le prototypage ne fut utilisé. Cette notation est interdite depuis C90.

type_retour fonction(par1, par2, ..., parN)
type1 par1;
type2 par2;
...
typeN parN;
{
        /* Déclarations de variables ... */
        /* Instructions ... */
}

Au lieu de déclarer les types à l'intérieur même de la fonction, ils sont simplement décrits après la fonction et avant la première accolade ouvrante. À noter que type_retour pouvait être omis, et dans ce cas valait par défaut int.

Fichiers

modifier

Exécuter des fichiers

modifier

Les primitives sont execl, execlp, execle, exect, execv, execvp[2].

L'exemple ci-dessous lance un fichier test.exe sur un bureau de Windows 7 :

#include <unistd.h>
 
int main()
{
     int fichier;
     fichier = execl ( "c:\\Users\\public\\Desktop\\test.exe", "test.exe", ".", (char*)0);
}

Renommer et supprimer des fichiers

modifier

Utiliser la fonction rename (appel système) pour renommer un fichier.

Utiliser remove[3] (bibliothèque C, tous types de fichiers) ou unlink (appel système pour supprimer un fichier) et rmdir (appel système pour supprimer un répertoire).

Attention, remove, unlink et rmdir ne suppriment pas réellement le fichier ou répertoire, mais uniquement un nom (lien statique ou "dur" (hardlink)) dudit fichier ou répertoire. Si le nom en question est le dernier nom du fichier ou répertoire, alors le système de fichiers n'a plus de référence vers le fichier qui n'est dès lors plus accessible et l'espace qu'il occupait est disponible pour d'autres fichiers. Les données ne sont aucunement effacées, seul le lien permettant d'y accéder est détruit. Si le fichier avait plusieurs noms il reste accessible par ses autres noms. La commande "ln ancien_nom nouveau_nom" permet de donner un nom supplémentaire "nouveau_nom" au fichier "ancien_nom".

Copier des fichiers

modifier

Il faut copier le contenu du premier dans le second[4].


Références

modifier
  1. C'est le cas en C et en C++. Voir https://en.cppreference.com/w/c/language/eval_order (anglais)
  2. http://www.lipn.fr/~cerin/SE/SeETlangC.pdf
  3. https://openclassrooms.com/courses/apprenez-a-programmer-en-c/lire-et-ecrire-dans-des-fichiers
  4. http://c.developpez.com/faq/?page=fichiers#FICHIERS_copier