¿Es mejor usar #define o const int para constantes?

26

Arduino es un híbrido extraño, donde se utiliza alguna funcionalidad de C ++ en el mundo integrado, tradicionalmente un entorno C. De hecho, una gran cantidad de código Arduino es muy similar a C.

C ha usado tradicionalmente #defines para constantes. Hay un número de razones para esto:

  1. No puede establecer tamaños de matriz usando const int.
  2. No puede usar const intcomo etiquetas de declaración de caso (aunque esto funciona en algunos compiladores)
  3. No se puede inicializar a constcon otro const.

Puede consultar esta pregunta en StackOverflow para obtener más razonamiento.

Entonces, ¿qué debemos usar para Arduino? Tiendo hacia #define, pero veo algo de código usando consty algunos usando una mezcla.

Cybergibbons
fuente
un buen optimizador hacerla discutible
trinquete monstruo
3
De Verdad? No veo cómo un compilador resolverá cosas como la seguridad de tipos, no poder usar para definir la longitud de la matriz, etc.
Cybergibbons
Estoy de acuerdo. Además, si miras mi respuesta a continuación, demuestro que hay circunstancias en las que realmente no sabes qué tipo usar, por lo que #definees la opción obvia. Mi ejemplo es nombrar pines analógicos, como A5. No hay un tipo apropiado para él que pueda usarse como un, constpor lo que la única opción es usar #defineay dejar que el compilador lo sustituya como entrada de texto antes de interpretar el significado.
SDsolar

Respuestas:

21

Es importante tener en cuenta que const intno se comporta de manera idéntica en C y en C ++, por lo que, de hecho, varias de las objeciones en su contra a las que se ha aludido en la pregunta original y en la amplia respuesta de Peter Bloomfields no son válidas:

  • En C ++, las const intconstantes son valores de tiempo de compilación y se pueden usar para establecer límites de matriz, como etiquetas de caso, etc.
  • const intlas constantes no necesariamente ocupan ningún almacenamiento. A menos que tome su dirección o la declare externa, generalmente tendrán una existencia en tiempo de compilación.

Sin embargo, para las constantes de enteros, a menudo puede ser preferible usar un (con nombre o anónimo) enum. A menudo me gusta esto porque:

  • Es compatible con versiones anteriores de C.
  • Es casi tan seguro como el tipo const int(cada bit como seguro en C ++ 11).
  • Proporciona una forma natural de agrupar constantes relacionadas.
  • Incluso puede usarlos para una cierta cantidad de control de espacio de nombres.

Entonces, en un programa idiomático de C ++, no hay ninguna razón para usar #definepara definir una constante entera. Incluso si desea seguir siendo compatible con C (debido a requisitos técnicos, porque está abandonando la vieja escuela o porque las personas con las que trabaja lo prefieren de esa manera), aún puede usar enumy debe hacerlo, en lugar de usar #define.

microtherion
fuente
2
Planteas algunos puntos excelentes (especialmente sobre los límites de la matriz; todavía no me había dado cuenta de que el compilador estándar con Arduino IDE lo soportaba). Sin embargo, no es del todo correcto decir que una constante de tiempo de compilación no utiliza almacenamiento, porque su valor todavía tiene que aparecer en el código (es decir, en la memoria del programa en lugar de en la SRAM) en cualquier lugar donde se use. Eso significa que impacta el Flash disponible para cualquier tipo que ocupe más espacio que un puntero.
Peter Bloomfield
1
"Entonces, de hecho, varias de las objeciones en su contra a las que se ha aludido en la pregunta original": ¿por qué no son válidas en la pregunta original, ya que se afirma que son restricciones de C?
Cybergibbons
@Cybergibbons Arduino se basa en C ++, por lo que no me queda claro por qué las restricciones de C solo serían pertinentes (a menos que su código por alguna razón también deba ser compatible con C).
microtherion
3
@ PeterR.Bloomfield, mi punto sobre las constantes que no requieren almacenamiento adicional se limitó a const int. Para los tipos más complejos, tiene razón en que se puede asignar almacenamiento, pero aun así, es poco probable que esté peor que con un #define.
microtherion
7

EDITAR: microtherion da una excelente respuesta que corrige algunos de mis puntos aquí, particularmente sobre el uso de memoria.


Como ha identificado, hay ciertas situaciones en las que se ve obligado a usar a #define, porque el compilador no permitirá una constvariable. Del mismo modo, en algunas situaciones se ve obligado a usar variables, como cuando necesita una matriz de valores (es decir, no puede tener una matriz de #define).

Sin embargo, hay muchas otras situaciones en las que no hay necesariamente una sola respuesta "correcta". Aquí hay algunas pautas que seguiría:

Tipo de seguridad
Desde un punto de vista de programación general, las constvariables son generalmente preferibles (cuando sea posible). La razón principal de esto es la seguridad de tipo.

Una #define(macro de preprocesador) copia directamente el valor literal en cada ubicación del código, haciendo que cada uso sea independiente. Hipotéticamente, esto puede dar lugar a ambigüedades, porque el tipo puede terminar resolviéndose de manera diferente dependiendo de cómo / dónde se use.

Una constvariable es solo un tipo, que se determina por su declaración y se resuelve durante la inicialización. A menudo requerirá una conversión explícita antes de que se comporte de manera diferente (aunque hay varias situaciones en las que puede promocionarse de forma implícita de forma segura). Como mínimo, el compilador puede (si está configurado correctamente) emitir una advertencia más confiable cuando ocurre un problema de tipo.

Una posible solución para esto es incluir una conversión explícita o un sufijo de tipo dentro de a #define. Por ejemplo:

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

Sin embargo, ese enfoque puede causar problemas de sintaxis en algunos casos, dependiendo de cómo se use.

Uso de la memoria
A diferencia de la informática de propósito general, la memoria es obviamente una prima cuando se trata de algo así como un Arduino. El uso de una constvariable vs. a #definepuede afectar el lugar donde se almacenan los datos en la memoria, lo que puede obligarlo a usar uno u otro.

  • const las variables (generalmente) se almacenarán en SRAM, junto con todas las demás variables.
  • Los valores literales utilizados a #definemenudo se almacenarán en el espacio del programa (memoria Flash), junto con el propio boceto.

(Tenga en cuenta que hay varias cosas que pueden afectar exactamente cómo y dónde se almacena algo, como la configuración y optimización del compilador).

SRAM y Flash tienen limitaciones diferentes (por ejemplo, 2 KB y 32 KB respectivamente para el Uno). Para algunas aplicaciones, es bastante fácil quedarse sin SRAM, por lo que puede ser útil cambiar algunas cosas a Flash. Lo contrario también es posible, aunque probablemente sea menos común.

PROGMEM
Es posible obtener los beneficios de la seguridad de escritura mientras se almacenan los datos en el espacio del programa (Flash). Esto se hace usando la PROGMEMpalabra clave. No funciona para todos los tipos, pero se usa comúnmente para matrices de enteros o cadenas.

La forma general que figura en la documentación es la siguiente:

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

Las tablas de cadenas son un poco más complicadas, pero la documentación tiene todos los detalles.

Peter Bloomfield
fuente
1

Para las variables de un tipo especificado que no se alteran durante la ejecución, generalmente se puede usar cualquiera.

Para los números de pin digitales contenidos en las variables, cualquiera puede funcionar, como:

const int ledPin = 13;

Pero hay una circunstancia donde siempre uso #define

Es para definir números de pin analógicos, ya que son alfanuméricos.

Claro, puede codificar los números de PIN como a2, a3etc., en todo el programa y el compilador sabrá qué hacer con ellos. Luego, si cambia los pines, entonces cada uso necesitaría ser cambiado.

Además, siempre me gusta tener mis definiciones de pin en la parte superior, todo en un solo lugar, por lo que la pregunta se convierte en qué tipo de constpin sería apropiado para un pin definido como A5.

En esos casos siempre uso #define

Divisor de voltaje Ejemplo:

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

Todas las variables de configuración están en la parte superior y nunca habrá un cambio en el valor, adcPinexcepto en el momento de la compilación.

No te preocupes por qué tipo adcPines. Y no se usa RAM adicional en el binario para almacenar una constante.

El compilador simplemente reemplaza cada instancia de adcPincon la cadena A5antes de compilar.


Hay un interesante hilo del Foro Arduino que analiza otras formas de decidir:

#define vs.const variable (foro Arduino)

Excertps:

Sustitución de código:

#define FOREVER for( ; ; )

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

Código de depuración:

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

Definiendo truey falsecomo booleano para ahorrar RAM

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

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

Mucho de esto se reduce a preferencias personales, sin embargo, está claro que #definees más versátil.

SDsolar
fuente
En las mismas circunstancias, a constno usará más RAM que a #define. Y para los pines analógicos, los definiría como const uint8_t, aunque const intno haría ninguna diferencia.
Edgar Bonet
Usted escribió " a en constrealidad no usa más RAM [...] hasta que realmente se usa ". Te perdiste mi punto: la mayoría de las veces, a constno usa RAM, incluso cuando se usa . Entonces, " este es un compilador multipass ". Lo más importante, es un compilador optimizador . Siempre que sea posible, las constantes se optimizan en operandos inmediatos .
Edgar Bonet