Czy lepiej jest używać #define lub const int dla stałych?

26

Arduino to dziwna hybryda, w której niektóre funkcje C ++ są używane w świecie osadzonym - tradycyjnie środowisko C. Rzeczywiście, wiele kodu Arduino jest bardzo podobna do C.

C tradycyjnie używa #defines dla stałych. Istnieje wiele powodów:

  1. Nie można ustawić rozmiarów tablic za pomocą const int.
  2. Nie można używać const intjako etykiet instrukcji case (choć działa to w niektórych kompilatorach)
  3. Nie możesz zainicjować constinnego const.

Możesz sprawdzić to pytanie na StackOverflow, aby uzyskać więcej uzasadnień.

Czego więc powinniśmy użyć dla Arduino? Skłaniam się w kierunku #define, ale widzę trochę kodu używającego, consta niektóre używającego mieszanki.

Cybergibbons
źródło
dobry optymalizator sprawi, że będzie to dyskusja
maniak zapadkowy
3
Naprawdę? Nie widzę, jak kompilator ma zamiar rozwiązać takie kwestie, jak bezpieczeństwo typu, ponieważ nie jest w stanie określić długości tablicy i tak dalej.
Cybergibbons
Zgadzam się. Ponadto, jeśli spojrzysz na moją odpowiedź poniżej, pokazuję, że istnieją okoliczności, w których tak naprawdę nie wiesz, jakiego rodzaju użyć, więc #definejest to oczywisty wybór. Mój przykład dotyczy nazwania pinów analogowych - takich jak A5. Nie ma odpowiedniego typu, który mógłby być użyty jako consttak, więc jedynym wyborem jest użycie a #definei pozwolić kompilatorowi zastąpić go jako tekst wejściowy przed interpretacją znaczenia.
SDsolar

Odpowiedzi:

21

Ważne jest, aby pamiętać, że const intnie nie zachowują się identycznie w C i C ++, więc w rzeczywistości kilka zarzutów przeciwko niemu, które zostały nawiązywał w pierwotnego pytania i odpowiedzi w obszernej Petera Bloomfields nie są ważne:

  • W C ++ const intstałe są wartościami czasu kompilacji i mogą być używane do ustawiania limitów tablic, jak etykiety liter itp.
  • const intstałe niekoniecznie zajmują miejsce w pamięci. O ile nie weźmiesz ich adresu lub nie ogłosisz ich zewnętrznymi, na ogół będą one po prostu istnieć w czasie kompilacji.

Jednak w przypadku stałych całkowitych często preferowane może być użycie (nazwanego lub anonimowego) enum. Często to lubię, ponieważ:

  • Jest wstecznie kompatybilny z C.
  • Jest prawie tak samo bezpieczny jak typ const int(co najmniej tak samo bezpieczny dla typu w C ++ 11).
  • Zapewnia naturalny sposób grupowania stałych powiązanych.
  • Możesz nawet użyć ich do kontroli przestrzeni nazw.

Tak więc w idiomatycznym programie C ++ nie ma żadnego powodu, aby używać go #definedo definiowania stałej całkowitej. Nawet jeśli chcesz pozostać kompatybilny z C (z powodu wymagań technicznych, ponieważ kopiesz go w starej szkole lub ponieważ ludzie, z którymi pracujesz, wolą w ten sposób), możesz nadal używać enumi powinieneś to robić, zamiast używać #define.

mikrotherion
źródło
2
Podnosisz kilka doskonałych punktów (szczególnie o limitach macierzy - jeszcze nie zdawałem sobie sprawy, że standardowy kompilator z obsługą Arduino IDE to jeszcze obsługuje). Nie jest jednak poprawne stwierdzenie, że stała czasu kompilacji nie używa pamięci, ponieważ jej wartość wciąż musi występować w kodzie (tj. Pamięci programu, a nie SRAM) w dowolnym miejscu, w którym jest używana. Oznacza to, że wpływa na dostępną pamięć Flash dowolnego typu, który zajmuje więcej miejsca niż wskaźnik.
Peter Bloomfield
1
„a więc kilka zarzutów przeciwko niemu, na które powołano się w pierwotnym pytaniu” - dlaczego nie są one ważne w pierwotnym pytaniu, ponieważ stwierdzono, że są to ograniczenia C?
Cybergibbons
@Cybergibbons Arduino opiera się na C ++, więc nie jest dla mnie jasne, dlaczego tylko ograniczenia C byłyby stosowne (chyba że kod z jakiegoś powodu musi być również zgodny z C).
microtherion
3
@ PeterR.Bloomfield, mój punkt widzenia na temat stałych niewymagających dodatkowego miejsca był ograniczony const int. W przypadku bardziej złożonych typów masz rację, że przestrzeń dyskowa może być przydzielona, ​​ale mimo to raczej nie znajdziesz się w gorszej sytuacji niż w przypadku #define.
microtherion
7

EDYCJA: mikrotherion daje doskonałą odpowiedź, która koryguje niektóre moje uwagi tutaj, szczególnie dotyczące zużycia pamięci.


Jak już zidentyfikowałeś, są pewne sytuacje, w których musisz użyć a #define, ponieważ kompilator nie zezwala na constzmienną. Podobnie, w niektórych sytuacjach jesteś zmuszony używać zmiennych, na przykład gdy potrzebujesz tablicy wartości (tzn. Nie możesz mieć tablicy #define).

Istnieje jednak wiele innych sytuacji, w których niekoniecznie istnieje jedna „poprawna” odpowiedź. Oto kilka wskazówek, których chciałbym przestrzegać:

Bezpieczeństwo typu
Z ogólnego punktu widzenia programowania constzmienne są zwykle preferowane (tam, gdzie to możliwe). Głównym tego powodem jest bezpieczeństwo typu.

#define(Preprocesor makro) bezpośrednio kopie wartości dosłownym do każdej lokalizacji w kodzie, dzięki czemu każdy niezależnie użytkowania. Może to hipotetycznie prowadzić do niejednoznaczności, ponieważ typ może zostać rozwiązany w różny sposób w zależności od tego, jak / gdzie jest używany.

constZmienna jest zawsze tylko jeden typ, który jest określony przez jego deklaracji, a rozwiązany podczas inicjalizacji. Często będzie wymagało jawnego obsady, zanim będzie się zachowywać inaczej (chociaż istnieją różne sytuacje, w których można go bezpiecznie promować domyślnie). Kompilator może przynajmniej (jeśli poprawnie skonfigurowany) emitować bardziej niezawodne ostrzeżenie, gdy wystąpi problem z typem.

Możliwym obejściem tego problemu jest dołączenie jawnej rzutowania lub sufiksu typu w pliku #define. Na przykład:

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

Takie podejście może potencjalnie powodować problemy ze składnią w niektórych przypadkach, w zależności od tego, jak jest używane.

Zużycie pamięci
W przeciwieństwie do obliczeń ogólnego przeznaczenia, pamięć jest oczywiście cenna w przypadku czegoś takiego jak Arduino. Używanie constzmiennej vs. #definemoże mieć wpływ na to, gdzie dane są przechowywane w pamięci, co może zmusić cię do użycia jednego lub drugiego.

  • const zmienne będą (zwykle) przechowywane w SRAM, wraz ze wszystkimi innymi zmiennymi.
  • Używane dosłownie wartości #definebędą często przechowywane w przestrzeni programu (pamięć Flash) obok samego szkicu.

(Należy pamiętać, że istnieją różne rzeczy, które mogą wpływać dokładnie na to, jak i gdzie coś jest przechowywane, takie jak konfiguracja i optymalizacja kompilatora.)

SRAM i Flash mają różne ograniczenia (np. Odpowiednio 2 KB i 32 KB dla Uno). W przypadku niektórych aplikacji dość łatwo jest zabraknąć SRAM, więc może być pomocne przejście niektórych rzeczy na Flash. Odwrotna sytuacja jest również możliwa, choć prawdopodobnie mniej powszechna.

PROGMEM
Możliwe jest uzyskanie korzyści z bezpieczeństwa typu przy jednoczesnym przechowywaniu danych w przestrzeni programu (Flash). Odbywa się to za pomocą PROGMEMsłowa kluczowego. Nie działa dla wszystkich typów, ale jest powszechnie używany do tablic liczb całkowitych lub ciągów.

Ogólny formularz podany w dokumentacji jest następujący:

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

Tabele ciągów są nieco bardziej skomplikowane, ale dokumentacja zawiera pełne szczegóły.

Peter Bloomfield
źródło
1

W przypadku zmiennych określonego typu, które nie są zmieniane podczas wykonywania, zwykle można użyć albo.

W przypadku numerów pinów cyfrowych zawartych w zmiennych może działać dowolna - na przykład:

const int ledPin = 13;

Ale jest jedna okoliczność, w której zawsze używam #define

Służy do definiowania analogowych numerów pinów, ponieważ są one alfanumeryczne.

Jasne, można ciężko kod numery pin a2, a3itp wszystkie w całym programie i kompilator będzie wiedział, co z nimi zrobić. Następnie, jeśli zmienisz piny, każde użycie będzie wymagało zmiany.

Co więcej, zawsze lubię mieć moje definicje pinów na górze wszystko w jednym miejscu, więc constpojawia się pytanie, który typ byłby odpowiedni dla pin zdefiniowanego jako A5.

W takich przypadkach zawsze używam #define

Przykład dzielnika napięcia:

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

Wszystkie zmienne konfiguracji znajdują się na górze i nigdy nie będzie zmiany wartości, z adcPinwyjątkiem czasu kompilacji.

Nie martw się o typ adcPin. W pamięci binarnej nie jest używana dodatkowa pamięć RAM do przechowywania stałej.

Kompilator po prostu zastępuje każde wystąpienie adcPinłańcucha A5przed kompilacją.


Istnieje interesujący wątek na forum Arduino, który omawia inne sposoby decydowania:

#define vs. const zmienna (forum Arduino)

Excertps:

Podstawienie kodu:

#define FOREVER for( ; ; )

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

Kod debugowania:

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

Definiowanie truei falsejako wartość logiczna w celu oszczędzania pamięci RAM

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

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

Wiele z nich sprowadza się do osobistych preferencji, jednak jasne jest, że #definejest bardziej wszechstronny.

SDsolar
źródło
W tych samych okolicznościach a constnie zużywa więcej pamięci RAM niż a #define. A dla pinów analogowych zdefiniowałbym je jako const uint8_t, choć const intnie zrobiłoby to żadnej różnicy.
Edgar Bonet
Napisałeś „ a consttak naprawdę nie zużywa więcej pamięci RAM [...], dopóki nie zostanie faktycznie wykorzystane ”. Przegapiłeś mój punkt: przez większość czasu a constnie używa pamięci RAM, nawet jeśli jest używana . Następnie „ jest to kompilator wielopasmowy ”. Co najważniejsze, jest to kompilator optymalizujący . O ile to możliwe, stałe są optymalizowane do bezpośrednich operandów .
Edgar Bonet