É melhor usar #define ou const int para constantes?

26

O Arduino é um híbrido ímpar, onde algumas funcionalidades do C ++ são usadas no mundo incorporado - tradicionalmente um ambiente C. De fato, muitos códigos do Arduino são muito parecidos com o C.

C tradicionalmente usa #defines para constantes. Há várias razões para isso:

  1. Você não pode definir tamanhos de matriz usando const int.
  2. Você não pode usar const intcomo rótulos de declaração de caso (embora isso funcione em alguns compiladores)
  3. Você não pode inicializar um constcom outro const.

Você pode verificar esta pergunta no StackOverflow para obter mais argumentos.

Então, o que devemos usar para o Arduino? Eu costumo #define, mas vejo algum código usando conste outros usando uma mistura.

Cybergibbons
fonte
um bom otimizador irá torná-lo discutível
catraca aberração
3
Sério? Não vejo como um compilador resolverá coisas como segurança de tipo, não podendo usar o para definir o comprimento da matriz e assim por diante.
Cybergibbons
Concordo. Além disso, se você olhar para a minha resposta abaixo, demonstrarei que há circunstâncias em que você realmente não sabe qual tipo usar, assim #definecomo a escolha óbvia. Meu exemplo é nomear pinos analógicos - como A5. Não existe um tipo apropriado para ele que possa ser usado como uma opção; constportanto, a única opção é usar #defineae deixar que o compilador a substitua como entrada de texto antes de interpretar o significado.
SDsolar 26/03

Respostas:

21

É importante observar que const intnão se comporta de maneira idêntica em C e em C ++; portanto, várias das objeções contra ele mencionadas na pergunta original e na extensa resposta de Peter Bloomfields não são válidas:

  • No C ++, const intconstantes são valores de tempo de compilação e podem ser usadas para definir limites de matriz, como rótulos de maiúsculas e minúsculas, etc.
  • const intconstantes não ocupam necessariamente nenhum armazenamento. A menos que você escolha o endereço ou o declare externo, eles geralmente terão uma existência de tempo de compilação.

No entanto, para constantes inteiras, pode ser preferível usar a (nomeado ou anônimo) enum. Costumo gostar disso porque:

  • É compatível com o C.
  • É quase tão seguro quanto o tipo const int(tão seguro quanto o tipo no C ++ 11).
  • Ele fornece uma maneira natural de agrupar constantes relacionadas.
  • Você pode até usá-los para uma certa quantidade de controle de namespace.

Portanto, em um programa C ++ idiomático, não há razão para usar #definepara definir uma constante inteira. Mesmo se você quiser permanecer compatível com C (por causa de requisitos técnicos, porque você está começando na velha escola ou porque as pessoas com quem você trabalha preferem assim), você ainda pode usar enume deve fazê-lo, em vez de usar #define.

microtherion
fonte
2
Você levantou alguns pontos excelentes (especialmente sobre os limites da matriz - eu ainda não havia percebido o compilador padrão com o Arduino IDE). Não é correto dizer que uma constante em tempo de compilação não usa armazenamento, porque seu valor ainda precisa ocorrer no código (ou seja, na memória do programa em vez da SRAM) em qualquer lugar que seja usado. Isso significa que afeta o Flash disponível para qualquer tipo que ocupa mais espaço do que um ponteiro.
Peter Bloomfield
11
"portanto, de fato, várias das objeções contra ele mencionadas na pergunta original" - por que elas não são válidas na pergunta original, pois afirma-se que essas são restrições de C?
Cybergibbons
@ Cyybibbons O Arduino é baseado em C ++, então não está claro para mim por que as restrições apenas em C seriam pertinentes (a menos que seu código, por algum motivo, também precise ser compatível com C).
microtherion
3
@ PeterR.Bloomfield, meu argumento sobre constantes que não exigem armazenamento extra foi limitado const int. Para tipos mais complexos, você está certo de que o armazenamento pode ser alocado, mas, mesmo assim, é improvável que esteja pior do que com um #define.
microtherion
7

EDIT: microtherion fornece uma excelente resposta que corrige alguns dos meus pontos aqui, particularmente sobre o uso de memória.


Como você identificou, há certas situações em que você é forçado a usar a #define, porque o compilador não permite uma constvariável. Da mesma forma, em algumas situações, você é forçado a usar variáveis, como quando precisa de uma matriz de valores (ou seja, não pode ter uma matriz de #define).

No entanto, existem muitas outras situações em que não há necessariamente uma única resposta 'correta'. Aqui estão algumas diretrizes que eu seguiria:

Segurança do tipo
Do ponto de vista geral da programação, as constvariáveis ​​são geralmente preferíveis (sempre que possível). A principal razão para isso é a segurança do tipo.

Uma #define(macro de pré-processador) copia diretamente o valor literal em cada local no código, tornando cada uso independente. Hipoteticamente, isso pode resultar em ambiguidades, porque o tipo pode acabar sendo resolvido de maneira diferente, dependendo de como / onde é usado.

Uma constvariável é apenas um tipo, que é determinado por sua declaração e resolvido durante a inicialização. Geralmente, exige uma conversão explícita antes de se comportar de maneira diferente (embora existam várias situações nas quais ele pode ser implicitamente promovido com segurança). No mínimo, o compilador pode (se configurado corretamente) emitir um aviso mais confiável quando ocorrer um problema de tipo.

Uma solução possível para isso é incluir uma conversão explícita ou um sufixo de tipo em a #define. Por exemplo:

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

Essa abordagem pode potencialmente causar problemas de sintaxe em alguns casos, dependendo de como é usada.

Uso da memória
Ao contrário da computação de uso geral, a memória é obviamente superior quando se lida com algo como um Arduino. O uso de uma constvariável vs. a #definepode afetar o local em que os dados são armazenados na memória, o que pode forçá-lo a usar um ou outro.

  • const variáveis ​​(geralmente) serão armazenadas na SRAM, juntamente com todas as outras variáveis.
  • Os valores literais usados #definegeralmente serão armazenados no espaço do programa (memória Flash), ao lado do próprio esboço.

(Observe que existem várias coisas que podem afetar exatamente como e onde algo é armazenado, como configuração e otimização do compilador.)

SRAM e Flash têm limitações diferentes (por exemplo, 2 KB e 32 KB, respectivamente, para o Uno). Para alguns aplicativos, é muito fácil ficar sem SRAM, portanto, pode ser útil mudar algumas coisas para o Flash. O inverso também é possível, embora provavelmente menos comum.

PROGMEM
É possível obter os benefícios da segurança de tipo e também armazenar os dados no espaço do programa (Flash). Isso é feito usando a PROGMEMpalavra - chave Não funciona para todos os tipos, mas é comumente usado para matrizes de números inteiros ou seqüências de caracteres.

A forma geral fornecida na documentação é a seguinte:

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

As tabelas de strings são um pouco mais complicadas, mas a documentação possui detalhes completos.

Peter Bloomfield
fonte
1

Para variáveis ​​de um tipo especificado que não são alteradas durante a execução, geralmente podem ser usadas.

Para números de pinos digitais contidos em variáveis, ambos podem funcionar - como:

const int ledPin = 13;

Mas há uma circunstância em que eu sempre uso #define

É para definir números de pinos analógicos, pois eles são alfanuméricos.

Claro, você pode codificar os números dos pinos como a2, a3etc. durante todo o programa e o compilador saberá o que fazer com eles. Então, se você alterar os pinos, cada uso precisará ser alterado.

Além disso, eu sempre gosto de ter minhas definições de pinos no topo, tudo em um só lugar, de modo que a pergunta se torna sobre que tipo de constseria apropriado para um pino definido como A5.

Nesses casos, eu sempre uso #define

Exemplo de divisor de tensão:

//
//  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 as variáveis ​​de configuração estão no topo e nunca haverá uma alteração no valor de adcPinexceto em tempo de compilação.

Não se preocupe com o tipo adcPin. E nenhuma RAM extra é usada no binário para armazenar uma constante.

O compilador simplesmente substitui cada instância de adcPinpela string A5antes de compilar.


Há um tópico interessante no Fórum do Arduino que discute outras maneiras de decidir:

#define vs. const variable (fórum do Arduino)

Excertps:

Substituição de código:

#define FOREVER for( ; ; )

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

Código de depuração:

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

Definindo truee falsecomo Booleano para economizar RAM

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

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

Muito disso se resume à preferência pessoal, porém é claro que #defineé mais versátil.

SDsolar
fonte
Nas mesmas circunstâncias, a constnão utilizará mais RAM que a #define. E para os pinos analógicos, eu os definiria como const uint8_t, embora const intnão faça diferença.
Edgar Bonet 26/03
Você escreveu " a constrealmente não usa mais RAM [...] até que seja realmente usado ". Você não entendeu: na maioria das vezes, a constnão usa RAM, mesmo quando usada . Então, “ este é um compilador multipass ”. Mais importante ainda, é um compilador de otimização . Sempre que possível, as constantes são otimizadas em operandos imediatos .
Edgar Bonet