Menu

Enum в C: именованные константы, автонумерация и перевод в строку

Как объявить enum в C: автоматическая нумерация, явные значения, использование перечислений в switch, чтобы компилятор ловил пропущенные case, перевод enum в строку и соглашения об именовании, которые делают код читаемым.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Код, набитый голыми числами, — это код, который никто не может прочитать. 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>.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ