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

programming c++ coding-standards

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

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

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

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

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

, 👍31

Обсуждение

хороший оптимизатор сделает это спорным, @ratchet freak

Действительно? Я не вижу, как компилятор собирается решать такие проблемы, как безопасность типов, невозможность использовать для определения длины массива и так далее., @Cybergibbons

Я согласен. Кроме того, если вы посмотрите на мой ответ ниже, я продемонстрирую, что бывают обстоятельства, когда вы действительно не знаете, какой тип использовать, поэтому " #define` - очевидный выбор. Мой пример заключается в именовании аналоговых контактов, таких как A5. Для него нет подходящего типа, который можно было бы использовать в качестве "const", поэтому единственный выбор-использовать "#define " и позволить компилятору заменить его в качестве ввода текста, прежде чем интерпретировать значение., @SDsolar


4 ответа


Лучший ответ:

24

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

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

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

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

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

,

Вы поднимаете несколько отличных моментов (особенно в отношении ограничений массива-я еще не знал, что стандартный компилятор с Arduino IDE поддерживает это). Не совсем правильно говорить, что константа времени компиляции не использует хранилище, хотя бы потому, что ее значение все равно должно присутствовать в коде (т. Е. в программной памяти, а не в SRAM) в любом месте, где она используется. Это означает, что это влияет на доступную прошивку для любого типа, который занимает больше места, чем указатель., @Peter Bloomfield

"таким образом, на самом деле несколько возражений против этого, на которые указывалось в исходном вопросе" - почему они недействительны в исходном вопросе, поскольку указано, что это ограничения C?, @Cybergibbons

@Cybergibbons Arduino основан на C++, поэтому мне непонятно, почему будут уместны ограничения только на C (если только ваш код по какой-либо причине также не должен быть совместим с C)., @microtherion

@PeterR.Bloomfield, моя точка зрения о константах, не требующих дополнительного хранилища, ограничивалась "const int". Для более сложных типов вы правы в том, что хранилище может быть выделено, но даже в этом случае вам вряд ли будет хуже, чем с " #define`., @microtherion


7

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

,

1

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

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

const int ledPin = 13;

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

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

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

Более того, мне всегда нравится, чтобы мои определения выводов были вверху в одном месте, поэтому возникает вопрос, какой тип константы подходит для вывода, определенного как 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. И никакая дополнительная оперативная память не используется в двоичном файле для хранения константы.

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


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

#определение переменной по сравнению с константой (форум Arduino)

Отрывки:

Подстановка кода:

#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 как логических для экономии оперативной памяти

Вместо использования "const bool true = 1;" и то же самое для "false"

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

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

,

В тех же обстоятельствах "const" не будет использовать больше оперативной памяти, чем "#define". А для аналоговых выводов я бы определил их как "const uint8_t", хотя const int не будет иметь никакого значения., @Edgar Bonet

Вы написали “_a const на самом деле не использует больше оперативной памяти [ ... ], пока она на самом деле не используется”. Вы упустили мою мысль: большую часть времени "const" не использует оперативную память, даже когда она используется. Затем: “ _ это многопроходный компилятор_". Самое главное, что это _оптимизирующий_ компилятор. Всякий раз, когда это возможно, константы оптимизируются в [непосредственные операнды](https://en.wikipedia.org/wiki/Addressing_mode#Immediate/literal)., @Edgar Bonet


0
  1. Вы не можете задать размеры массива с помощью const int.

    Вы можете с помощью constexpr int:

    constexpr auto size{ 15 };
    int array[size]{};
    
  2. Вы не можете использовать const int в качестве меток операторов case (хотя это работает в некоторых компиляторах).

    Опять же, вы можете с помощью constexpr int:

    constexpr auto zero{ 0 };
    constexpr auto one{ 1 };
    
    int number{ 1 };
    switch (number)
    {
    case zero: Serial.println("zero!"); break;
    case one:  Serial.println("one!"); break;
    default:   Serial.println("neither!"); break;
    }
    
  3. Вы не можете инициализировать константу с помощью другой константы.

    Вы даже можете инициализировать constexpr с помощью другого constexpr:

    constexpr int x{ 1 };
    const     int y{ x };
    constexpr int z{ x };
    

Кроме того, constexpr гарантирует, что переменная будет оценена во время компиляции, в отличие от const. constexpr также не имеет некоторых недостатков, присущих макросам препроцессора(learncpp):

  1. Отладчики, подобные Visual Studio, не позволяют просматривать макросы препроцессора.
  2. Макросы могут конфликтовать с обычным кодом.
  3. Макросы не имеют области действия, что увеличивает вероятность конфликтов имен.
,