È meglio usare #define o const int per le costanti?


26

Arduino è uno strano ibrido, in cui alcune funzionalità C ++ sono utilizzate nel mondo embedded, tradizionalmente un ambiente C. In effetti, molto codice Arduino è molto simile a C.

C ha tradizionalmente usato #defines per costanti. Ci sono diverse ragioni per questo:

  1. Non è possibile impostare le dimensioni dell'array utilizzando const int.
  2. Non è possibile utilizzare const intcome etichette di istruzione case (anche se questo funziona in alcuni compilatori)
  3. Non è possibile inizializzare un constcon un altro const.

Puoi controllare questa domanda su StackOverflow per ulteriori ragionamenti.

Quindi, cosa dovremmo usare per Arduino? Tendo a farlo #define, ma vedo un po 'di codice usando conste un po' usando una fusione.


un buon ottimizzatore lo renderà discutibile
maniaco del cricchetto,

3
Veramente? Non vedo come un compilatore risolverà cose come la sicurezza dei tipi, non potendo usare il per definire la lunghezza dell'array e così via.
Cybergibbons,

Sono d'accordo. Inoltre, se guardi la mia risposta qui sotto, dimostro che ci sono circostanze in cui non sai davvero quale tipo usare, quindi #defineè la scelta ovvia. Il mio esempio è nella denominazione dei pin analogici, come A5. Non esiste un tipo appropriato che possa essere usato come a, constquindi l'unica scelta è quella di utilizzare a #definee lasciare che il compilatore lo sostituisca come input di testo prima di interpretare il significato.
SDsolar,

Risposte:


21

È importante notare che const intnon si comporta in modo identico in C e in C ++, quindi in realtà molte delle obiezioni contro cui sono state accennate nella domanda originale e nella risposta estesa di Peter Bloomfields non sono valide:

  • In C ++, le const intcostanti sono valori di tempo di compilazione e possono essere utilizzate per impostare limiti di array, come etichette di casi, ecc.
  • const intle costanti non occupano necessariamente alcuna memoria. A meno che tu non prenda il loro indirizzo o li dichiari esterni, in genere avranno solo un'esistenza in fase di compilazione.

Tuttavia, per le costanti intere, è spesso preferibile utilizzare un (con nome o anonimo) enum. Mi piace spesso perché:

  • È retrocompatibile con C.
  • È quasi sicuro come il tipo const int(altrettanto sicuro come il tipo in C ++ 11).
  • Fornisce un modo naturale di raggruppare le costanti correlate.
  • Puoi persino usarli per una certa quantità di controllo dello spazio dei nomi.

Quindi in un programma C ++ idiomatico, non c'è alcun motivo da usare #defineper definire una costante intera. Anche se vuoi rimanere compatibile con C (a causa di requisiti tecnici, perché lo stai dando alla vecchia scuola o perché le persone con cui lavori lo preferiscono in quel modo), puoi comunque usare enume dovresti farlo, piuttosto che usare #define.


2
Sollevi alcuni punti eccellenti (soprattutto riguardo ai limiti dell'array - non avevo ancora realizzato che il compilatore standard con IDE Arduino lo supportasse). Non è del tutto corretto affermare che una costante di compilazione non utilizza memoria, tuttavia, poiché il suo valore deve ancora trovarsi nel codice (ovvero nella memoria del programma anziché nella SRAM) ovunque venga utilizzato. Ciò significa che influisce su Flash disponibile per qualsiasi tipo che occupa più spazio di un puntatore.
Peter Bloomfield,

1
"così, in effetti, molte delle obiezioni contro di essa che sono state accennate nella domanda originale" - perché non sono valide nella domanda originale, poiché si afferma che questi sono vincoli di C?
Cybergibbons,

@Cybergibbons Arduino si basa su C ++, quindi non mi è chiaro il motivo per cui solo i vincoli di C sarebbero pertinenti (a meno che il tuo codice per qualche motivo non sia compatibile anche con C).
microtherion

3
@ PeterR.Bloomfield, il mio punto sulle costanti che non richiedono memoria aggiuntiva era limitato const int. Per tipi più complessi, hai ragione a dire che è possibile allocare spazio di archiviazione, ma anche così, è improbabile che tu stia peggio che con a #define.
microtherion

7

EDIT: microtherion fornisce una risposta eccellente che corregge alcuni dei miei punti qui, in particolare sull'utilizzo della memoria.


Come hai identificato, ci sono alcune situazioni in cui sei costretto a usare a #define, perché il compilatore non consente una constvariabile. Allo stesso modo, in alcune situazioni sei costretto a usare variabili, come quando hai bisogno di una matrice di valori (cioè non puoi avere una matrice di #define).

Tuttavia, ci sono molte altre situazioni in cui non esiste necessariamente un'unica risposta "corretta". Ecco alcune linee guida che seguirei:

Sicurezza del tipo
Da un punto di vista generale della programmazione, le constvariabili sono generalmente preferibili (ove possibile). Il motivo principale è la sicurezza dei tipi.

Una #define(macro preprocessore) copia direttamente il valore letterale in ogni posizione nel codice, rendendo indipendente ogni utilizzo. Ciò può provocare ipoteticamente ambiguità, perché il tipo potrebbe finire per essere risolto in modo diverso a seconda di come / dove viene utilizzato.

Una constvariabile è sempre e solo un tipo, che è determinato dalla sua dichiarazione e risolto durante l'inizializzazione. Richiederà spesso un cast esplicito prima che si comporti in modo diverso (anche se ci sono varie situazioni in cui può essere tranquillamente promosso implicitamente il tipo). Per lo meno, il compilatore può (se configurato correttamente) emettere un avviso più affidabile quando si verifica un problema di tipo.

Una possibile soluzione per questo è includere un cast esplicito o un suffisso di tipo in a #define. Per esempio:

#define THE_ANSWER (int8_t)42
#define NOT_QUITE_PI 3.14f

Tale approccio può potenzialmente causare problemi di sintassi in alcuni casi, a seconda di come viene utilizzato.

Uso della memoria
A differenza dell'elaborazione per scopi generici, la memoria è ovviamente un vantaggio quando si tratta di qualcosa come un Arduino. L'uso di una constvariabile rispetto a a #definepuò influire sulla posizione in cui i dati sono archiviati, il che potrebbe costringerti a utilizzare l'uno o l'altro.

  • const le variabili verranno (di solito) archiviate in SRAM, insieme a tutte le altre variabili.
  • I valori letterali utilizzati #defineverranno spesso memorizzati nello spazio del programma (memoria Flash), accanto allo schizzo stesso.

(Notare che ci sono varie cose che possono influenzare esattamente come e dove viene archiviato qualcosa, come la configurazione e l'ottimizzazione del compilatore.)

SRAM e Flash hanno limitazioni diverse (ad es. 2 KB e 32 KB rispettivamente per Uno). Per alcune applicazioni, è abbastanza facile rimanere senza SRAM, quindi può essere utile spostare alcune cose in Flash. È anche possibile il contrario, sebbene probabilmente meno comune.

PROGMEM
È possibile ottenere i vantaggi della sicurezza dei tipi memorizzando anche i dati nello spazio del programma (Flash). Questo viene fatto usando la PROGMEMparola chiave. Non funziona per tutti i tipi, ma è comunemente usato per array di numeri interi o stringhe.

Il modulo generale riportato nella documentazione è il seguente:

dataType variableName[] PROGMEM = {dataInt0, dataInt1, dataInt3...}; 

Le tabelle di stringhe sono un po 'più complicate, ma la documentazione ha tutti i dettagli.


1

Per le variabili di un tipo specificato che non vengono modificate durante l'esecuzione, in genere possono essere utilizzate entrambe.

Per i numeri di pin digitali contenuti nelle variabili, entrambi possono funzionare, ad esempio:

const int ledPin = 13;

Ma c'è una circostanza in cui uso sempre #define

Serve a definire numeri pin analogici, poiché sono alfanumerici.

Certo, puoi codificare i numeri di pin come a2, a3ecc. In tutto il programma e il compilatore saprà cosa farne. Quindi, se si cambiano i pin, è necessario modificare ogni utilizzo.

Inoltre, mi piace sempre avere le mie definizioni dei pin in alto tutte in un unico posto, quindi la domanda diventa quale tipo di constsarebbe appropriato per un pin definito come A5.

In quei casi lo uso sempre #define

Esempio di divisore di tensione:

//
//  read12     Reads Voltage of 12V Battery
//
//        SDsolar      8/8/18
//
#define adcInput A5    // Voltage divider output comes in on Analog A5
float R1 = 120000.0;   // R1 for voltage divider input from external 0-15V
float R2 =  20000.0;   // R2 for voltage divider output to ADC
float vRef = 4.8;      // 9V on Vcc goes through the regulator
float vTmp, vIn;
int value;
.
.
void setup() {
.
// allow ADC to stabilize
value=analogRead(adcPin); delay(50); value=analogRead(adcPin); delay(50);
value=analogRead(adcPin); delay(50); value=analogRead(adcPin); delay(50);
value=analogRead(adcPin); delay(50); value=analogRead(adcPin);
.
void loop () {
.
.
  value=analogRead(adcPin);
  vTmp = value * ( vRef / 1024.0 );  
  vIn = vTmp / (R2/(R1+R2)); 
 .
 .

Tutte le variabili di configurazione sono in alto e non ci sarà mai una modifica al valore di adcPintranne al momento della compilazione.

Non preoccuparti di che tipo adcPinsia. E nessuna RAM aggiuntiva viene utilizzata nel file binario per memorizzare una costante.

Il compilatore sostituisce semplicemente ogni istanza di adcPincon la stringa A5prima della compilazione.


C'è un interessante thread del forum Arduino che discute altri modi per decidere:

#define vs. const variabile (forum Arduino)

Excertps:

Sostituzione del codice:

#define FOREVER for( ; ; )

FOREVER
 {
 if (serial.available() > 0)
   ...
 }

Codice di debug:

#ifdef DEBUG
 #define DEBUG_PRINT(x) Serial.println(x)
#else
 #define DEBUG_PRINT(x)
#endif

Definizione truee falsecome booleano per salvare la RAM

Instead of using `const bool true = 1;` and same for `false`

#define true (boolean)1
#define false (boolean)0

Molto dipende dalle preferenze personali, tuttavia è chiaro che #defineè più versatile.


Nelle stesse circostanze, a constnon utilizzerà più RAM di a #define. E per i pin analogici, li definirei come const uint8_t, sebbene const intnon farebbe alcuna differenza.
Edgar Bonet,

Hai scritto " a constnon utilizza effettivamente più RAM [...] fino a quando non viene effettivamente utilizzato ". Hai perso il mio punto: il più delle volte, a constnon usa la RAM, anche quando viene utilizzata . Quindi, " questo è un compilatore multipass ". Ancora più importante, è un compilatore ottimizzante . Quando possibile, le costanti vengono ottimizzate in operandi immediati .
Edgar Bonet,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.