IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Notes sur le langage C

Notes sur le langage C


précédentsommairesuivant

X. Le type retourné par main()

Bien que main() soit censé retourner un int, on voit quelquefois écrit

 
Sélectionnez
void main (void)
{
}

Qu'en est-il exactement ?

D'après la définition du langage C, dans un programme conforme, main() doit retourner int. D'ailleurs un compilateur comme Borland C 3.1 en mode ANSI refuse void main() (error). Dans les mêmes conditions, gcc qui émet un avertissement (warning).

X-A. Historique

Dès l'apparition du langage C, une des formes canoniques de main() était :

 
Sélectionnez
main()
{
   return 0;
}

l'autre étant la forme qui permet de récupérer les arguments de la ligne de commande.

Il faut bien comprendre qu'à cette époque, une fonction définie sans type de retour explicite, retournait un int (c'est toujours le cas en C90, mais plus en C99 où le type doit être explicite). Le mot clé 'void' n'existait pas.

Il n'y avait donc aucune raison d'utiliser une forme void main().

Ensuite, est venue la normalisation du langage C. (1989 ANSI, 1990 ISO). Dans le texte, les deux formes canoniques sont décrites :

 
Sélectionnez
int main (void)

et :

 
Sélectionnez
int main (int argc, char **argv)

Il est précisé en remarque (dans la partie non normative) qu'il existe d'autres formes sans autres précisions. Elles ne font donc pas partie de la norme, leur comportement est donc indéfini dans le cadre d'un programme respectueux de la norme (dit 'strictement conforme').

XI. C'est quoi un prototype ?

Lorsqu'on défini une fonction :

 
Sélectionnez
int f (char *s)
{
  ...
}

on définit en même temps son interface, à savoir :

  • son nom (ici , f) ;
  • son type de retour (ici, int) ;
  • ses paramètres (ici, s, de type pointeur sur char).

Cet interface est aussi appelé prototype intégré. Il permet une utilisation de la fonction si l'appel est placé après la définition (il n'y a pas de raison de faire autrement, sauf cas exceptionnels et souvent douteux...) :

 
Sélectionnez
int f (char *s)
{
  ...
}
 
int main (void)
{
   int x = f("hello");
}

Maintenant, dans le cas de compilation séparée, on doit 'détacher' le prototype et le placer dans un fichier qui sera inclus à la fois dans le fichier d'implémentation de la fonction et dans le ou les fichiers qui l'utilisent.

 
Sélectionnez
/* f.h */
int f (char *s);
 
Sélectionnez
/* f.c */
#include "f.h"
int f (char *s)
{
  ...
}
 
Sélectionnez
/* main.c */
#include "f.h"
 
int main (void)
{
   int x = f("hello");
}

Pour compléter, il est fortement recommandé de protéger les fichiers .h contre les inclusions multiples avec une garde (guard) :

 
Sélectionnez
#ifndef H_F
#define H_F
/* f.h */
 
int f (char *s);
 
#endif

Détails dans cet article ici

XII. Bibliothèques de fonctions

Une bibliothèque (library) est une collection de fonctions mise à la disposition des programmeurs. Elle se compose d'une interface matérialisée par un ou plusieurs fichiers d'entêtes (.h), et d'un fichier d'implémentation qui contient le corps des fonctions (.lib, .a, .dll, .so etc.) sous forme exécutable.

Il est aussi possible à un programmeur de 'capitaliser' son travail en réalisant des fonctions réutilisables qu'il peut ensuite organiser en bibliothèques. Cette pratique est courante et encouragée. La création physique d'une bibliothèque est assez simple si on respecte quelques règles de conception, comme l'absence de globales, la souplesse et l'autonomie (voir l'article Comment construire sa bibliothèque). Elle nécessite un outil spécialisé (librarian) généralement livré avec toute implémentation du langage C.

Une bibliothèque peut être statique ou dynamique (partagée).

XII-A. Bibliothèque statique

Une bibliothèque à édition de lien statique (.lib, .a etc.) est liée à l'application pour ne former qu'un seul exécutable. La taille de celui-ci peut être importante, mais il a l'avantage d'être autonome. Cette pratique a l'avantage de la simplicité, et elle ne requiert aucune action particulière de la part d'un éventuel système lors de l'exécution du programme. Elle est très utilisée en programmation embarquée (embedded).

XII-B. Bibliothèque dynamique

Une bibliothèque à édition de lien dynamique, est un fichier séparé (.dll, .so, ...) qui doit être livré avec l'exécutable. L'intérêt est que plusieurs applications peuvent se partager la même bibliothèque, ce qui est intéressant, surtout si sa taille est importante. Dans ce cas, les exécutables sont plus petits. Autre avantage, les défauts de la bibliothèque peuvent être corrigés indépendamment des applications. Ensuite, la correction est répercutée immédiatement sur toutes les applications concernées par simple mise à jour de la bibliothèque dynamique.

Une application qui utilise une bibliothèque dynamique doit réaliser le lien avec la bibliothèque à l'exécution. Pour cela, elle utilise des appels à des fonctions système spécifiques dans sa phase d'initialisation. Les détails dépendent de la plate-forme.

XIII. Pourquoi ma fonction ne modifie pas ma variable ?

Il y a deux façon de modifier la valeur d'un objet (aka variable) avec une fonction :

  • récupérer la valeur retournée par la fonction :
 
Sélectionnez
int x = f();

ou :

 
Sélectionnez
int x;
x = f();

avec :

 
Sélectionnez
int f(void);
  • passer l'adresse de la variable dont on veut modifier la valeur à la fonction :
 
Sélectionnez
int x;
f(&x);

avec :

 
Sélectionnez
void f(int *);

Si l'objet est un pointeur, la règle est la même :

  • retourner une valeur :
 
Sélectionnez
int *px = f();

avec :

 
Sélectionnez
int *f(void);
  • passer l'adresse :
 
Sélectionnez
int *px;
f(&px);

avec :

 
Sélectionnez
void f(int **);

Il n'y a aucune chance de modifier l'entier en faisant ceci :

 
Sélectionnez
int x;
f(x);

De même, il n'y a aucune chance de modifier le pointeur en faisant cela :

 
Sélectionnez
int *px;
f(px);

XIV. Les fonctions à nombre d'arguments variable

XIV-A. Introduction

Bien que leur usage ne soit pas recommandé, car il n'y a pas ou peu de contrôles possibles par le compilateur, le langage C supporte les fonctions à nombre variable d'arguments (variadic).

Attention ! Non seulement le nombre est variable, mais aussi le type.

Un exemple bien connu est printf() :

 
Sélectionnez
printf ("s:%d\n", __FILE__, __LINE__);

XIV-B. Interface

  • la fonction admet n'importe quel type de retour ;
  • elle a obligatoirement un paramètre fixe ;
  • les paramètres variables marqués par l'ellipse '...', sont obligatoirement les derniers.

Ces prototypes sont valides :

 
Sélectionnez
void f(int a, ...);
int f(int a, char *b, ...);
/* etc. */

Le rôle du dernier paramètre fixe est de fournir un moyen de déterminer le nombre et éventuellement le type des paramètres variables (donc passés après).

Exemple :

 
Sélectionnez
void f (int n, ...);
 
<...>
{
   f(3, 'a', 'b', 'c');
}

Ici, le dernier paramètre fixe indique le nombre de caractères passés.

Un effort particulier doit être fait au niveau de la documentation de la fonction, car il n'y a pas de contrôle possible de la part du compilateur. Si on se trompe de type ou de nombre, c'est le drame…

XIV-C. Implémentation

Elle varie évidemment selon la fonction à réaliser, mais elle s'appuie sur quelques principes communs :

  • inclure <stdarg.h> pour accéder aux fonctions et macros de manipulation des paramètres variables
  • définir une variable de traitement des variadics de type va_list
  • initialiser cette variable correctement avec va_start() et le nom du dernier paramètre fixe
  • récupérer et traiter les paramètres selon la spécification de la fonction
  • terminer l'analyse proprement avec va_end()
 
Sélectionnez
#include <stdarg.h>
 
void f (int n, ...)
{
   va_list va;
   va_start (va, n);
 
   /* traitement des paramètres selon la fonction */
 
   va_end (va);
}

La récupération des paramètre se fait séquentiellement de gauche à droite par appels successifs à va_arg() en précisant le type attendu, parmi 'int', 'long', 'double', 'void*' et 'char*' [C90].

 
Sélectionnez
int x = va_arg (va, int);
double y = va_arg (va, double);
char *z = va_arg (va, char *);
/* etc. */

Il doit y avoir correspondance parfaite entre le type de la variable et celui passé en paramètre à la macro va_arg().

Là encore, la vigilance est de rigueur, car aucun contrôle n'est possible de la part du compilateur.

Exemple d'implémentation de la fonction présentée au-dessus :

 
Sélectionnez
#include <stdarg.h>
#include <stdio.h>
 
void f (int n, ...)
{
   va_list va;
   va_start (va, n);
 
   int i;
 
   for (i = 0; i < n; i++)
   {
      int c = va_arg (va, int);
      putchar (c);
   }
   putchar ('\n');
   va_end (va);
}
 
int main (void)
{
   f (3, 'a', 'b', 'c');
   f (4, 'x', 'y', 'z', 't');
 
   return 0;
}
 
Sélectionnez
abc
xyzt
 
Process returned 0 (0x0)   execution time : 0.025 s
Press any key to continue.

XIV-D. Conclusion

Nous avons jeté les bases de la construction des fonctions variadics. Attention, ces fonctions sont une source importante de problèmes invisibles au yeux du compilateur.

Ne pas les utiliser dans des projets critiques sans une procédure de validation de l'implémentation et de l'usage extrêmement rigoureuse. Dans certains domaines sensibles (médical, avionique, nucléaire), leur usage est tout simplement interdit.


précédentsommairesuivant

Copyright © 2009 Emmanuel Delahaye. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.