Урок №58. Перечисления

  Юрий  | 

  |

  Обновл. 4 Сен 2021  | 

 163232

 ǀ   36 

Язык C++ позволяет программистам создавать свои собственные (пользовательские) типы данных.

Перечисляемые типы

Перечисление (или «перечисляемый тип») — это тип данных, где любое значение (или «перечислитель») определяется как символьная константа. Объявить перечисление можно с помощью ключевого слова enum. Например:

Объявление перечислений не требует выделения памяти. Только когда переменная перечисляемого типа определена (например, как переменная paint в примере, приведенном выше), только тогда выделяется память для этой переменной.

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

Примечание: До C++11, конечная запятая после последнего перечислителя (как после COLOR_PURPLE в примере, приведенном выше) не разрешается (хотя многие компиляторы её все равно принимают). Однако начиная с C++11 конечная запятая разрешена.

Имена перечислений


Идентификаторы перечислений часто начинаются с заглавной буквы, а имена перечислителей вообще состоят только из заглавных букв. Поскольку перечислители вместе с перечислением находятся в едином пространстве имен, то имена перечислителей не могут повторяться в разных перечислениях:

Распространено добавление названия перечисления в качестве префикса к перечислителям, например: ANIMAL_ или COLOR_, как для предотвращения конфликтов имен, так и в целях комментирования кода.

Значения перечислителей

Каждому перечислителю автоматически присваивается целочисленное значение в зависимости от его позиции в списке перечисления. По умолчанию, первому перечислителю присваивается целое число 0, а каждому следующему — на единицу больше, чем предыдущему:

Результат выполнения программы:

4

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

Обратите внимание, ANIMAL_HORSE и ANIMAL_ZEBRA имеют одинаковые значения. Хотя C++ это не запрещает, присваивать одно значение нескольким перечислителям в одном перечислении не рекомендуется.

Совет: Не присваивайте свои значения перечислителям.

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

Обработка перечислений


Поскольку значениями перечислителей являются целые числа, то их можно присваивать целочисленным переменным, а также выводить в консоль (как переменные типа int):

Результат выполнения программы:

6

Компилятор не будет неявно конвертировать целочисленное значение в значение перечислителя. Следующее вызовет ошибку компиляции:

Тем не менее, вы можете сделать подобное с помощью оператора static_cast:

Компилятор также не позволит вам вводить перечислители через std::cin:

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

Каждый перечисляемый тип считается отдельным типом. Следовательно, попытка присвоить перечислитель из одного перечисления перечислителю из другого — вызовет ошибку компиляции:

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

Вывод перечислителей

Попытка вывести перечисляемое значение с помощью std::cout приведет к выводу целочисленного значения самого перечислителя (т.е. его порядкового номера). Но как вывести значение перечислителя в виде текста? Один из способов — написать функцию с использованием стейтментов if:

Выделение памяти для перечислений


Перечисляемые типы считаются частью семейства целочисленных типов, и компилятор сам определяет, сколько памяти выделять для переменных типа enum. По стандарту C++ размер перечисления должен быть достаточно большим, чтобы иметь возможность вместить все перечислители. Но чаще всего размеры переменных enum будут такими же, как и размеры обычных переменных типа int.

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

Польза от перечислений

Перечисляемые типы невероятно полезны для документации кода и улучшения читабельности.

Например, функции часто возвращают целые числа обратно в caller в качестве кодов ошибок, если что-то пошло не так. Как правило, небольшие отрицательные числа используются для представления возможных кодов ошибок. Например:

Однако магические числа, как в вышеприведенном примере, не очень эффективное решение. Альтернатива — использовать перечисления:

Это и читать легче, и понять проще. Кроме того, функция, которая вызывает другую функцию, может проверить возвращаемое значение на соответствующий перечислитель. Это лучше, нежели самому сравнивать возвращаемый результат с конкретными целочисленными значениями, чтобы понять какая именно ошибка произошла, не так ли? Например:

Перечисляемые типы лучше всего использовать при определении набора связанных идентификаторов. Например, предположим, что вы пишете игру, в которой игрок может иметь один предмет, но этот предмет может быть нескольких разных типов:

Или, если вы пишете функцию для сортировки группы значений:

Многие языки программирования используют перечисления для определения логических значений. По сути, логический тип данных — это простое перечисление всего лишь с двумя перечислителями: true и false! Однако в языке C++ значения true и false определены как ключевые слова вместо перечислителей.

Тест

Задание №1

Напишите перечисление со следующими перечислителями: ogre, goblin, skeleton, orc и troll.

Ответ №1

Задание №2

Объявите переменную перечисляемого типа, который вы определили в задании №1, и присвойте ей значение ogre.

Ответ №2

Задание №3

Правда или ложь:

Перечислителям можно:

   присваивать целочисленные значения;

   не присваивать значения;

   явно присваивать значения типа с плавающей точкой;

   присваивать значения предыдущих перечислителей (например, COLOR_BLUE = COLOR_GRAY).

Перечислители могут быть:

   отрицательными;

   не уникальными.

Ответ №3

Перечислителям можно:

   Правда.

   Правда. Перечислителю без значения будет неявно присвоено целочисленное значение предыдущего перечислителя +1. Если предыдущего перечислителя нет, то тогда присвоится значение 0.

   Ложь.

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

Перечислители могут быть:

   Правда.

   Правда.

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (487 оценок, среднее: 4,87 из 5)
Загрузка...

Комментариев: 36

  1. Platon:

    Возможно ошибка в Тесте.
    ———————————————
    Задание №3
    Правда или ложь:

    Перечислители могут быть:
    -> не уникальными.

    Ответ: Правда. (14/12/2021)
    ———————————————
    Имена перечислителей могут быть не уникальными только в разных перечислениях находящихся в различных прастранствах имен, иначе не компилируется. Если в тесте это имеется ввиду, то замечание снимается.

    Спасибо за сайт. Очень полезный.

    1. Petr:

      Скорее всего идет речь про значение, которое может быть присвоено перечислению. Потому что или отрицательное или не уникальное, то есть может быть повторено в перечислении.

  2. Pechenechka:

    Тут можно как-нибудь сделать ввод буквами? К примеру перед пользователем появляется вывод и нужно ввести, например, цвет. RED, ORANGE и т.д. А в темах перечисления и классы enum рассказывается только о таком способе. Через цифру как-то некрасиво.

    1. Константин:

      эй печенечька пишешь, мол, так и так, ежели пользователь вводит RED в приготовленную переменную типа string, то функция возвращает ноль, который статик кэстом преобразуется в соответству.ющий перечислитель и т.д.

  3. Aleksandr P:

    Доброго времени суток, в чем смысл указывать в return std::string, все прекрасно работает и без него, либо я чего-то не понимаю. Сама функция же имеет тип возвращаемого значения string.

    1. Артурка:

      Да, ты прав, функция имеет возвращаемый тип std::string, по этому если не использовать конструкторы типа std::string в возвращаемом типе, то каст const char* в std::string будет произведен неявно. Видимо это сделано для наглядности происходящего.

  4. Максим:

    Возможно ли вывести через оператор std::cout на экран не значение одного из перечеслителей а сам перечеслитель? ( не значение цвета RED = 1, в виде еденицы а само RED )

    1. Aleksandr P:

      С помощью Switch или If else;

  5. Юлия:

    Очень мотивируют живые примеры с оружием и монстрами, а не с поднадоевшими студентами

    1. Константин:

      …сразу окончание анекдота: "Ну вот ты например кем работаешь?"
      "Автослесарем, а что?"
      "А теперь представь себе: в отпуске лежишь себе на пляже, а вокруг — машины, машины, машины…":-)

  6. Владимир:

    Прочитав урок 3 раза и туго понимая что к чему, я таки нашел ключевую фразу, которая сразу прояснила все в голове. Все стало понятно. Думаю не я один такой, поэтому рассказываю:
    для меня этой ключевой фразой послужил 3-ий сверху комментарий в 1-ом коде урока. // Это все возможные значения этого типа данных
    …Шит!… это же ТИП данных, такой же как int или char! А перечисление — это буквально ПЕРЕЧИСЛЕНИЕ всех символьных имен этого( создаваемого нами самими типа), с присвоением им порядковых, или иных, на выбор, номеров! Т.е. просто перечисляем по очереди имена всех возможных значений этого типа, и присваиваем им ( или это делает автоматически компилятор) целочисленные идентификационные значения (номера). И конечно, они могут быть только константными(постоянными). Это же логично. К примеру, в типе CHAR , мы же заранее знаем все символьные значения и их числовые эквиваленты, и все это неизменно. А здесь у нас свобода назвать свой тип данных, выбрать символы и их числовые эквиваленты. Кстати, получается таблица ASII с ее символами и нумерацией, это тоже своего рода перечисление, только по умолчанию. Вот и все!

  7. koresh:

    Перечисления перечислениями , а мне непонятно одно — для чего нужны переменные типа перечисления если и без них всё работает. Ответа нигде не нашёл.

    1. Алекс:

      Перечисления нужны:
      1) для лучшего комментирования кода;
      2) для упрощения разработки.
      Как минимум.

  8. Алексей:

    Тест №3 — подстава, с плавающей ничего не было) Хотя я внимательно читал — целочисловые.

    Вот присвоение значений других определителей — тоже самое, нету об этом, хотя что стоит дать число от другого, когда можно дать любое число перечислителю.

  9. Алексей:

    В задании №2 сделал так:

    Правда, тут отличается? Это же инициализация, не просто объявление.

  10. Денис:

    Юрий, добрый день)
    Я извиняюсь, но я вообще не могу понять зачем это нужно и где его применить… Как enum может помочь в сортировке и в прочем. Я учу каждый урок по очереди. На данном этапе вообще не понятно. Это очень не удобная конструкция (как по мне), скорее всего просто я её не понял. Если можно было-бы присвоить int, string и т.д. значения этим перечислениям, было-бы здорово, а так это просто растянутый текст типа (COLOR_Space_Blue), который хранит в себе (0,1,2 и в том духе далее), который, как написано, кроме как вывести это самое 0,1,2… , больше не на что не способно.

    Может вы не раскрыли потенциал enum в статье, либо я тормоз и не могу понять что, как, а главное — зачем… Я уже 3-й раз возвращаюсь к этому уроку, но в пустую. Написал программу по выбору автомобиля, в зависимости от суммы, в надежде применить enum car, model, color… Но enum как не лепил, везде обхожусь самим if, cout, cin. Перечисления просто в шапке программы весит никому не нужное.
    Ответьте пожалуйста, можно не выкладывать на сайт мой отзыв, почта у вас есть 🙂

    1. Константин:

      Денис, "Приключения Электроника" смотрел? Помнишь Гусев в сердцах крикнул в сторону ЭВМ: "А-а-ах! Чурбан железный — чтоб ты заржавел!!!"? Вот и я о том же, что эта скотина никаких букв не понимает. Только цифры и только две. А эти enum выдуманы для напоминалок кодеру что какая цифра обозначает. Ну это как хозяйка семена весной замачивает-садит и пишет себе записочки где и какие семена сидят. А т.к. слова ПК не понимает, то приходится двойную бухгалтерию вести — сперва расписывать имена перечислений, а чтобы в консоли прочитать что это было пристёгивать к ним (с помощью cout) ещё одну записочку "CAБЖ такой-то". А Юре не досуг на тупые вопросы отвечать — по себе знаю!

    2. Давид:

      Преимущество исключительно в удобочитаемости кода. Я обычно перечисления использую как уникальные идентификаторы чего-либо, допустим горячей клавиши, когда читаешь код обработки горячих клавиш где вместо цифр 1, 2, 3… стоят имена из перечисления, то сразу становится понятно. Плюс, так относительно легко добавить новую горячую клавишу, не нужно следить за уникальностью идентификаторов.

  11. Alexey:

    Не знаю как в других компиляторах, но в VS очень удобно использовать Switch вместе с enum, он автоматом подставляет значения перечислений.

  12. Константин:

    Юра, эти уроки — настоящее интеллектуальное развлечение! Но вот я, таки, решил поиграть в игрушку-стрелялку. Запустил программу, т. е. режим run-time. Бегу, стреляю из ружья — разработчик дал мне его:

    …и вот я подбегаю к такому участку, где нельзя шуметь, иначе на шум сбегутся столько монстров, что разорвут меня вместе с ружьём. А вот из арбалета можно тихонько завалить одного монстра-охранника и пройти это непроходимое место дальше. Только КАК можно на ходу переключиться на арбалет? Он же заявлен в типах вооружений:

    1. Илья:

      поменять на арбалет:

      или

      1. Илья:

      2. Константин:

        Ээ-э-эй, Илья! Когда я уже бегу внутри игры — это RUNTIME, т.е. режим выполнения проги процем. А ты предлагаешь вариант COMPILETIME, т.е. задать условия ДО начала игры…

        1. Денис:

          можно сделать событием прокручивания колёсиком/клик на любую клавишу или комбинацию клавиш (так называемые hot keys), но это всё прелести Qt. Писал проэкты и много раз делал события на нажатия на кнопки клавы, клацанье на кнопки мыши и т.д. и т.п.
          Qt очень даже офигенная вещь в этом плане!

        2. Константин:

          A-a! LДогнал! Надо( используя статик_кэст через промежуточную целочисленную переменную ) всунуть нужное значение в переменную, придуманного мною типа!

    2. Константин:

  13. Dimoss76:

    Извините, не могу сообразить. Возникли вопросы:
    1.можно ли имена перечислителей перебрать и вывести на печать с помощью цикла?
    2.Можно ли переменной задать следующее значение из списка перечислителей?

    1. Фото аватара Юрий:

      1. Можно, цикл for в помощь.
      2. Тоже можно, инкремент в помощь.

  14. Liam:

    Ничего не понятно, в самом начале объявляем enum, но в "Выделение памяти для перечислений. Предварительное объявление" пишут, что объявлять предварительно нельзя.
    Потом пишут, что объявления не требуют памяти, затем в абзаце написано, что и определение не требует памяти, так когда же память выделяется?

    1. Фото аватара Юрий:

      Объявление самого перечисления без объявления переменных не приводит к выделению памяти. Память выделяется при определении переменных перечисления, НЕ при объявлении самого перечисления — что тут непонятного и как объяснить еще проще — я уже и не знаю.

      Насчет предварительного объявления — вы урок об этом читали? Вот урок о предварительном объявлении. Объявление и предварительное объявление — это не одно и то же. Объявление следует вместе с определением (телом перечисления), предварительное объявление — это одна строчка коду.

      Объявлять перечисление вы можете сколько угодно раз, использовать предварительное объявление (одну строчку кода, без определения самого перечисления — тела перечисления) — нет.

      1. Liam:

        Гхм, кто-то из нас явно что-то не так понимает, наверное я, раз я только начал изучать С++ . Посмотрел вашу статью, вы в примерах о предварительном объявлении и просто об объявлении даете абсолютно один и тот же пример —
        int add(int x, int y) . И там и там одна строчка кода.
        И вот как раз в определении вы дали правильный пример int add(int x, int y) {return x+y}
        Во-вторых я только что проверил, мой компилятор не ругается на конструкцию enum Colors{}; Т.е я могу объявлять таким образом enum

        1. Фото аватара Юрий:

          Запустите у себя код:

          Не скомпилируется. Почему? Потому что нельзя использовать предварительное объявление для перечислений.

          Определение функции включает в себя объявление:

          и определение:

          Прототип функции (то же предварительное объявление) включает в себя только строку объявления объекта:

          Прототип функции указывается выше main(), а само определение функции ниже main(). Здесь уже на примерах показал вам.

  15. Илья:

    Здравствуйте.
    Есть вопрос:
    В примере с задачкой про GUN, ARBALET и SWORD программа не работает как надо, ведь функция getItemName всегда будет возвращать ARBALET, так как он равен 1 (GUN == 0, SWORD == 2). Если в типе данных enum ItemType всем присвоить 1, то выдает первое значение — GUN.
    Как это исправить? Спасибо.

    1. Фото аватара Юрий:

      В примере функция getItemName возвращает Gun, так как мы передаем аргумент itemType, которому, предварительно в строке 26 (ItemType itemType(ITEMTYPE_GUN);) установили значение ITEMTYPE_GUN. Функция getItemName не будет работать без передаваемого аргумента, т.е. мы сами определяем, что будет выводиться на экран, указывая один из элементов структуры ItemType (ITEMTYPE_GUN, ITEMTYPE_ARBALET или ITEMTYPE_SWORD). Члены ItemType имеют значения от 0 до 2, но определение того, что будет выводить getItemName осуществляется в main, когда мы указываем аргумент. Как вы уже изменили код, что у вас программа выводит что-либо другое, кроме того, что вы указываете в строке 26 (ItemType itemType(ITEMTYPE_GUN);) — это уже под вопросом.

  16. Old G.B.:

    вот здесь я впервые понял что ничего не понял

    1. Фото аватара Юрий:

      Не без этого. Попробуйте внимательнее перечитать этот урок, но уже не следующий день. Поищите дополнительную информацию в Интернете на эту тему, если уж никак не идёт.

Добавить комментарий

Ваш E-mail не будет опубликован. Обязательные поля помечены *