Kod pełen gołych liczb to kod, którego nikt nie przeczyta. if (state == 2) nic ci nie mówi; if (state == STATE_RUNNING) mówi wszystko. enum to sposób C na nadanie porządnych nazw małemu, stałemu zestawowi liczb całkowitych, a w przeciwieństwie do sterty #define są one zgrupowane w jednym typie, o którym kompilator wie.
Definiowanie enuma
Deklaracja nadaje nazwę (tag) i wymienia enumeratory w nawiasach klamrowych:
Dwie rzeczy warto zauważyć od razu. Po pierwsze, numeracja zaczyna się od 0 i rośnie o jeden, więc RED to 0, GREEN to 1, a BLUE to 2. Po drugie, nazwy enumeratorów są widoczne w całym otaczającym zasięgu: piszesz GREEN, a nie Color.GREEN. Żyją w tej samej przestrzeni nazw co twoje zmienne, dlatego w kodzie C zwyczajowo się je KRZYCZY albo poprzedza przedrostkiem typu (COLOR_RED), żeby uniknąć kolizji.
Do wypisywania enuma używa się %d, bo wartość wyliczeniowa jest wartością całkowitą.
Jawne wartości
Przypisz dowolnemu enumeratorowi jawną stałą, a kolejne będą kontynuować od niej:
Wartości nie muszą być unikalne ani uporządkowane: dwa enumeratory mogą mieć tę samą liczbę, co czasem przydaje się do aliasów (COLOR_DEFAULT = COLOR_BLACK). Muszą być stałymi całkowitymi czasu kompilacji, więc nie da się wyliczyć żadnej ze zmiennej.
Częsty idiom wykorzystuje to do zapisania liczby elementów:
enum Suit {
CLUBS, DIAMONDS, HEARTS, SPADES,
SUIT_COUNT /* automatycznie 4, pozostaje poprawne po dodaniu koloru */
};
SUIT_COUNT nie jest prawdziwym kolorem karcianym; to liczba kolorów, utrzymywana za darmo przez automatyczną numerację. Pętle i rozmiary tablic mogą jej używać i nigdy się nie zdezaktualizują.
typedef enum
Podobnie jak przy strukturach pełna nazwa typu w C to enum Color, a typedef pozwala pominąć słowo kluczowe:
Ponieważ enumeratory to zwykłe liczby całkowite zaczynające się od 0, służą też za indeksy tablic: dx[DIR_EAST] działa bez żadnej konwersji. To najbardziej przydatna właściwość enuma numerowanego od zera.
Enumy w switch
Połączenie enuma ze switch to moment, w którym ten typ zaczyna się opłacać. Skompiluj z -Wall, a kompilator powie ci, gdy zapomnisz o jakimś przypadku.
Dodaj STATE_FAILED do enuma, a GCC ostrzeże: enumeration value 'STATE_FAILED' not handled in switch. To ostrzeżenie jest najbliższym odpowiednikiem sprawdzania kompletności, jaki ma C, i mocnym powodem, by pisać takie switche bez przypadku default:: default wycisza ostrzeżenie i pozwala nowym stanom prześlizgnąć się bez obsługi. Zamiast tego umieść wartość awaryjną za switch, tak jak robi to describe.
Zamiana enuma na string
C nie przechowuje nazw w czasie działania; printf("%s", GREEN) nie może zadziałać, bo GREEN to po prostu 1. Rozwiązują to dwa wzorce.
Wersja ze switch, którą sprawdza kompilator:
Oraz wersja z tablicą wyszukiwania, krótsza, ale niesprawdzana: jeśli tablica i enum się rozjadą, nic nie ostrzeże:
Desygnowane indeksy tablicy ([RED] = "RED") przynajmniej trzymają nazwy przy ich miejscach, więc zmiana kolejności w enumie nie przetasuje po cichu stringów. Sprawdzenie zakresu ma znaczenie, bo nic nie powstrzyma wywołującego przed przekazaniem wartości spoza enuma; zobacz następną sekcję.
Enumy nie są szczelne
Zmienna typu enum to pod spodem liczba całkowita, a C nie pilnuje zakresu:
Arytmetyka na enumeratorach działa, porównania ze zwykłymi intami działają, a rzutowanie spoza zakresu jest akceptowane. Traktuj więc enum jako świetną dokumentację i dobrą podpowiedź dla kompilatora, a nie jako gwarancję. Sprawdzaj każdą wartość, która przychodzi spoza twojego programu (z pliku, argumentu, wiadomości sieciowej), zanim użyjesz jej w switch.
Rozmiar i przechowywanie
W C17 każdy enumerator musi mieścić się w int: enum Huge { BIG = 3000000000 }; to naruszenie ograniczenia, które ścisły kompilator (-pedantic-errors) odrzuci, choć gcc i clang w trybie domyślnym akceptują to jako rozszerzenie. Same enumeratory mają typ int, a rozmiar typu enum zależy od implementacji (w praktyce to int):
(C23 znosi to ograniczenie: enumerator może wykraczać poza zakres int, a typ bazowy można nawet wybrać przez enum E : unsigned long { ... }. Dopóki twoje narzędzia nie celują w C23, trzymaj enumeratory w zakresie int.)
Ponieważ dokładny wybór zależy od implementacji, nigdy nie zapisuj enuma bezpośrednio do pliku binarnego ani pakietu sieciowego, oczekując, że inna maszyna go odczyta. Na granicy zamieniaj go na typ o stałej szerokości, taki jak uint8_t albo uint32_t z <stdint.h>.
Enum a #define
Oba nadają nazwę liczbie. Enum jest zazwyczaj lepszy:
enum | #define | |
|---|---|---|
| Obsługuje go | kompilator | preprocesor |
| Grupowanie | powiązane wartości mają wspólny typ | każda stała jest niezależna |
| Automatyczna numeracja | tak | nie, numerujesz ręcznie |
| Widoczny w debuggerze | często, z nazwy | nie, tylko liczba |
Ostrzeżenie o kompletności switch | tak | nie |
| Zasięg | respektuje zasięg bloku | tekstowy, aż do #undef |
Sięgaj po #define, gdy stała nie jest liczbą całkowitą (string, float) albo gdy musi z niej korzystać sam preprocesor, jak w kompilacji warunkowej. Dla zestawu powiązanych stanów, flag czy rodzajów, które są liczbami całkowitymi, używaj enuma.
Konwencje nazewnictwa
Enumy łatwo ze sobą kolidują, bo ich nazwy leżą w zwykłym zasięgu. Konwencje, które utrzymują porządek w dużych projektach w C:
- Poprzedzaj każdy enumerator nazwą typu:
COLOR_RED,STATE_IDLE,HTTP_OK.REDbez przedrostka w programie graficznym w końcu spotka czyjeś inneRED. - Pisz enumeratory WIELKIMI_LITERAMI, tak jak wyglądają inne stałe.
- Nazywaj typ w liczbie pojedynczej w PascalCase albo jako
snake_case_t:Color,Statealbocolor_t, konsekwentnie w całym projekcie. - Dodaj na końcu wartownika
_COUNT, gdy będziesz iterować albo indeksować enumem.
Najczęściej zadawane pytania
Jak zdefiniować enum w C?
enum Color { RED, GREEN, BLUE }; definiuje typ, którego wartościami są trzy nazwane stałe. Domyślnie są numerowane od 0, więc RED to 0, GREEN to 1, a BLUE to 2. Zmienną deklaruje się przez enum Color c = GREEN; albo dodaje się typedef, żeby móc pisać po prostu Color c.
Czy w C można przypisać enumowi konkretne wartości?
Tak: enum Status { OK = 200, NOT_FOUND = 404, ERROR = 500 };. Możesz ustawić niektóre i pozwolić, żeby pozostałe poszły dalej: każda nazwa bez jawnej wartości kontynuuje od poprzedniej, więc enum { A = 10, B, C }; daje B = 11 i C = 12.
Jak zamienić enum na string w C?
C nie ma do tego wbudowanego sposobu; nazwy enumeratorów nie istnieją w czasie działania. Typowe podejście to mała funkcja ze switch, która zwraca literał stringowy dla każdej wartości, albo tablica stringów indeksowana enumem. Wersja ze switch jest bezpieczniejsza, bo -Wall ostrzega, gdy nowy enumerator nie ma swojego przypadku.
Jaki jest rozmiar enuma w C?
Zależny od implementacji, ale w praktyce sizeof(int), czyli 4 bajty w większości systemów. Kompilator wybiera typ całkowity, który pomieści każdy enumerator. Nie zakładaj konkretnego rozmiaru przy układaniu pliku binarnego czy pakietu sieciowego; użyj tam typu o stałej szerokości z <stdint.h>.