Question:
C - envelopper les globaux dans une structure?
Dirk Bruere
2019-11-19 20:17:29 UTC
view on stackexchange narkive permalink

Une question de style firmware concernant C.

J'ai un ancien code que je suis en train de ranger.L'une des caractéristiques désagréables est la dispersion des variables globales dans les fichiers source.Je peux en faire quelques-uns des locaux, ce qui est bien.Cependant, comment gérer le reste.Je préfère créer une structure et la mettre à l'intérieur.

Je sais qu'il y a théoriquement un autre niveau d'indirection lors de leur accès, mais à partir d'un style POV est-ce meilleur ou pire que de les mettre dans des fichiers globals.c et / ou globals.h?

Dupe?https://stackoverflow.com/questions/2868651/including-c-header-file-with-lots-of-global-variables "Uniquement les déclarations dans les fichiers .h et les définitions dans les fichiers .c."
@TonyStewartSunnyskyguyEE75 Sorte de.La réponse acceptée est d'utiliser un struct.Pourtant, il y a encore plus de choses perverses dans le code, y compris le code dans #define et la priorité d'opérateur implicite utilisée, par exemple x = y / 1/2/3/4;
Pas une réponse mais une opinion: je préfère avoir des "globaux" groupés d'une manière ou d'une autre, comme `typedef struct {static const int baudrate = 115200;statique const int TX_Pin = 11;statique const int RX_Pin = 12;} UART;» De cette façon, je peux appeler `UART :: TX_Pin` ou quelque chose comme ça (pseudo code) Cela permet également aux différents groupes d'être placés là où ils correspondent le mieux (plus besoin de `globals.h`)
@FMashiro: C'est C ++, pas C.
@LaurentLARIZZA qui est un pseudo-code inspiré de `c ++`, l'idée peut être implémentée en `C` ainsi que dans pratiquement n'importe quel autre langage de programmation.
À quoi servent les variables globales?Je suis sûr qu'avec suffisamment de temps / volonté, il est possible de s'en débarrasser complètement.
@JohnGo-Soco Malheureusement, je n'ai que quelques semaines pour documenter et ranger quelque 6000 lignes de code non documenté remplies de choses telles que des globaux ayant les mêmes noms que les locaux, #defines contenant du code et des nombres magiques
@DirkBruere Pensé autant.La meilleure façon d'aller de l'avant est de se débarrasser du plus possible, mais de documenter copieusement là où vous ne pouvez pas.
Dirk, avez-vous oublié d'accepter une réponse?
Sept réponses:
Lundin
2019-11-19 20:49:17 UTC
view on stackexchange narkive permalink

Il est bien connu mauvaise pratique d'avoir un fichier "super-en-tête" comme "globals.h" ou "includes.h" etc, car en plus des globaux sont mauvais dans le En premier lieu, cela crée également une dépendance de couplage étroit entre chaque fichier unique et non lié de votre projet.

Disons que vous avez un pilote PWM et un module d'impression de débogage RS-232. Vous souhaitez récupérer le module d'impression de débogage RS-232 et le réutiliser dans un autre projet. Soudain, vous avez besoin de PWM.h dont vous n'avez aucune idée de ce que c'est ou d'où vient le besoin. Vous vous surprendrez à vous demander pourquoi diable vous avez besoin d'un pilote PWM pour exécuter RS-232.

Et puis nous n'avons même pas envisagé de rentrer dans cette structure globale désordonnée. Ne faisons même pas ça.


La bonne façon de démêler les spaghettis mondiaux serait plutôt quelque chose comme ceci:

  • La variable est-elle utilisée en premier lieu? Sinon, supprimez. (C'est assez courant)
  • La variable peut-elle être déplacée vers la portée locale à l'intérieur d'une fonction?
  • La variable peut-elle être déplacée vers la portée du fichier .c local en la rendant statique ? Pouvez-vous en réduire l'accès depuis l'extérieur du fichier .c en implémentant des fonctions setter / getter?

Si tout ce qui précède a échoué, alors la variable que vous vous trouvez en train de regarder devrait être soit un registre matériel mappé en mémoire dans une carte de registre, soit un correctif critique en temps réel qui a été ajouté pendant la maintenance.

Qu'est-ce qui ne va pas avec le fait de regrouper des variables * liées * dans une structure?Supposons que vous souhaitiez récupérer le module de pilote PWM et le réutiliser dans un autre projet où vous devez instancier deux d'entre eux.
@Bergi Pas de problème du tout s'ils sont dans le fichier .c.S'ils sont globaux dans un fichier .h, les placer dans une structure n'est pas une amélioration évidente.S'ils appartiennent ensemble, ils peuvent être dans une structure, quel que soit l'endroit où les instances de cette structure sont déclarées.
J'aime la troisième option où la variable est définie dans le fichier .c et les fonctions get / set uniquement sont dans l'en-tête.
@danmcb: Je suppose qu'avec l'optimisation du temps de liaison, cela ne coûte, espérons-le, aucune performance et vous empêche de prendre l'adresse du global.IDK si un appel de fonction global est beaucoup plus lisible qu'un nom de variable, en fonction des noms choisis, mais cela le rend différent des autres variables.En faire une portée de fichier `statique` avec getter / setter n'améliore pas vraiment les choses si elle est toujours accessible partout dans le style spaghetti.Cette réponse suggère que c'est utile si * la plupart * de l'utilisation peut être contenue dans un fichier, mais que vous avez toujours besoin d'un accès extérieur.
Il convient de souligner que la "portée locale" peut toujours avoir une classe de stockage statique.Méfiez-vous simplement qu'un initialiseur non-const n'est plus une erreur de compilation comme il est à portée globale en C, et qu'il vous coûte à la place des performances d'exécution pour vérifier un indicateur déjà initialisé.(Et au premier appel pour faire une init thread-safe.)
@PeterCordes Un avantage évident avec les setter / getters est que de nombreuses variables d'étendue de fichier dans les systèmes embarqués sont présentes car elles sont utilisées pour communiquer avec un ISR.Ensuite, environ 90% de tous les logiciels embarqués écrits ne parviennent pas à protéger correctement ces variables contre les conditions de concurrence.Lorsque le programmeur découvre finalement que leurs rares bogues intermittents sont causés par cela, ils doivent implémenter la ré-entrée et alors les setter / getters sont l'endroit idéal pour le faire.Impossible de faire cela correctement dans le spaghettiware qui permet à d'autres unités de traduction de communiquer directement avec l'ISR.
@PeterCordes à droite.Comme le dit Lundin, s'il devient plus tard nécessaire de s'assurer que "chaque fois que je modifie la valeur de X, je devrais aussi vérifier que ..." vous avez un endroit facile pour le faire si vous avez utilisé get / set.Cela arrive assez souvent à mesure que ces types de systèmes évoluent.Je n'ai jamais vu la surcharge de l'appel être un problème, si c'était le cas, je suppose que vous pourriez le déclarer en ligne ou quelque chose du genre.En tout cas pas besoin d'optimiser prématurément.
Anders Petersson
2019-11-19 20:31:55 UTC
view on stackexchange narkive permalink

Cela fonctionnerait pour définir une structure que vous instanciez en tant que variable globale unique.Les accès sur le formulaire «the_global.the_var» n'ajouteront pas de surcharge d'exécution et peuvent clarifier qu'il s'agit bien d'un global. Comme le mentionne https://stackoverflow.com/questions/2868651/including-c-header-file-with-lots-of-global-variables, cela vous évite des déclarations et définitions séparées.

Personnellement, je ne prendrais pas la peine de créer une structure, préférant plutôt trier les globaux dans les fichiers d'en-tête où je pense qu'ils appartiennent logiquement et utiliser un préfixe commun pour chaque fichier d'en-tête.Exemple: fichier Calculate.h déclarant "extern int calc_result;"et calculer.c définissant "int calc_result;"

D'autres variables s'en sortent en étant localement dans un fichier, c'est-à-dire "résultat int statique;"dans le fichier .c.

Puisque vous avez du code hérité avec lequel je suppose que vous ne travaillerez pas beaucoup au-delà du rangement, je dirais que la solution la plus rapide qui crée une structure claire est la meilleure.

+1 ne pas en faire trop avec l'ancien code.Bien sûr, vous pourriez faire mieux avec une réécriture, etc. (même les auteurs originaux le pourraient probablement), mais laissez-moi vous assurer qu'il y a beaucoup plus de code à écrire dans votre carrière.Concentrez-vous sur l'amener à un stade plus maintenable le plus rapidement possible et passez à autre chose.
Geoxion
2019-11-19 20:29:04 UTC
view on stackexchange narkive permalink

Si vous ne pouvez pas vous débarrasser des globaux, je dirais que vous ne devriez les regrouper dans une structure que s'ils sont réellement liés.Sinon, je les garderais séparés ou dans des structures plus petites.

De plus, je n'aimerais pas non plus un fichier globals.h.Gardez-les en haut du fichier source là où ils appartiennent le plus.De cette façon, lorsque vous naviguez dans le code, vous restez probablement à l'endroit où vous étiez ou allez à l'endroit où vous vouliez probablement aller.

Aussi: les mettre dans une structure sans nécessité pourrait induire en erreur les futurs programmeurs (y compris vous-même) d'allouer plusieurs instances de ces structures;et puis ça devient vraiment déroutant
@Curd D'un autre côté, si le code est écrit suffisamment modulaire, il * permet * d'instancier facilement de telles structures plusieurs fois sans être une nuisance.
Mais d'après le contexte, je suppose qu'il est non seulement improbable que ce soit nécessaire mais même nuisible si ces structures existaient plusieurs fois.
Cela me semble être l'approche la plus pragmatique.L'OP préférerait clairement ne pas avoir de globaux, donc s'ils pouvaient être supprimés sans une énorme quantité de refactoring, la question ne serait pas posée.
En général, il n'y a rien de mal à rassembler certaines variables globales dans une structure - si elles sont liées les unes aux autres (par exemple, appartenant à la même interface).Ce n'est pas une bonne idée, cependant, de mettre ** tous ** globaux ensemble dans une structure ** pour aucune autre raison que d'être des globaux **.
Michel Keijzers
2019-11-19 20:29:21 UTC
view on stackexchange narkive permalink

En fait, ce n'est pas mieux, le seul avantage est que vous «savez» ce qui est global. Vous avez donc tous les globaux au même endroit, mais ils sont toujours dispersés.

C

Pour chaque global:

  • Recherchez le fichier initial dans lequel il est créé ou utilisé.
  • Rendez-le statique dans ce fichier
  • Si le global est utilisé à de nombreux endroits, utilisez-le en externe à partir d'autres fichiers (en utilisant le mot-clé extern ).
  • Mais le moyen préférable est de transmettre la variable aux endroits où elle est nécessaire.

C ++

Pour chaque global:

  • Trouvez l'endroit initial où il est créé ou utilisé.
  • Trouvez la classe qui convient le mieux.
  • Déplacez-le dans cette classe (en tant que champ, connecté à un objet).
  • Créez maintenant une méthode Get / Set (fonctions). De préférence, la méthode Set est protégée ou privée).
  • Au lieu des méthodes Get et Set, vous pouvez également les transmettre via des arguments de méthode.
  • Utilisez les méthodes Get / Set d'autres classes.
  • Dans certains cas, vous verrez qu'il ne peut pas être un champ d'une classe, par exemple quand il ne peut y en avoir qu'un. Dans ce cas, utilisez le modèle de conception Singleton. (Méfiez-vous de cela; c'est toujours une sorte de mondial, alors assurez-vous qu'à l'avenir vous n'en ferez plus jamais plus; sinon, c'est un espace global déguisé).
Eh bien, C ++ serait beaucoup plus agréable, mais je suis coincé avec C à moins que je ne fasse une réécriture totale (ce que j'aimerais faire, mais pas de temps, etc.)
La portée de fichier «statique» @DirkBruere est un moyen parfaitement adapté d'encapsulation privée dans des systèmes MCU à un seul cœur et à processus unique.La plupart du temps, vous n'avez pas besoin d'encapsulation privée OO, mais si vous le faites, regardez le concept de _opaque type_ sur SO.
@DirkBruere Je pensais vraiment avoir vu C ++, mais j'ai mis à jour ma réponse.Gardé le C ++ juste pour être complet.
* "C ... Rendez-le statique dans cette classe" * - devrait-il être * "C ... Rendez-le statique dans ce fichier" *?
Les concepts @DigitalTrauma OO comme les classes sont indépendants du langage.C n'a pas de mot-clé `class` mais vous pouvez avoir des classes dans n'importe quel langage de programme.C'est un terme de conception de programme.
@DigitalTrauma Merci, le fichier est meilleur dans ce cas, mais comme l'explique Lundin, un fichier peut être utilisé comme classe (au moins comme base).
@DirkBruere, Je ne connais pas les spécificités de votre environnement, mais passer du C au C ++ ne signifie pas en général une réécriture totale.Il n'est généralement pas trop difficile de traduire du code de C vers le sous-ensemble commun de C et C ++, vous pouvez alors basculer le compilateur et commencer à utiliser les fonctionnalités C ++ dans le nouveau code.
Je pense que stackoverflow alimente souvent une fausse dichtomie selon laquelle il faut écrire "plain C" ou "C ++ moderne" et que les points intermédiaires entre les deux sont en quelque sorte invalides.
Un fichier "peut être utilisé comme classe" mais souvent il ne devrait pas - à moins que vous ne programmiez dans un langage comme Java qui mélange des concepts de conception de programme comme les classes et des détails d'implémentation comme le nom de vos fichiers.
@PeterGreen D'accord… Je vois au travail que les gens pensent qu'ils écrivent en C ++ moderne mais en fait ils écrivent à peine C en utilisant à peine les principes OO.
@alephzero Je ne sais pas ce que vous voulez dire… En C et C ++, vous avez des fichiers .h et .cpp.Si un programmeur C structure bien son code, une classe est plus ou moins un fichier (ou peut être convertie en une autre).
Je dois admettre que je préfère utiliser statique volatile pour les variables mises à jour * uniquement * dans les gestionnaires d'interruptions et les déclarer extern là où elles sont testées / mises à jour (j'ai * fait * un fichier d'en-tête pour ces déclarations externes si elles se regroupent naturellement).
grahamj42
2019-11-21 03:03:47 UTC
view on stackexchange narkive permalink

Les grands avantages à placer des variables globales dans une structure sont:

  1. C'est clair lorsque vous utilisez une variable globale ou locale.
  2. Il est impossible de masquer par accident une variable globale avec une variable locale du même nom.

Les avantages moindres sont:

  1. Il est plus facile de déboguer les débogages dans un tableau global ou une mémoire tampon car la disposition de la mémoire peut être déduite de la structure.
  2. La structure globale peut être très simplement enregistrée dans / chargée depuis un stockage non volatile.

Une utilisation sensée des variables globales peut réduire les exigences de la pile, mais augmente le risque d'erreur si leur utilisation n'est pas bien documentée et les règles respectées.

Laurent LA RIZZA
2019-11-21 18:28:24 UTC
view on stackexchange narkive permalink

Pourquoi y aurait-il un autre niveau d'indirection d'accès aux variables dans une struct ? Si vous déclarez votre structure comme ceci:

  typedef struct tagUART {
    int field1;
    int field2;
} UART;
 

Ensuite, déclarez un global:

  UART uart;
 

Au lieu de déclarer ces globaux:

  int uartField1;
int uartField2;
 

Le compilateur, aussi merdique qu'il puisse être, n'a aucune excuse pour introduire un autre niveau d'indirection si vous accédez à votre variable via uart.field1 qu'avec un accès à uartField1 . Le compilateur connaît l'adresse de départ de votre structure et la déclaration struct définit le décalage de champ dans la structure. Le compilateur sait tout au point de référence.

Maintenant, le niveau supplémentaire d'indirection se produirait si vous commencez à passer des pointeurs vers uart comme arguments de fonction, et les appels de fonction ne sont pas incorporés. Là, le compilateur perd le fait que "Hé, les champs ont une adresse bien connue".

Peter Green
2019-12-20 01:49:47 UTC
view on stackexchange narkive permalink

Je sais qu'il existe théoriquement un autre niveau d'indirection lors de leur accès,

Ça dépend, un compilateur vraiment stupide peut introduire des niveaux supplémentaires d'indirection pour une structure, mais un compilateur intelligent peut en fait produire un meilleur code si une structure est impliquée.

Contrairement à d'autres réponses, le compilateur NE connaît PAS l'adresse des variables globales. Pour les variables globales dans la même unité de compilation, il connaît leur emplacement les uns par rapport aux autres, mais pas leur emplacement absolu. Pour les variables d'autres unités de compilation, il ne sait rien de leur emplacement en termes absolus ou relatifs.

Au lieu de cela, le code généré par le compilateur aura des espaces réservés pour les emplacements des variables globales, ceux-ci seront ensuite remplacés par des emplacements réels lorsque l'adresse finale est déterminée (traditionnellement lorsque le programme est lié, mais parfois pas avant son exécution ).

De plus, bien que certains processeurs (comme le 6502) puissent accéder directement à un emplacement mémoire global sans aucune configuration préalable, il y en a beaucoup qui ne le peuvent pas. Sur arm par exemple, pour accéder à une variable globale, le compilateur doit d'abord charger l'adresse de la variable dans un registre, soit en utilisant un pool littéral, soit, sur les versions plus récentes d'arm, en utilisant movw / movt. Accédez ensuite à la variable globale en utilisant une instruction de chargement relative au registre.

Cela signifie que pour les variables en dehors de l'unité de compilation, l'accès à plusieurs éléments de la même structure globale est probablement plus efficace que l'accès à des variables globales individuelles.

Pour tester cela, j'ai introduit le code suivant dans godbolt avec l'optimisation ARM gcc 8.2 et -O3.

  extern int a;
extern int b;

struct Foo {
    int a;
    int b;
};

extern struct Foo foo;

void f1 (void) {
    a = 1;
    b = 2;
}

void f2 (void) {
    toto.a = 1;
    toto.b = 2;
}
 

Cela a abouti à

  f1:
        mov r0, # 1
        mov r2, # 2
        ldr r1, .L3
        ldr r3, .L3 + 4
str r0, [r1]
        str r2, [r3]
        bx lr
.L3:
        .word a
        .word b
f2:
        mov r1, # 1
        mov r2, # 2
        ldr r3, .L6
        stm r3, {r1, r2}
        bx lr
.L6:
        .word foo
 

Dans le cas de f1, nous voyons que le compilateur charge les adresses de a et b séparément des pools littéraux (les adresses dans les pools littéraux seront remplies plus tard par l'éditeur de liens).

Cependant dans le cas de f2 le compilateur n'a qu'à charger l'adresse de la structure du pool littéral une seule fois, mieux encore car il sait que les deux variables sont côte à côte en mémoire il peut les écrire avec un seul "storeplusieurs "instructions plutôt que deux instructions de magasin séparées.

mais à partir d'un style de POV, est-ce mieux ou pire que de les mettre dans des fichiers globals.c et / ou globals.h?

IMO qui dépend si les variables sont réellement liées ou non.



Ce Q&R a été automatiquement traduit de la langue anglaise.Le contenu original est disponible sur stackexchange, que nous remercions pour la licence cc by-sa 4.0 sous laquelle il est distribué.
Loading...