Код, набитый голыми числами, — это код, который никто не может прочитать. if (state == 2) не говорит ни о чём; if (state == STATE_RUNNING) говорит всё. enum — это способ C дать небольшому фиксированному набору целых чисел нормальные имена, и, в отличие от кучи #define, они собраны в один тип, о котором знает компилятор.
Объявление перечисления
Объявление задаёт тег и перечисляет элементы в фигурных скобках:
Сразу две детали. Во-первых, нумерация начинается с 0 и растёт на единицу, поэтому RED — 0, GREEN — 1, BLUE — 2. Во-вторых, имена элементов видны везде в охватывающей области видимости: вы пишете GREEN, а не Color.GREEN. Они живут в том же пространстве имён, что и ваши переменные, и поэтому в C их традиционно пишут КАПСОМ или снабжают префиксом типа (COLOR_RED), чтобы избежать конфликтов.
Печатается перечисление через %d, потому что значение перечисления и есть целочисленное значение.
Явные значения
Задайте любому элементу явную константу — и следующие за ним продолжат нумерацию оттуда:
Значения не обязаны быть уникальными или упорядоченными — два элемента могут разделять одно число, что иногда удобно для псевдонимов (COLOR_DEFAULT = COLOR_BLACK). Они должны быть целочисленными константами времени компиляции, поэтому вычислить элемент из переменной нельзя.
Распространённая идиома использует это, чтобы хранить количество:
enum Suit {
CLUBS, DIAMONDS, HEARTS, SPADES,
SUIT_COUNT /* автоматически 4 - остаётся верным, если добавить масть */
};
SUIT_COUNT — не настоящая масть; это их количество, которое автонумерация поддерживает бесплатно. Циклы и размеры массивов могут на него опираться и никогда не устареют.
typedef enum
Как и со структурами, полное имя типа в C — это enum Color, а typedef убирает ключевое слово:
Поскольку элементы перечисления — обычные целые числа, начинающиеся с 0, они заодно служат индексами массива: dx[DIR_EAST] работает без каких-либо преобразований. Это самое полезное свойство перечисления, отсчитываемого с нуля.
Перечисления в switch
Связка перечисления с switch — это место, где тип начинает окупаться. Скомпилируйте с -Wall, и компилятор скажет вам, что вы забыли один из случаев.
Добавьте в перечисление STATE_FAILED, и GCC предупредит: enumeration value 'STATE_FAILED' not handled in switch. Это предупреждение — самое близкое к проверке полноты, что есть в C, и это веский довод писать такие switch без ветки default:: default заглушает предупреждение и позволяет новым состояниям проскочить необработанными. Запасной вариант ставьте после switch, как это делает describe.
Преобразование enum в строку
C не хранит имена во время выполнения; printf("%s", GREEN) работать не может, потому что GREEN — это просто 1. Задачу решают два шаблона.
Вариант со switch, который проверяет компилятор:
И вариант с таблицей поиска — короче, но без проверок: если массив и перечисление разойдутся, никто не предупредит:
Назначенные индексы массива ([RED] = "RED") хотя бы удерживают имена рядом с их ячейками, так что перестановка элементов перечисления не перемешает строки молча. Проверка границ важна потому, что ничто не мешает вызывающему передать значение вне перечисления — см. следующий раздел.
Перечисления не герметичны
Переменная перечисления — это целое число внутри, и C не следит за диапазоном:
Арифметика над элементами работает, сравнения с обычными int работают, и приведение значения вне диапазона принимается. Так что относитесь к перечислению как к отличной документации и хорошей подсказке компилятору, но не как к гарантии. Любое значение, пришедшее извне вашей программы — из файла, из аргумента, из сетевого сообщения, — проверяйте, прежде чем передавать в switch.
Размер и хранение
В C17 каждый элемент перечисления должен помещаться в int: enum Huge { BIG = 3000000000 }; — это нарушение ограничения, которое строгий компилятор (-pedantic-errors) отвергает, даже если gcc и clang в режиме по умолчанию принимают его как расширение. Сами элементы имеют тип int, а размер типа перечисления определяется реализацией (на практике — int):
(C23 снимает это ограничение: элемент может выходить за диапазон int, и можно даже выбрать базовый тип записью enum E : unsigned long { ... }. Пока ваш инструментарий не нацелен на C23, держите элементы в пределах int.)
Поскольку конкретный выбор определяется реализацией, никогда не записывайте перечисление напрямую в двоичный файл или сетевой пакет в расчёте на то, что другая машина прочитает его обратно. На границе преобразуйте его в тип фиксированной ширины вроде uint8_t или uint32_t из <stdint.h>.
Enum против #define
Оба дают числу имя. Перечисление, как правило, лучше:
enum | #define | |
|---|---|---|
| Кем обрабатывается | компилятором | препроцессором |
| Группировка | связанные значения имеют общий тип | каждая константа сама по себе |
| Автонумерация | да | нет, нумеруете вручную |
| Видно в отладчике | часто, по имени | нет, только число |
Предупреждение о полноте switch | да | нет |
| Область видимости | уважает блочную область видимости | текстовая, до #undef |
Берите #define, когда константа не целочисленная (строка, дробное число) или когда она должна быть доступна самому препроцессору, как в условной компиляции. Для набора связанных целочисленных состояний, флагов или разновидностей используйте перечисление.
Соглашения об именовании
Перечисления легко конфликтуют, потому что их имена лежат в обычной области видимости. Соглашения, которые спасают крупные проекты на C:
- Снабжайте каждый элемент префиксом типа:
COLOR_RED,STATE_IDLE,HTTP_OK. БезымянныйREDв графической программе рано или поздно встретит чей-нибудь чужойRED. - Пишите элементы КАПСОМ, как выглядят и остальные константы.
- Называйте тип в единственном числе в PascalCase или как
snake_case_t:Color,Stateилиcolor_t— единообразно по всему проекту. - Добавляйте в конец страж
_COUNT, если собираетесь по перечислению итерироваться или индексировать.
Часто задаваемые вопросы
Как объявить enum в C?
enum Color { RED, GREEN, BLUE }; объявляет тип, значениями которого являются три именованные константы. По умолчанию нумерация идёт с 0, поэтому RED — это 0, GREEN — 1, а BLUE — 2. Переменная объявляется как enum Color c = GREEN;, или добавьте typedef, чтобы писать просто Color c.
Можно ли задать конкретные значения элементам enum в C?
Да: enum Status { OK = 200, NOT_FOUND = 404, ERROR = 500 };. Можно задать часть значений, а остальные пойдут следом — любое имя без явного значения продолжает нумерацию от предыдущего, поэтому enum { A = 10, B, C }; даёт B = 11 и C = 12.
Как преобразовать enum в строку в C?
Встроенного способа в C нет: имена элементов перечисления не существуют во время выполнения. Обычный подход — небольшая функция со switch, возвращающая строковый литерал для каждого значения, либо массив строк, индексируемый перечислением. Вариант со switch безопаснее, потому что -Wall предупредит, если у нового элемента нет своего case.
Какой размер у enum в C?
Определяется реализацией, но на практике это sizeof(int) — 4 байта на большинстве систем. Компилятор выбирает целочисленный тип, способный вместить все элементы. Не рассчитывайте на конкретный размер при описании двоичного файла или сетевого пакета; там используйте тип фиксированной ширины из <stdint.h>.