C Avancé

Portrait de marseillais

Du C Avancé, c'est du C compliqué. Avant de faire compliquer il faut faire propre.

Du code propre

Du code propre c'est quoi?
Ca se lit facilement

  1. Donc c'est indenté, c'est net, ca bave pas: si y'a trop d'indentation c'est qu'il faut probablement revoir le bout de code...
  2. Pas de lignes de 3 km
  3. C'est pas du C syntaxe C++ ou un truc moche du style
  4. Des fonctions pas trop longues: si ca fait plus d'une page, c'est que ca doit être divisé

Ca se comprend vite

  1. Ca se lit comme une histoire
  2. C'est commenté quand ca se corse MAIS...
  3. C'est commenté uniquement quand c'est nécessaire

Ca fait ce qu'on veut et pas plus

  1. Pas la peine de faire 3 fois la même chose
  2. Pas la peine de tester ces choses qui ne peuvent pas arriver
  3. Pas la peine de mettre des options dont on ne va pas se servir
  4. On ne se branle pas au nombre de lignes

C'est du code réfléchi:

  1. Pas de fuite de mémoire (on détruit ce qu'on alloue)
  2. Pas de boucles inutiles (ca prend du temps)
  3. Des variables dont la portée est controlée
  4. Des fonctions qui ne font qu'une seule chose à la fois
  5. Ca gère les situations d'erreurs

Les ptites feintes du préprocesseur

'''#define'''

permet de définir des constantes et des macros. Lorsque des valeurs constantes vont êtres utilisées au cours du programme, il est bon de les définir avec des '''#define''': le préprocesseur remplacera les constantes dans le code par leur valeur au moment de la compilation. C'est une utilisation à privilégier par rapport à définir des variables globales constantes.

'''#if #ifdef, #ifndef, #else, #endif'''

Sont des structures conditionnelles du préprocesseur. Cela permet notamment de "condamner" du code. Par exemple, si vous avez un morceau de code de debug que vous ne voulez pas exécuter au final, vous pouvez procéder de la sorte:

 #define DEBUG 1
 ...
 #if DEBUG
 printf("debug: %d\n", toto);
 #endif

Au moment de la compilation si DEBUG est strictement supérieur à 0 alors le code dans le '''#if''' sera compilé et vous aurez votre affichage de debug. Au moment de la compilation finale quand vous ne débuggerez plus, il suffira de mettre DEBUG à 0 pour que le code soit supprimé de la compilation.

Il est aussi possible de définir des constantes du préprocesseur directement dans la ligne de commande de gcc en utilisant l'option '''-D='''

L'autre façon de faire est d'utiliser #ifdef comme le montre l'example suivant. Dans ce cas, l'option de compilation serra simplement '''-D'''

 #define DEBUG
 ...
 #ifdef DEBUG
 printf("debug: %d\n", toto);
 #endif

Les petits mots clefs qui changent tout

'''static'''

Ce mot clef s'utilise lors de la déclaration d'une variable, avant d'en spécifier le type: par exemple:

 static int i;

Une variable déclarée statique voit sa portée étendue (ou limitée) à tout le fichier. L'utilité est surtout de limiter la portée d'une variable globale dans un projet multifichier, ou alors d'utiliser 2 fonctions ayant le même nom dans des fichiers différents.

'''inline'''

Ce mot clef s'utilise avec les procédures et fonctions dont le code au moment de l'assemblage du fichier doit être inclus directement dans la fonction appelante. L'utilité est d'augmenter la lisibilité du code en séparant les fonctions, mais de limiter les branchements et donc l'overhead des sauvegardes et restaurations de contexte au moment de l'exécution.

Il ne faut pas confondre l'utilisation d''''inline''' avec l'utilisation de macros: en effet, le code d'une macro est adapté dans le code appelant par le préprocesseur, alors que le code d'une procédure "'''inline'''" est adapté au moment de la construction du code assembleur. Dans le premier cas, la transformation de la macro en code assembleur est donc effectuée autant de fois que le code est recopié. Ce n'est pas le cas de la fonction '''inline'''.

Exemple:

 inline int max(int i1, int i2);

'''extern'''

Ce mot clef est utilisé pour éviter de déclarer deux fois une variable dans deux fichiers différents: il indique que la déclaration a déjà été effectuée, et qu'il n'est pas nécessaire d'allouer a nouveau de la mémoire et un espace dans la table des symboles, la variable y sera placée par le linker à partir d'un autre fichier.

Exemple:

extern int i;

'''const'''

Ce mot clef, placé comme les autres devant le type des variables, permet d'indiquer au compilateur que la variable ne doit pas être modifiée, autrement dit, elle doit être considérée comme une constante. Si, lors de la compilation, la valeur de la variable est modifiée, une erreur sera générée.

On peut par exemple l'utiliser avec les paramètres d'une fonction pour éviter qu'ils soient modifiés dans la fonction. C'est surtout utilisé pour les pointeurs, afin de s'assurer que la mémoire sur laquelle ils pointent n'est pas modifiée.

Exemple:

 void print_string(const char *s);

La gestion d'erreurs

En utilisant un header spécial du système, '''errno.h''', une variable de type entier dont le nom est '''errno''' est déclarée. Cette variable est utilisée pour contenir les codes de retour des appels systèmes, si ceux-ci plantent. Par exemple, si le mode d'ouverture de fichier passé à fopen est erroné, fopen placera la valeur '''-EINVAL''' dans errno.

Il existe un appel système dont le nom est '''perror''' qui prend en argument une chaine de caractère. Il consultera la valeur de errno, affichera l'erreur correspondante et enrichira le message d'erreur de la chaine de caractère passée en argument.

D'une manière générale, il est bon pour contrôler le déroulement du programme, particulièrement les parties sensibles, d'utiliser la gestion d'erreur afin d'éviter des failles. Par exemple, si le programme n'arrive pas à ouvrir le fichier de configuration, ce n'est pas la peine qu'il continue à tourner.

Voici un exemple très simple d'utilisation de '''perror()''':

 FILE *fd;
 if((fd = fopen("toto.conf","r")) == NULL) {
     perror("Erreur d'ouverture du fichier de configuration");
     exit(-1);
 }

Ce code permet de sortir du programme en affichant une erreur qui, au moment de l'exécution, est explicite.