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

  Юрий  | 

    | 

  Обновл. 21 Апр 2019  | 

 19232

 ǀ   17 

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 (82 оценок, среднее: 4,76 из 5)
Загрузка...

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

  1. Аватар Денис:

    Юрий, добрый день)
    Я извиняюсь, но я вообще не могу понять зачем это нужно и где его применить… Как 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. Аватар Alexey:

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

  3. Аватар Константин:

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

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

    1. Аватар Илья:

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

      или

      1. Аватар Илья:

      2. Аватар Константин:

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

  4. Аватар Dimoss76:

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

    1. Юрий Юрий:

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

  5. Аватар 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(). Здесь уже на примерах показал вам.

  6. Аватар Илья:

    Здравствуйте.
    Есть вопрос:
    В примере с задачкой про 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);) — это уже под вопросом.

  7. Аватар Old G.B.:

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

    1. Юрий Юрий:

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

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

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