Est-il préférable d'utiliser #define ou const int pour les constantes?

26

Arduino est un hybride étrange, où certaines fonctionnalités C ++ sont utilisées dans le monde embarqué, traditionnellement un environnement C. En effet, beaucoup de code Arduino ressemble beaucoup au C.

C a traditionnellement utilisé #defines pour les constantes. Il ya un certain nombre de raisons à cela:

  1. Vous ne pouvez pas définir la taille des tableaux à l'aide de const int.
  2. Vous ne pouvez pas utiliser const intcomme étiquettes d'instructions de cas (bien que cela fonctionne dans certains compilateurs)
  3. Vous ne pouvez pas initialiser un constavec un autre const.

Vous pouvez vérifier cette question sur StackOverflow pour plus de raisonnement.

Alors, que devrions-nous utiliser pour Arduino? Je tends vers #define, mais je vois un peu de code en utilisant constet certains en utilisant un mélange.

Cybergibbons
la source
un bon optimiseur le rendra théorique
ratchet freak
3
Vraiment? Je ne vois pas comment un compilateur va résoudre des choses comme la sécurité des types, ne pas pouvoir utiliser pour définir la longueur du tableau, etc.
Cybergibbons
Je suis d'accord. De plus, si vous regardez ma réponse ci-dessous, je démontre qu'il y a des circonstances où vous ne savez pas vraiment quel type utiliser, c'est donc #definele choix évident. Mon exemple est de nommer des broches analogiques - comme A5. Il n'y a pas de type approprié pour cela qui pourrait être utilisé comme un constdonc le seul choix est d'utiliser un #defineet de laisser le compilateur le remplacer comme entrée de texte avant d'interpréter la signification.
SDsolar

Réponses:

21

Il est important de noter que const intcela ne se comporte pas de la même manière en C et en C ++, donc en fait plusieurs des objections contre cela qui ont été évoquées dans la question d'origine et dans la réponse détaillée de Peter Bloomfields ne sont pas valides:

  • En C ++, les const intconstantes sont des valeurs de temps de compilation et peuvent être utilisées pour définir des limites de tableau, comme des étiquettes de cas, etc.
  • const intles constantes n'occupent pas nécessairement de stockage. À moins que vous ne preniez leur adresse ou que vous ne les déclariez externes, ils auront généralement une existence au moment de la compilation.

Cependant, pour les constantes entières, il peut souvent être préférable d'utiliser un (nommé ou anonyme) enum. J'aime souvent ça parce que:

  • Il est rétrocompatible avec C.
  • Il est presque aussi sûr que le type const int(chaque bit est aussi sûr que le type en C ++ 11).
  • Il fournit un moyen naturel de regrouper les constantes liées.
  • Vous pouvez même les utiliser pour une certaine quantité de contrôle d'espace de noms.

Donc, dans un programme C ++ idiomatique, il n'y a aucune raison à utiliser #definepour définir une constante entière. Même si vous voulez rester compatible C (en raison d'exigences techniques, parce que vous vous démarquez à l'ancienne ou parce que les gens avec lesquels vous travaillez le préfèrent de cette façon), vous pouvez toujours utiliser enumet devez le faire, plutôt que d'utiliser #define.

microtherion
la source
2
Vous soulevez quelques excellents points (en particulier concernant les limites du tableau - je n'avais pas encore réalisé que le compilateur standard avec Arduino IDE le supportait). Il n'est pas tout à fait correct de dire qu'une constante de compilation n'utilise pas de stockage, car sa valeur doit encore apparaître dans le code (c'est-à-dire la mémoire de programme plutôt que SRAM) partout où elle est utilisée. Cela signifie qu'il affecte Flash disponible pour tout type qui prend plus de place qu'un pointeur.
Peter Bloomfield
1
"donc en fait plusieurs des objections à son encontre qui ont été évoquées dans la question d'origine" - pourquoi ne sont-elles pas valables dans la question d'origine, car il est dit que ce sont des contraintes de C?
Cybergibbons
@Cybergibbons Arduino est basé sur C ++, il n'est donc pas clair pour moi pourquoi seules les contraintes C seraient pertinentes (sauf si votre code doit être compatible avec C également).
microtherion
3
@ PeterR.Bloomfield, mon point sur les constantes ne nécessitant pas de stockage supplémentaire a été limité à const int. Pour les types plus complexes, vous avez raison de penser que le stockage peut être alloué, mais même ainsi, il est peu probable que votre situation soit pire qu'avec un #define.
microtherion
7

EDIT: microtherion donne une excellente réponse qui corrige certains de mes points ici, en particulier sur l'utilisation de la mémoire.


Comme vous l'avez identifié, il existe certaines situations où vous êtes obligé d'utiliser un #define, car le compilateur n'autorise pas une constvariable. De même, dans certaines situations, vous êtes obligé d'utiliser des variables, comme lorsque vous avez besoin d'un tableau de valeurs (c'est-à-dire que vous ne pouvez pas en avoir un #define).

Cependant, il existe de nombreuses autres situations où il n'y a pas nécessairement une seule réponse «correcte». Voici quelques directives que je suivrais:

Sécurité des types
Du point de vue de la programmation générale, les constvariables sont généralement préférables (si possible). La principale raison en est la sécurité de type.

Une #define(macro préprocesseur) copie directement la valeur littérale dans chaque emplacement du code, ce qui rend chaque utilisation indépendante. Cela peut hypothétiquement entraîner des ambiguïtés, car le type peut finir par être résolu différemment selon la façon dont il est utilisé.

Une constvariable n'est qu'un seul type, déterminé par sa déclaration et résolu lors de l'initialisation. Il nécessitera souvent une conversion explicite avant de se comporter différemment (bien qu'il existe diverses situations où il peut être implicitement promu par type en toute sécurité). À tout le moins, le compilateur peut (s'il est configuré correctement) émettre un avertissement plus fiable lorsqu'un problème de type se produit.

Une solution de contournement possible consiste à inclure une distribution explicite ou un suffixe de type dans a #define. Par exemple:

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

Cette approche peut cependant causer des problèmes de syntaxe dans certains cas, selon la façon dont elle est utilisée.

Utilisation de la mémoire
Contrairement à l'informatique à usage général, la mémoire est évidemment à un prix élevé lorsqu'il s'agit de quelque chose comme un Arduino. L'utilisation d'une constvariable par rapport à a #definepeut affecter l'emplacement de stockage des données en mémoire, ce qui peut vous obliger à utiliser l'une ou l'autre.

  • const les variables seront (généralement) stockées dans SRAM, avec toutes les autres variables.
  • Les valeurs littérales utilisées dans #defineseront souvent stockées dans l'espace programme (mémoire Flash), à côté de l'esquisse elle-même.

(Notez que diverses choses peuvent affecter exactement comment et où quelque chose est stocké, comme la configuration et l'optimisation du compilateur.)

SRAM et Flash ont des limitations différentes (par exemple 2 Ko et 32 ​​Ko respectivement pour l'Uno). Pour certaines applications, il est assez facile de manquer de SRAM, il peut donc être utile de déplacer certaines choses dans Flash. L'inverse est également possible, bien que probablement moins courant.

PROGMEM
Il est possible de bénéficier des avantages de la sécurité de type tout en stockant les données dans l'espace programme (Flash). Cela se fait à l'aide du PROGMEMmot - clé. Cela ne fonctionne pas pour tous les types, mais il est couramment utilisé pour les tableaux d'entiers ou de chaînes.

La forme générale donnée dans la documentation est la suivante:

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

Les tables de chaînes sont un peu plus compliquées, mais la documentation contient tous les détails.

Peter Bloomfield
la source
1

Pour les variables d'un type spécifié qui ne sont pas modifiées pendant l'exécution, l'une ou l'autre peut généralement être utilisée.

Pour les numéros de broches numériques contenus dans des variables, l'un ou l'autre peut fonctionner - tels que:

const int ledPin = 13;

Mais il y a une circonstance où j'utilise toujours #define

Il s'agit de définir des numéros de broches analogiques, car ils sont alphanumériques.

Bien sûr, vous pouvez coder en dur les numéros de broches que a2, a3etc. tout au long du programme et le compilateur saura quoi faire avec eux. Ensuite, si vous changez d'épingles, chaque utilisation devra être modifiée.

De plus, j'aime toujours avoir mes définitions de broches en haut en un seul endroit, donc la question devient quel type de constserait approprié pour une broche définie comme A5.

Dans ces cas, j'utilise toujours #define

Exemple de diviseur de tension:

//
//  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)); 
 .
 .

Toutes les variables de configuration sont tout en haut et il n'y aura jamais de modification de la valeur de adcPinsauf au moment de la compilation.

Pas de soucis sur le type adcPin. Et aucune RAM supplémentaire n'est utilisée dans le binaire pour stocker une constante.

Le compilateur remplace simplement chaque instance de adcPinpar la chaîne A5avant la compilation.


Il existe un fil de discussion intéressant sur le forum Arduino qui discute d'autres façons de décider:

#define vs const variable (forum Arduino)

Excertps:

Substitution de code:

#define FOREVER for( ; ; )

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

Code de débogage:

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

Définir trueet falsecomme booléen pour économiser de la RAM

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

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

Beaucoup se résume à des préférences personnelles, mais il est clair que #definec'est plus polyvalent.

SDsolar
la source
Dans les mêmes circonstances, a constn'utilisera pas plus de RAM que a #define. Et pour les broches analogiques, je les définirais comme const uint8_t, bien que const intcela ne fasse aucune différence.
Edgar Bonet
Vous avez écrit « a constn'utilise pas réellement plus de RAM [...] jusqu'à ce qu'il soit réellement utilisé ». Vous avez manqué mon point: la plupart du temps, un constn'utilise pas de RAM, même lorsqu'il est utilisé . Ensuite, « ceci est un compilateur multipass ». Plus important encore, il s'agit d'un compilateur d' optimisation . Dans la mesure du possible, les constantes sont optimisées en opérandes immédiats .
Edgar Bonet