Domanda:
C - avvolgere le globali in una struttura?
Dirk Bruere
2019-11-19 20:17:29 UTC
view on stackexchange narkive permalink

Una domanda sullo stile del firmware riguardante C.

Ho un codice legacy che sto riordinando.Una delle caratteristiche sgradevoli è che le variabili globali vengono sparse nei file sorgente.Posso crearne alcuni locali, il che va bene.Tuttavia, come affrontare il resto.Preferisco creare una struttura e inserirla al suo interno.

So che teoricamente esiste un altro livello di riferimento indiretto quando si accede a loro, ma da uno stile POV è meglio o peggio che metterli nei file globals.c e / o globals.h?

Ingannare?https://stackoverflow.com/questions/2868651/including-c-header-file-with-lots-of-global-variables "Solo le dichiarazioni nei file .h e le definizioni nei file .c."
@TonyStewartSunnyskyguyEE75 Sorta di.La risposta accettata è usare una struttura.Tuttavia, ci sono ancora più cose malvagie nel codice, incluso il codice in #define e la priorità dell'operatore implicita usata ad esempio x = y / 1/2/3/4;
Non una risposta ma un'opinione: preferisco avere "globals" raggruppati in qualche modo, come `typedef struct {static const int baudrate = 115200;const statico int TX_Pin = 11;static const int RX_Pin = 12;} UART;" In questo modo posso chiamare `UART :: TX_Pin` o qualcosa del genere (pseudo codice) Questo permette anche ai diversi gruppi di essere posizionati dove meglio si adattano (non c'è più bisogno di `globals.h`)
@FMashiro: Questo è C ++, non C.
@LaurentLARIZZA che è uno pseudo codice ispirato a `c ++`, l'idea può essere implementata in `C` così come praticamente in qualsiasi altro linguaggio di programmazione.
A cosa servono le variabili globali?Sono sicuro che con abbastanza tempo / volontà, è possibile sbarazzarsene completamente.
@JohnGo-Soco Sfortunatamente ho solo un paio di settimane per documentare e riordinare circa 6000 righe di codice non documentato piene di cose come le variabili globali che hanno gli stessi nomi dei locali, #defines contenenti codice e numeri magici
@DirkBruere Pensato altrettanto.Il modo migliore per andare avanti è davvero sbarazzarsi di quanto puoi, ma documentare copiosamente dove non puoi.
Dirk, hai dimenticato di accettare una risposta?
Sette risposte:
Lundin
2019-11-19 20:49:17 UTC
view on stackexchange narkive permalink

È ben nota la cattiva pratica di avere un file "super header" come "globals.h" o "includes.h" ecc., perché oltre alle variabili globali sono cattive nel in primo luogo, questo crea anche una stretta dipendenza di accoppiamento tra ogni singolo file non correlato nel progetto.

Supponiamo che tu abbia un driver PWM e un modulo di stampa di debug RS-232. Vuoi prendere il modulo di stampa di debug RS-232 e riutilizzarlo in un altro progetto. All'improvviso ti ritrovi ad aver bisogno di un PWM.h di cui non hai la più pallida idea di cosa sia o da dove venga il bisogno. Ti ritroverai a chiederti perché diavolo hai bisogno di un driver PWM per eseguire RS-232.

E poi non abbiamo nemmeno considerato il rientro di questa struttura globale disordinata. Non lo facciamo nemmeno.


Il modo corretto per districare gli spaghetti globali sarebbe piuttosto qualcosa del genere:

  • La variabile viene utilizzata in primo luogo? In caso contrario, rimuovere. (Questo è abbastanza comune)
  • La variabile può essere spostata in ambito locale all'interno di una funzione?
  • È possibile spostare la variabile nell'ambito del file .c locale rendendola statica ? Puoi ridurre l'accesso dall'esterno del file .c implementando le funzioni setter / getter?

Se tutto quanto sopra non è riuscito, la variabile che ti trovi a guardare dovrebbe essere una parte del registro hardware mappata in memoria di una mappa di registro, oppure è una correzione sporca critica in tempo reale che è stata aggiunta durante la manutenzione.

Cosa c'è di sbagliato nel mettere insieme variabili * correlate * in una struttura?Supponiamo che tu voglia prendere il modulo del driver PWM e riutilizzarlo in un altro progetto in cui devi istanziarne due.
@Bergi Nessun problema se sono nel file .c.Se sono globali in un file .h, inserirli in una struttura non è un miglioramento evidente.Se appartengono insieme potrebbero essere in una struttura, indipendentemente da dove vengono dichiarate le istanze di quella struttura.
Mi piace la terza opzione in cui la variabile è definita nel file .c e le funzioni get / set sono solo nell'intestazione.
@danmcb: Immagino che con l'ottimizzazione del tempo di collegamento si spera che non costi alcuna prestazione e ti impedisca di prendere l'indirizzo del globale.IDK se qualche chiamata di funzione globale è molto più leggibile del nome di una variabile, a seconda dei nomi scelti, ma la fa sembrare diversa dalle altre variabili.Rendendolo uno scope di file `statico` con getter / setter non migliora davvero le cose se ha ancora accesso dappertutto in stile spaghetti.Questa risposta suggerisce che è utile se * la maggior parte * dell'uso può essere contenuto in un file, ma è comunque necessario un accesso esterno.
Vale la pena sottolineare che "l'ambito locale" può ancora avere una classe di archiviazione statica.Fai solo attenzione che un inizializzatore non const non è più un errore in fase di compilazione come è nell'ambito globale in C, e invece ti costa le prestazioni di runtime per controllare un flag già inizializzato.(E alla prima chiamata per eseguire l'inizializzazione thread-safe.)
@PeterCordes Un ovvio vantaggio di setter / getter è che molte variabili di ambito di file nei sistemi embedded sono presenti perché vengono utilizzate per comunicare con un ISR.Quindi circa il 90% di tutto il software incorporato scritto non riesce a proteggere adeguatamente tali variabili dalle race condition.Quando il programmatore alla fine scopre che i loro rari bug intermittenti sono causati da questo, devono implementare il re-entrancy e quindi i setter / getter sono il posto perfetto per farlo.Impossibile farlo correttamente in spaghettiware che consente ad altre unità di traduzione di comunicare direttamente con l'ISR.
@PeterCordes a destra.Come dice Lundin, se in seguito diventa necessario assicurarsi che "ogni volta che modifico il valore di X dovrei anche controllare che ..." hai un posto facile per farlo se hai usato get / set.Questo accade un bel po 'mentre questi tipi di sistema si evolvono.Non ho mai visto il sovraccarico della chiamata come un problema, se lo fosse immagino che potresti dichiararlo in linea o qualcosa del genere.Comunque non c'è bisogno di ottimizzare prematuramente.
Anders Petersson
2019-11-19 20:31:55 UTC
view on stackexchange narkive permalink

Funzionerebbe per definire una struttura di cui istanziare come singola variabile globale.Gli accessi nella forma "the_global.the_var" non aggiungeranno un sovraccarico di runtime e possono chiarire che è effettivamente globale. Come menzionato https://stackoverflow.com/questions/2868651/including-c-header-file-with-lots-of-global-variables, ti salva da dichiarazioni e definizioni separate.

Personalmente non mi preoccuperei di creare una struttura, preferendo invece ordinare le globali nei file di intestazione a cui sento che logicamente appartengono e utilizzare un prefisso comune per ogni file di intestazione.Esempio: file calcola.h che dichiara "extern int calc_result;"e calcola.c definendo "int calc_result;"

Altre variabili se la cavano con l'essere file locali, ad esempio "static int result;"nel file .c.

Dato che hai un codice legacy con cui immagino che non lavorerai molto oltre a metterlo in ordine, direi che la soluzione più rapida che consente una struttura chiara è la migliore.

+1 non farlo più sul codice legacy.Ovviamente potresti farlo meglio con una riscrittura, ecc. (Anche gli autori originali potrebbero farlo), ma lascia che ti assicuri che c'è molto più codice da scrivere nella tua carriera.Concentrati sul portarlo a uno stadio più manutenibile il più rapidamente possibile e vai avanti.
Geoxion
2019-11-19 20:29:04 UTC
view on stackexchange narkive permalink

Se non puoi sbarazzarti delle variabili globali, direi che dovresti comprimerle insieme in una struttura solo se sono effettivamente correlate.In caso contrario, li terrei separati o in strutture più piccole.

Inoltre, non vorrei nemmeno un file globals.h.Tienili in cima al file sorgente a cui appartengono maggiormente.In questo modo, quando navighi nel codice, probabilmente rimani nel luogo in cui ti trovavi o vai nel luogo in cui probabilmente volevi andare.

Inoltre: inserirli in una struttura senza necessità potrebbe indurre in errore i futuri programmatori (incluso te stesso) ad allocare più istanze di tali strutture;e poi diventa davvero confuso
@Curd D'altra parte, se il codice è scritto in modo abbastanza modulare, * consente * di istanziare facilmente tali strutture più volte senza essere un fastidio.
Ma dal contesto presumo che non solo sia improbabile che sia necessario ma anche dannoso se quelle strutture esistessero più volte.
Questo suona come l'approccio più pragmatico per me.L'OP chiaramente preferirebbe non avere alcun tipo di globali, quindi se potessero essere rimossi senza un'enorme quantità di refactoring, la domanda non sarebbe stata posta.
In generale non c'è niente di sbagliato nel mettere insieme determinate variabili globali in una struttura, se sono correlate tra loro (ad esempio, appartengono alla stessa interfaccia).Non è una buona idea, tuttavia, mettere insieme ** qualsiasi ** globale in una struttura ** per nessun altro motivo se non per essere globali **.
Michel Keijzers
2019-11-19 20:29:21 UTC
view on stackexchange narkive permalink

In realtà, non è meglio, l'unico vantaggio è che "sai" quali sono globali. Quindi hai tutte le globali in un unico posto, ma sono ancora sparse.

C

Per ogni globale:

  • Trova il file iniziale in cui è stato creato o utilizzato.
  • Rendilo statico in quel file
  • Nel caso in cui il global venga utilizzato in molti posti, usalo esternamente da altri file (utilizzando la parola chiave extern ).
  • Ma il modo preferibile è passare la variabile nei punti in cui è necessaria.

C ++

Per ogni globale:

  • Trova la posizione iniziale in cui viene creato o utilizzato.
  • Trova la classe a cui si adatta meglio.
  • Spostalo in quella classe (come campo, connesso a un oggetto).
  • Ora crea un metodo Get / Set (funzioni). Preferibilmente il metodo Set è protetto o privato).
  • Invece dei metodi Get e Set, puoi anche passarli attraverso gli argomenti del metodo.
  • Utilizza i metodi Get / Set di altre classi.
  • In alcuni casi vedrai che non può essere un campo di una classe, ad esempio quando può essercene solo "uno". In questo caso, usa il design pattern Singleton. (Attenzione a questo; è ancora una specie di globale, quindi assicurati di non farne mai più in futuro; altrimenti è uno spazio globale sotto mentite spoglie).
Bene, C ++ sarebbe molto più carino, ma sono bloccato con C a meno che non esegua una riscrittura totale (cosa che vorrei fare, ma non ho tempo ecc.)
@DirkBruere l'ambito del file `statico` è un modo perfetto di incapsulamento privato in sistemi MCU single-core e single-process.Il più delle volte, non hai bisogno di incapsulamento privato OO, ma se lo fai, guarda il concetto di _paque type_ su SO.
@DirkBruere Pensavo davvero di aver visto C ++, ma ho aggiornato la mia risposta.Ho mantenuto il C ++ solo per completezza.
* "C ... Rendilo statico in quella classe" * - dovrebbe essere * "C ... Rendilo statico in quel file" *?
@DigitalTrauma I concetti OO come le classi sono indipendenti dalla lingua.C non ha una parola chiave `class` ma puoi avere classi in qualsiasi linguaggio di programma.È un termine di progettazione del programma.
@DigitalTrauma Grazie, il file in questo caso è migliore, ma come spiega Lundin, un file può essere utilizzato come classe (almeno come base).
@DirkBruere, Non conosco le specifiche del tuo ambiente, ma passare da C a C ++ non significa in generale una riscrittura totale.In genere non è troppo difficile tradurre il codice da C al sottoinsieme comune di C e C ++, quindi puoi cambiare il compilatore e iniziare a utilizzare le funzionalità C ++ nel nuovo codice.
Sento che stackoverflow alimenta spesso una falsa dictomia che si dovrebbe scrivere "C semplice" o "C ++ moderno" e che i punti intermedi tra i due in qualche modo non sono validi.
Un file "può essere usato come una classe" ma spesso non dovrebbe - a meno che non stiate programmando in un linguaggio come Java che mescola concetti di progettazione del programma come classi e dettagli di implementazione come ciò che devono essere i nomi dei file.
@PeterGreen D'accordo… Vedo al lavoro che le persone pensano di scrivere in C ++ moderno ma in realtà scrivono C a malapena usando i principi OO.
@alephzero Non sono sicuro di cosa intendi ... Sia in C che in C ++ hai file .h e .cpp.Se un programmatore C struttura bene il suo codice, una classe è più o meno un file (o può essere convertita l'una nell'altra).
Devo ammettere che preferisco usare static volatile per variabili aggiornate * solo * nei gestori di interrupt e dichiararle extern dove vengono testate / aggiornate (* ho * un file di intestazione per quelle istruzioni extern se si raggruppano naturalmente insieme).
grahamj42
2019-11-21 03:03:47 UTC
view on stackexchange narkive permalink

I grandi vantaggi dell'inserimento di variabili globali in una struttura sono:

  1. È chiaro quando utilizzi una variabile globale rispetto a una variabile locale.
  2. È impossibile mascherare accidentalmente una variabile globale con una variabile locale con lo stesso nome.

I vantaggi minori sono:

  1. È più facile eseguire il debug degli overflow in un array globale o in un buffer di memoria perché il layout della memoria può essere dedotto dalla struttura.
  2. La struttura globale può essere molto semplicemente salvata / caricata da una memoria non volatile.

Un uso ragionevole delle variabili globali può ridurre i requisiti di stack, ma aumenta il rischio di errore se il loro utilizzo non è ben documentato e le regole rispettate.

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

Perché dovrebbe esserci un altro livello di accesso indiretto alle variabili in una struct ? Se dichiari la tua struttura in questo modo:

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

Quindi dichiara un globale:

  UART uart;
 

Invece di dichiarare questi valori globali:

  int uartField1;
int uartField2;
 

Il compilatore, per quanto schifoso possa essere, non ha scuse per introdurre un altro livello di riferimento indiretto se accedi alla tua variabile tramite uart.field1 rispetto a quello che farebbe con un accesso a uartField1 . Il compilatore conosce l'indirizzo iniziale della vostra struttura e la dichiarazione della struttura definisce l'offset del campo all'interno della struttura. Il compilatore sa tutto nel punto di riferimento.

Ora, il livello extra di riferimento indiretto si verifica se inizi a passare i puntatori a uart come argomenti di funzione e le chiamate di funzione non vengono inline. Lì, il compilatore perde il fatto che "Ehi, i campi hanno un indirizzo ben noto".

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

So che teoricamente esiste un altro livello di riferimento indiretto quando si accede ad essi,

Beh, dipende, un compilatore veramente stupido può introdurre livelli extra di riferimento indiretto per una struttura, ma un compilatore intelligente può effettivamente produrre codice migliore se è coinvolta una struttura.

Contrariamente ad altre risposte, il compilatore NON conosce l'indirizzo delle variabili globali. Per le variabili globali nella stessa unità di compilazione conosce la loro posizione l'una rispetto all'altra, ma non la loro posizione assoluta. Per le variabili in altre unità di compilazione non sa nulla della loro posizione in termini assoluti o relativi.

Invece il codice generato dal compilatore avrà dei segnaposto per le posizioni delle variabili globali, queste saranno poi sostituite dalle posizioni effettive quando viene determinato l'indirizzo finale (tradizionalmente quando il programma è collegato, ma a volte non finché non viene eseguito ).

Inoltre, mentre ci sono alcune CPU (come il 6502) che possono accedere direttamente a una posizione di memoria globale senza alcuna configurazione precedente, ce ne sono molte che non possono. Ad esempio, su arm, per accedere a una variabile globale, il compilatore deve in genere caricare prima l'indirizzo della variabile in un registro, utilizzando un pool letterale o, nelle versioni più recenti di arm, utilizzando movw / movt. Quindi accedi alla variabile globale utilizzando un'istruzione di caricamento relativa al registro.

Ciò significa che per variabili esterne all'unità di compilazione l'accesso a più elementi della stessa struttura globale è probabilmente più efficiente rispetto all'accesso a singole variabili globali.

Per testarlo ho inserito il seguente codice in godbolt con ARM gcc 8.2 e ottimizzazione -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) {
    foo.a = 1;
    foo.b = 2;
}
 

Questo ha portato a

  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
 

Nel caso di f1 vediamo che il compilatore carica gli indirizzi di aeb separatamente dai pool letterali (gli indirizzi nei pool letterali verranno inseriti successivamente dal linker).

Tuttavia nel caso di f2 il compilatore deve caricare solo una volta l'indirizzo della struttura dal pool letterale, meglio ancora perché sa che le due variabili sono una accanto all'altra in memoria può scriverle con un unico "archiviopiù "istruzioni" anziché due istruzioni separate per il negozio.

ma da uno stile POV è meglio o peggio che metterli nei file globals.c e / o globals.h?

IMO che dipende se le variabili sono effettivamente correlate o meno.



Questa domanda e risposta è stata tradotta automaticamente dalla lingua inglese. Il contenuto originale è disponibile su stackexchange, che ringraziamo per la licenza cc by-sa 4.0 con cui è distribuito.
Loading...