Regrouper les variables globales dans un seul fichier header en C

Mise à jour du 1er juillet 2014 : je viens de publier un article expliquant pourquoi la technique expliquée ci-dessous est mauvaise. Je vous déconseille de l’utiliser. Lisez *ici*  pourquoi.

On répète très souvent dans les cours de langage C qu’il est mal d’utiliser des variables globales. Je ne suis pas là pour débattre de ce sujet ; chacun se fera son avis et aura ses raisons si ce n’est pas déjà fait. Il existe néanmoins des cas où il est impossible de ne pas utiliser de telles variables : dans des routines d’interruption de micro-contrôleur par exemple. Elles ne prennent pas de paramètre et ne renvoient rien. Si vous voulez modifier et sauvegarder des données d’une exécution à l’autre et que cette information soit accessible hors de cette routine, vous n’aurez pas d’autre solution que d’utiliser des variables globales. Comme par hasard, j’ai découvert la technique dont je viens vous parler aujourd’hui au cours d’un projet sur micro-contrôleur PIC de Microchip.

Quand plusieurs modules = fichiers partagent des variables globales, on déclare les variables « normalement » dans l’un des fichiers sources et on les déclare avec le mot clé extern dans les autres. C’est la méthode offrant la gestion la plus fine du partage des variables puisqu’on ne déclarera avec extern que les variables nécessaires dans le fichier courant. On évite ainsi de rendre visible dans un module une variable inutile, qu’on pourrait modifier par erreur. Cette technique induit une certaine redondance et une dispersion des déclarations ; si vous avez 10 fichiers utilisant un tableau d’entiers et que vous souhaitez modifier la taille de ce tableau, il faut modifier la déclaration dans les 10 fichiers, avec le risque d’en oublier ou de se tromper. Vous pouvez légitimement avoir envie de regrouper les déclarations dans un fichier d’en-tête que vous incluriez là où vous en avez besoin.

Il faut quand même être franc : si vous souhaitez garder la finesse des déclarations externes, il est peu probable qu’un seul fichier d’en-tête suffise. Cela signifierait que tous les fichiers utilisent toutes les variables globales considérées. Plusieurs cas de figures:

  1. ô magie vous êtes dans ce cas et un fichier convient !
  2. ô mon dieu vous êtes un gros bourrin et vous mettez toutes les variables globales du programme dans un seul fichier !
  3. ô compromis vous utilisez quelques fichiers d’inclusion, en regroupant les variables par thème, en perdant un peu en finesse mais pas trop.
  4. ô ça ne le fait pas et vous restez à la méthode de base.

La première idée est de regrouper uniquement les déclarations externes dans un fichier d’en-tête et de laisser les déclarations normales dans l’un des fichiers sources. On peut faire encore mieux en mettant toutes les déclarations dans le même fichier et un #define dans l’un des fichiers sources. Petit exemple de principe :

Fichier ressources.h : il regroupe les déclarations normales et externes des variables globales.

#ifndef RESSOURCES_H
#define RESSOURCES_H

#ifdef VAR_GLOBALES
    int VAR_1=1;
    char VAR_2[]="deux";
#else
    extern int VAR_1;
    extern char VAR_2[];
#endif
#endif

Fichier main.c : on n’y connait pas les variables globales, les en-têtes des modules donnent la connaissance des fonctions d’affichage.

#include "afficherInt.h"
#include "afficherString.h"

int main(void)
{
    afficherInt();
    afficherString();
    return 0;
}

Module 1 : fichier afficherInt.c : ce module contiendra la déclaration normales des variables grâce au #define qui va bien. Aucun autre module ne doit contenir le même #define sous peine de déclaration multiple des variables.

 // La ligne importante est la suivante /!\
#define VAR_GLOBALES
#include "ressources.h"
#include  

void afficherInt(void)
{
    printf("VAR_1 = %d\n",VAR_1);
}

En-tête : fichier afficherInt.h

#ifndef AFFICHER_INT_H
#define AFFICHER_INT_H
void afficherInt(void);
#endif

Module 2 : fichier afficherString.c : les variables y seront externes

#include "ressources.h"
#include 

void afficherString(void)
{
    printf("VAR_2 = %s\n",VAR_2);
}

En-tête : fichier afficherString.h

#ifndef AFFICHER_STRING_H
#define AFFICHER_STRING_H
void afficherString(void);
#endif
Publicités

5 Réponses

  1. JackDesBwa

    Je désapprouve fortement.
    La méthode que tu proposes ici me semble mauvaise en plusieurs points.

    D’abord, regrouper tout au même endroit est mauvais pour l’évolution du code et la maintenance en général. Il est préférable de compartimenter par lien fonctionnel : communication, calcul, outils, etc… De manière générale, encapsuler et hiérarchiser ne fait pas de mal.

    Second point, mettre du code dans un fichier header, n’est vraiment pas une bonne pratique et peut mener à des erreurs. Même si tu mets en garde le lecteur de ton billet, la personne qui reprendra le code n’en sera pas nécessairement avertie.

    Plus vicieux, imagines que tu aies plusieurs modules dont un seul définit VAR_GLOBALES :
    #define VAR_GLOBALES
    #include « superbe_entete.h »
    #include « ressources.h »
    #include

    Si « superbe_entete.h » inclue « ressources.h » car il a besoin d’une de tes variables par exemple, tu te retrouves avec du code dupliqué dans le même module qui mène à des erreurs de compilation incompréhensibles pour le pauvre mainteneur qui vient à ta suite.

    Une solution plus propre est d’initialiser ta variable globale dans un module. De la déclarer « extern » dans le fichier d’entête associé au module.
    De cette manière, celui qui veut utiliser ta variable inclue le fichier d’entête hiérarchiquement associé, par exemple sonar.h pour avoir directement accès à distance_capteur_gauche.

    Cela dit, il y a bien sûr mieux. Par exemple, tu déclare ta variables distance_capteur_gauche statique dans ton module (static distance_capteur_gauche en début de fichier). De cette manière, elle est « globale » pour l’ensemble des fonctions de ton module et c’est tout. En particulier, l’interruption qui permet de lire cette valeur peut modifier la variable si elle est codée dans ce fichier. Personne ne peut y accéder de l’extérieur.
    Là où cette solution est meilleure, c’est que tu peux faire une fonction get_distance_capteur_gauche() qui renvoie la valeur… ET entoure l’accès à la variable des désactivations d’interruption nécessaire pour éviter les problèmes d’accès concurrents qu’on oublie facilement.

    Après si ton compilateur te permet d’employer du C++, fais-en bon usage. Sur AVR 8bits par exemple, il est tout à fait possible d’utiliser une fonction statique de classe comme interruption. Tu profites alors en plus de l’encapsulation du langage.

    J'aime

    7 mars 2012 à 7:10

    • Tout d’abord, merci de lire et de commenter ! Ca fait plaisir ! Plein de choses à répondre à ton message.

      1) J’ai vu cette technique en entreprise, dans le firmware d’un lecteur CD dont j’ai parcouru le code pour savoir si je pouvais en sortir quelque chose. Je l’ai trouvé pas mal, je l’ai oublié et je m’en suis souvenu il y a peu; mais je ne m’en suis jamais servi. En faisant un billet sur cette technique, j’espérais bien avoir des avis pour la valider……. ou pas XD

      2) Je suis d’accord sur le fait ne pas tout mettre au même endroit. Je dis bien qu’on pourrait regrouper les variables par thèmes. Rien à redire ici.
      Juste une petite question : on voit souvent dans des codes un fichier « ressources.h » contenant un paquet de déclarations externes, qui est inclus un peu partout. Que penses-tu de cette méthode ?

      3) J’avoue que je n’avais pas pensé au fait que définir les variables (déclarations « normales » comme je le dis dans l’article) revenait au même que mettre le code d’une fonction. C’est Albin qui m’a fait remarquer ça. C’est du code source au final et ce n’est pas top, en effet. Dans la suite du raisonnement encore plus vicieux, tu t’éloignes largement du cas simple que je « présente » ici et que j’avais observé. Il est certain que là, ça ne le fait plus.

      4) Parlons maintenant des autres solutions.
      a) C’est la méthode classique d’interface par en-tête. Deux questions : En incluant le header hiérarchique associé, tu inclues aussi les prototypes de fonctions. Cela pose t-il un problème ? Si tu as 4 variables dans 4 headers et que tu dois inclure le tout dans un 5e module, la quantité de code incluse a t-elle un impact sur la taille de l’exécutable ?
      b) La meilleure technique est effectivement inspirée du C++ ! C’est évidemment la technique la plus tentante et la plus propre ! Qu’en est-il des performances si tu accèdes souvent à tes variables globales avec les getters et setters ? Si ton application n’a pas de contraintes temporelles importantes, je présume que ça ne pose pas de soucis ? Même questions quand tu utilises le C++ (ce que je n’ai jamais fait…)

      Merci pour le commentaire. Cela confirme encore une fois que ce qu’on voit en entreprise, même si ça a l’air bien, ne l’est pas toujours ; et que j’ai encore des choses à apprendre 🙂

      J'aime

      7 mars 2012 à 8:18

  2. JackDesBwa

    Je prends le contexte d’un code embarqué qui devra être maintenu. Mes remarques ne s’appliquent pas forcément si tu fais un projet avec une centaine de lignes de code en trois fichiers.
    Je pense en particulier à un projet que j’ai architecturé l’an dernier qui disposait quand j’ai quitté le projet de 118 fichiers de code [150 ko de sources commentées + quelques tests unitaires] produits par nos soins pour un microcontrôleur 8 bits (qui devait gérer des communication I²C avec du matériel, des mesures de temps de signaux, de l’asservissement, des communications avec une intelligence distante, du filtrage numérique, etc… avec des contraintes temps-réel fortes)

    1) Je me suis déjà exprimé sur le sujet.

    2) C’est un peu le même principe.

    Après tout dépend de la situation, mais sans précision j’aurais tendance à vouloir les séparer selon le contexte d’utilisation.

    En fait, c’est avant tout une histoire d’architecture du code : comment tu l’organises pour que l’évolution, la maintenance et éventuellement la réutilisation soient faciles et logiques.

    Tout avoir dans un fichier, ça a des avantages (comme ne pas trop se poser la question de quelle fonctionnalité on a besoin lors des inclusions ; même si aujourd’hui les outils répondent à cette question eux-même) mais ça a également des inconvénients en particulier lorsque les projets grossissent.

    Le métier d’ingénieur consiste justement à choisir les compromis.

    3) L’exemple vicieux ne s’éloigne pas beaucoup de ton cas. Il suffit que l’entête ressource.h soit utilisée quelque part dans un fichier inclus pour que la méthode montre ses limites. Et ça peut arriver assez vite.

    Évidemment, tu peux éviter cela avec une directive #undef bien placée dans ton fichier ressource.h, mais c’est du rafistolage de solution qui n’est à la base pas une bonne idée.

    4a) Absolument pas.

    Le prototype (de même que le mot clé extern) n’est pas du code compilé. Il s’agit d’une indication pour le compilateur des fonctions disponibles et surtout comment les appeler. Par la suite, l’éditeur de liens recherche les fonctions réelles (compilées depuis des sources .c par exemple en fichiers objets .o) à lier aux appels (pour simplifier, il inscrit l’adresse réelle où le compilateur avait mis une instruction call).

    Dès lors qu’il n’y a pas de code à proprement parler (principalement bidouille pas propre), l’inclusion d’un fichier d’entête en elle-même n’a pas d’impact sur la taille de l’exécutable final. Mais l’utilisation de fonctions de cet entête (inline, macro [hum !] ou template principalement) peut mener à du code supplémentaire.

    Elle a cependant une incidence sur le temps de compilation car il faut aller chercher en mémoire et interpréter le contenu de ces fichiers et des fichiers eux-même inclus dedans récursivement.

    4b1) Côté performance, la première chose à rappeler est qu’il est mille fois préférable de faire un code maintenable et lisible qui « gaspille » 2000 cycles (125µs @ 16MHz) à faire plein d’appels qu’on peut éviter ; qu’un code qui gagne 200µs au prix d’efforts de lecture et de compréhension là où il n’y en a pas forcément besoin.

    Dans le cas où tu as des contraintes de temps importantes et que ces « getters and setters » te gênent vraiment, tu peux envisager de passer en version inline ou un accès direct à la mémoire par variable globale. Une solution intermédiaire serait de récupérer l’adresse mémoire de ta variable par un « getter » et d’y accéder ensuite par déférencement. (là encore avec ces solutions attention aux accès concurrents dans le cas des variables partagées avec interruptions en particulier ; ou systèmes multitâches)

    Il peut aussi être envisagé de regarder du côté des options d’optimisation du compilateur (éventuellement appliqué différemment selon les modules, d’où l’avantage de découper son code encore une fois)

    Mais bon, quand tu as vraiment des problèmes de performance, la première démarche est de mesurer et appliquer les correctifs aux endroits où ça fait mal. Il ne faut pas chercher à retirer un appel par-ci ou un déférencement par là.

    Cela dit, j’ai l’exemple d’un « firmware » de centrale inertielle que j’ai relu et corrigé. En ajoutant principalement des mots-clé « const », des passages par référence (C++) et des listes d’initialisation (C++) aux endroits appropriés, la fréquence de sortie des données a été multipliée par 3,6.
    Autrement dit, le simple fait de coder « proprement » a permis un gain phénoménal ; car le compilateur pouvait réaliser des optimisations supplémentaires.
    Il est donc utile aussi de se demander si l’architecture logicielle qu’on a mise en place est judicieuse pour l’application en question.

    4b2) En ce qui concerne le C++, une utilisation mesurée n’apporte pas de surcharge, ou en tout cas elle est faible [tu vas élaborer le programme différemment en objet et en procédural, donc il y aura des différences].

    Dans le projet que j’évoquais au début du message, nous avons fait entrer beaucoup de fonctionnalités dans le petit microcontrôleur 8 bits, tout étant codé en C++, avec des contraintes de temps importantes [et respectées]. Il est vrai cependant qu’on atteignait les limites de la puce, mais cela était dû aux fonctions qui s’exécutaient, non au C++.

    La couche objet apporte des avantages multiples en terme de code, mais il faut être conscient de ce qu’il se passe derrière l’instanciation d’une classe par exemple ; ou savoir que le polymorphisme et les méthodes virtuelles ont un coût qu’il faut prendre un compte avec soin.

    Pour l’anecdote, j’avais réalisé un code C++ (relativement complexe cependant) pour voir l’incidence du code sur l’exécutable final. Je suis parvenu (au prix d’efforts non anodins) à produire un exécutable identique au bit près avec ces deux versions :

    DigitalPin led_pin(GPIO_B0);
    DigitalOutput led(led_pin, INVERTED);
    led.on();
    led.off();
    led.set(true);
    led.toggle();

    DDRB |= 0x01;
    PORTB &= ~0x01;
    PORTB |= 0x01;
    PORTB &= ~0x01;
    PORTB ^= 0x01;

    La version C++ me semble plus compréhensible quand même…
    Encore une fois, ce n’est pas immédiat et il faut avoir un esprit bien tordu pour effectuer et réussir une telle expérience inutile.

    J'aime

    8 mars 2012 à 2:39

  3. Pingback: Gestion des variables globales en C | Pierre Gradot

  4. Pingback: Regrouper les variables globales en C | Pierre Gradot

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s