Лучше использовать #define или const int для констант?

26

Arduino - это странный гибрид, в котором во встроенном мире используется некоторая функциональность C ++ - традиционно среда C. Действительно, большая часть кода Arduino очень похожа на Си.

C традиционно использует #defines для констант. Есть ряд причин для этого:

  1. Вы не можете установить размеры массива, используя const int.
  2. Вы не можете использовать в const intкачестве меток оператора case (хотя это работает в некоторых компиляторах)
  3. Вы не можете инициализировать constс другим const.

Вы можете проверить этот вопрос на StackOverflow для большего количества рассуждений.

Итак, что мы должны использовать для Arduino? Я склоняюсь к этому #define, но вижу, что некоторые используют код, constа некоторые используют смесь.

Cybergibbons
источник
хороший оптимизатор сделает это спорным
ratchet freak
3
В самом деле? Я не понимаю, как компилятор будет разрешать такие вещи, как безопасность типов, не имея возможности использовать для определения длины массива и так далее.
Cybergibbons
Я согласен. Кроме того, если вы посмотрите на мой ответ ниже, я продемонстрирую, что существуют обстоятельства, когда вы не знаете, какой тип использовать, поэтому #defineочевидный выбор. Мой пример в названии аналоговых контактов - как A5. Для него нет подходящего типа, который можно было бы использовать как constединственный, поэтому единственный вариант - использовать a #defineи позволить компилятору подставить его в качестве ввода текста перед интерпретацией значения.
SDsolar

Ответы:

21

Важно отметить, что в C и C ++ const intони не ведут себя одинаково, поэтому на самом деле некоторые из возражений против этого, которые были упомянуты в первоначальном вопросе и в обширном ответе Питера Блумфилдса, недопустимы:

  • В C ++ const intконстанты являются значениями времени компиляции и могут использоваться для установки ограничений массива, таких как метки регистра и т. Д.
  • const intконстанты не обязательно занимают какое-либо хранилище. Если вы не берете их адрес или не объявляете их внешними, они обычно будут существовать во время компиляции.

Однако для целочисленных констант часто может быть предпочтительнее использовать (именованный или анонимный) enum. Мне часто это нравится, потому что:

  • Это обратно совместимо с C.
  • Это почти так же безопасно, как и тип const int(точно так же, как безопасность типов в C ++ 11).
  • Это обеспечивает естественный способ группировки связанных констант.
  • Вы даже можете использовать их для некоторого контроля пространства имен.

Таким образом, в идиоматической программе на C ++ нет никаких оснований для #defineопределения целочисленной константы. Даже если вы хотите оставаться совместимым с C (из-за технических требований, потому что вы заблуждаетесь в старой школе или потому, что люди, с которыми вы работаете, предпочитают это), вы все равно можете использовать enumи должны это делать, а не использовать #define.

microtherion
источник
2
Вы подняли несколько замечательных моментов (особенно в отношении ограничений массива - я еще не понял, что стандартный компилятор с Arduino IDE поддерживает это). Не совсем правильно говорить, что константа времени компиляции не использует хранилище, потому что ее значение все равно должно присутствовать в коде (то есть в памяти программы, а не в SRAM) везде, где она используется. Это означает, что это влияет на доступную Flash для любого типа, который занимает больше места, чем указатель.
Питер Блумфилд
1
«так на самом деле некоторые из возражений против него были упомянуты в первоначальном вопросе» - почему они не действительны в первоначальном вопросе, поскольку утверждается, что это ограничения C?
Cybergibbons
@Cybergibbons Arduino основан на C ++, поэтому мне не ясно, почему уместны только ограничения на C (если ваш код по какой-то причине не должен быть также совместим с C).
микротерион
3
@ PeterR.Bloomfield, моя точка зрения о константах, не требующих дополнительного хранения, была ограничена const int. Для более сложных типов вы правы в том, что хранилище может быть выделено, но даже в этом случае вам вряд ли будет хуже, чем с #define.
микротерион
7

РЕДАКТИРОВАТЬ: microtherion дает превосходный ответ, который исправляет некоторые из моих пунктов здесь, особенно об использовании памяти.


Как вы определили, в определенных ситуациях вы вынуждены использовать a #define, потому что компилятор не допустит constпеременную. Точно так же в некоторых ситуациях вы вынуждены использовать переменные, например, когда вам нужен массив значений (т.е. вы не можете иметь массив #define).

Тем не менее, существует много других ситуаций, в которых не существует единственного «правильного» ответа. Вот несколько рекомендаций, которым я бы следовал:

Безопасность типов
С общей точки зрения программирования constпеременные обычно предпочтительнее (где это возможно). Основной причиной этого является безопасность типов.

#define(Препроцессор макро) непосредственно копирует буквенное значение в каждое место в коде, что делает каждый независимо использование. Это может гипотетически привести к неоднозначности, потому что тип может быть решен по-разному в зависимости от того, как / где он используется.

constПеременная только когда - либо один тип, который определяется его декларации, и решены в ходе инициализации. Он часто требует явного приведения, прежде чем он будет вести себя по-другому (хотя существуют различные ситуации, когда его можно безопасно неявно повышать по типу). По крайней мере, компилятор может (если он настроен правильно) выдавать более надежное предупреждение при возникновении проблемы с типом.

Возможным обходным путем для этого является включение явного приведения или суффикса типа в #define. Например:

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

Такой подход может потенциально вызвать проблемы синтаксиса в некоторых случаях, в зависимости от того, как он используется.

Использование памяти
В отличие от вычислений общего назначения, память имеет преимущество при работе с чем-то вроде Arduino. Использование constпеременной против a #defineможет повлиять на то, где данные хранятся в памяти, что может заставить вас использовать одну или другую.

  • const переменные (как правило) будут храниться в SRAM вместе со всеми другими переменными.
  • Используемые литеральные значения #defineчасто хранятся в программном пространстве (флэш-память) вместе с самим эскизом.

(Обратите внимание, что есть разные вещи, которые могут повлиять на то, как и где что-то хранится, например, конфигурация компилятора и оптимизация.)

SRAM и Flash имеют разные ограничения (например, 2 КБ и 32 КБ соответственно для Uno). Для некоторых приложений достаточно просто запустить SRAM, поэтому полезно перенести некоторые вещи во Flash. Обратное также возможно, хотя, вероятно, менее распространено.

PROGMEM
Можно получить преимущества безопасности типов и одновременно хранить данные в программном пространстве (Flash). Это делается с помощью PROGMEMключевого слова. Он не работает для всех типов, но обычно используется для массивов целых чисел или строк.

Общая форма, приведенная в документации, выглядит следующим образом:

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

Строковые таблицы немного сложнее, но в документации есть полная информация.

Питер Блумфилд
источник
1

Для переменных определенного типа, которые не изменяются во время выполнения, обычно можно использовать любой из них.

Для цифровых номеров выводов, содержащихся в переменных, может работать любой из них, например:

const int ledPin = 13;

Но есть одно обстоятельство, когда я всегда использую #define

Он предназначен для определения аналоговых номеров выводов, поскольку они являются буквенно-цифровыми.

Конечно, вы можете жестко закодировать номера выводов как a2, a3и т. Д. Во всей программе, и компилятор будет знать, что с ними делать. Затем, если вы поменяете булавки, то каждое использование должно быть изменено.

Более того, я всегда хотел бы, чтобы мои определения выводов были наверху, все в одном месте, поэтому возникает вопрос, какой тип constбудет подходить для вывода, определенного как A5.

В этих случаях я всегда использую #define

Пример делителя напряжения:

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

Все установочные переменные находятся в самом верху, и никогда не будет изменений в значении, adcPinкроме как во время компиляции.

Не беспокойтесь о том, что тип adcPin. И никакая дополнительная RAM не используется в двоичном файле для хранения константы.

Компилятор просто заменяет каждый экземпляр adcPinна строку A5перед компиляцией.


Существует интересная ветка форума Arduino, в которой обсуждаются другие способы решения:

#define vs. const переменная (форум Arduino)

Excertps:

Замена кода:

#define FOREVER for( ; ; )

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

Код отладки:

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

Определение trueи falseкак логическое значение для экономии оперативной памяти

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

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

Многое из этого сводится к личным предпочтениям, однако ясно, что #defineоно более универсально.

SDsolar
источник
В тех же условиях a constне будет использовать больше оперативной памяти, чем a #define. А для аналоговых выводов я бы определил их как const uint8_t, хотя const intбез разницы.
Эдгар Бонет
Вы написали « a на constсамом деле не использует больше оперативной памяти [...] до тех пор, пока она не будет фактически использована ». Вы упустили мою мысль: большую часть времени a constне использует RAM, даже когда она используется . Затем « это многопроходный компилятор ». Самое главное, это оптимизирующий компилятор. По возможности константы оптимизируются в непосредственные операнды .
Эдгар Бонет