Мова C++ дозволяє програмістам створювати свої власні (користувацькі) типи даних.
Перелічуваний тип даних
Перерахування (або “перелічуваний тип даних”) — це тип даних, де будь-яке значення (енумератор) визначається як символьна константа. Оголосити перерахування можна за допомогою ключового слова enum. Наприклад:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// Оголошуємо нове перерахування Colors enum Colors { // Нижче знаходяться енумератори - всі можливі значення цього типу даних. // Кожний енумератор розділяється комою (НЕ крапкою з комою) COLOR_RED, COLOR_BROWN, COLOR_GRAY, COLOR_WHITE, COLOR_PINK, COLOR_ORANGE, COLOR_BLUE, COLOR_PURPLE, // про останню кому читайте нижче }; // але сам enum повинен закінчуватися крапкою з комою // Визначаємо декілька змінних перелічуваного типу Colors Colors paint = COLOR_RED; Colors house(COLOR_GRAY); |
Оголошення перерахувань не вимагає виділення пам’яті. Тільки коли змінна перелічуваного типу визначена (наприклад, як змінна paint
у вищенаведеному прикладі), тільки тоді виділяється пам’ять для цієї змінної.
Зверніть увагу, кожен енумератор розділяється комою, а саме перерахування закінчується крапкою з комою.
Примітка: До C++11, кінцева кома після останнього елементу (як після COLOR_PURPLE
у вищенаведеному прикладі) була заборонена (хоча не всі компілятори на це скаржились). Однак починаючи з C++11 кінцева кома дозволена.
Імена перерахувань
Імена перерахувань часто починаються з великої літери, а імена елементів взагалі складаються тільки з великих букв. Оскільки елементи знаходяться в одному і тому ж просторі імен, що і саме перерахування, то одне і те ж ім’я одного енумератора не може бути використано в кількох перерахувань одного і того ж простору імен:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
enum Colors { YELLOW, BLACK, // BLACK знаходиться в глобальному просторі імен PINK }; enum Feelings { SAD, ANGRY, BLACK // отримаємо помилку, так як BLACK вже використовується в enum Colors }; |
Також поширено додання назви перерахування в якості префіксу до його елементів, наприклад: ANIMAL_
чи COLOR_
, як для запобігання конфліктів імен, так і з метою коментування коду.
Значення енумераторів
Кожному енумератору автоматично присвоюється цілочисельне значення в залежності від його позиції в списку перерахування. За замовчуванням, першому елементу присвоюється ціле число 0
, а кожному наступному — на одиницю більше, ніж попередньому:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <iostream> enum Colors { COLOR_YELLOW, // присвоюється 0 COLOR_WHITE, // присвоюється 1 COLOR_ORANGE, // присвоюється 2 COLOR_GREEN, // присвоюється 3 COLOR_RED, // присвоюється 4 COLOR_GRAY, // присвоюється 5 COLOR_PURPLE, // присвоюється 6 COLOR_BROWN // присвоюється 7 }; int main() { Colors paint(COLOR_RED); std::cout << paint; return 0; } |
Результат виконання програми:
4
Можна й самому визначати значення елементів. Вони можуть бути як додатними, так і від’ємними, або взагалі мати аналогічні іншим елементам значення. Будь-які енумератори, що не визначені вами, матимуть значення на одиницю більше, ніж значення попередніх елементів. Наприклад:
1 2 3 4 5 6 7 8 9 10 |
// Визначаємо новий перелічуваний тип Animals enum Animals { ANIMAL_PIG = -4, ANIMAL_LION, // присвоюється -3 ANIMAL_CAT, // присвоюється -2 ANIMAL_HORSE = 6, ANIMAL_ZEBRA = 6, // має те ж значення, що і ANIMAL_HORSE ANIMAL_COW // присвоюється 7 }; |
Зверніть увагу, ANIMAL_HORSE
і ANIMAL_ZEBRA
мають однакові значення. Хоча C++ це не забороняє, присвоювати одне значення декільком енумераторам в одному перерахуванні не рекомендується.
Порада: Не присвоюйте свої значення енумераторам.
Правило: Не присвоюйте однакові значення двом енумераторам в одному перерахуванні, якщо на це немає вагомої причини.
Обробка перерахувань
Оскільки значеннями енумераторів є цілі числа, то їх можна присвоювати цілочисельним змінним, а також виводити на екран (як змінні типу int):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <iostream> // Визначаємо новий перелічуваний тип Animals enum Animals { ANIMAL_PIG = -4, ANIMAL_LION, // присвоюється -3 ANIMAL_CAT, // присвоюється -2 ANIMAL_HORSE = 6, ANIMAL_ZEBRA = 6, // має те ж значення, що і ANIMAL_HORSE ANIMAL_COW // присвоюється 7 }; int main() { int mypet = ANIMAL_PIG; std::cout << ANIMAL_HORSE; // конвертується в int, а потім виводиться на екран return 0; } |
Результат виконання програми:
6
Компілятор не буде неявно конвертувати цілочисельне значення в значення енумератора. Наступне викличе помилку компіляції:
1 |
Animals animal = 7; // викличе помилку компіляції |
Тим не менш, ви можете зробити подібне за допомогою оператора static_cast:
1 |
Colors color = static_cast<Colors>(5); // але так робити не рекомендується |
Компілятор також не дозволить вам вводити енумератори через std::cin:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <iostream> enum Colors { COLOR_PURPLE, // присвоюється 0 COLOR_GRAY, // присвоюється 1 COLOR_BLUE, // присвоюється 2 COLOR_GREEN, // присвоюється 3 COLOR_BROWN, // присвоюється 4 COLOR_PINK, // присвоюється 5 COLOR_YELLOW, // присвоюється 6 COLOR_MAGENTA // присвоюється 7 }; int main() { Colors color; std::cin >> color; // викличе помилку компіляції return 0; } |
Однак, ви можете ввести ціле число, а потім використати оператор static_cast, щоб помістити цілочисельне значення в перелічуваний тип:
1 2 3 4 |
int inputColor; std::cin >> inputColor; Colors color = static_cast<Colors>(inputColor); |
Кожен перелічуваний тип вважається окремим типом, тому спроба присвоїти енумератор з одного перерахування енумератору з іншого — викличе помилку компіляції:
1 |
Animals animal = COLOR_BLUE; // викличе помилку компіляції |
Як і у випадку з константами, перерахування відображаються у відлагоджувачеві, що робить їх ще більш корисними.
Вивід енумераторів
Спроба вивести енумератор за допомогою std::cout призведе до виводу цілочисельного значення самого енумератора (тобто його порядкового номера). Але як вивести значення енумератора у вигляді тексту? Один із способів — написати функцію з використанням if-стейтментів:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
enum Colors { COLOR_PURPLE, // присвоюється 0 COLOR_GRAY, // присвоюється 1 COLOR_BLUE, // присвоюється 2 COLOR_GREEN, // присвоюється 3 COLOR_BROWN, // присвоюється 4 COLOR_PINK, // присвоюється 5 COLOR_YELLOW, // присвоюється 6 COLOR_MAGENTA // присвоюється 7 }; void printColor(Colors color) { if (color == COLOR_PURPLE) std::cout << "Purple"; else if (color == COLOR_GRAY) std::cout << "Gray"; else if (color == COLOR_BLUE) std::cout << "Blue"; else if (color == COLOR_GREEN) std::cout << "Green"; else if (color == COLOR_BROWN) std::cout << "Brown"; else if (color == COLOR_PINK) std::cout << "Pink"; else if (color == COLOR_YELLOW) std::cout << "Yellow"; else if (color == COLOR_MAGENTA) std::cout << "Magenta"; else std::cout << "Who knows!"; } |
Виділення пам’яті для перерахувань
Перелічувані типи вважаються частиною сімейства цілочисельних типів, і компілятор сам визначає, скільки пам’яті виділяти для змінних типу enum. По стандарту мова C++ повідомляє, що розмір перерахування повинен бути досить великим, щоб мати можливість вмістити всі енумератори. Але найчастіше розміри змінних enum будуть такими ж, як і розміри звичайних змінних типу int.
Оскільки компілятору потрібно знати, скільки пам’яті виділяти для перерахування, то використовувати попереднє оголошення з ним ви не зможете. Однак існує простий обхідний шлях. Оскільки визначення перерахування саме по собі не вимагає виділення пам’яті і якщо перерахування необхідно використовувати в декількох файлах, то його можна визначити в заголовковому файлі і підключати цей заголовок всюди, де необхідно використовувати перерахування.
Користь від перерахувань
Перелічувані типи неймовірно корисні для документації коду і поліпшення читабельності.
Наприклад, функції часто повертають цілі числа назад у викликаючу функцію в якості кодів помилок, якщо щось пішло не так. Як правило, невеликі від’ємні числа використовуються для представлення можливих кодів помилок. Наприклад:
1 2 3 4 5 6 7 8 9 10 11 |
int readFileContents() { if (!openFile()) return -1; if (!parseFile()) return -2; if (!readFile()) return -3; return 0; // якщо все пройшло успішно } |
Однак магічні числа, як у вищенаведеному прикладі, не дуже ефективне рішення. Альтернативним рішенням є використання перерахувань:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
enum ParseResult { SUCCESS = 0, ERROR_OPENING_FILE = -1, ERROR_PARSING_FILE = -2, ERROR_READING_FILE = -3 }; ParseResult readFileContents() { if (!openFile()) return ERROR_OPENING_FILE; if (!parseFile()) return ERROR_PARSING_FILE; if (!readfile()) return ERROR_READING_FILE; return SUCCESS; // якщо все пройшло успішно } |
Це і читати легше, і зрозуміти простіше. Крім того, функція, яка викликає іншу функцію, може порівняти значення, що повертається, з певним енумератором. Це краще, ніж самому порівнювати результат, який повертається, з конкретними цілочисельними значеннями, щоб зрозуміти яка саме помилка сталася, чи не так? Наприклад:
1 2 3 4 5 6 7 8 |
if (readFileContents() == SUCCESS) { // Робимо що-небудь } else { // Виводимо повідомлення про помилку } |
Перелічувані типи найкраще використовувати при визначенні набору пов’язаних ідентифікаторів. Наприклад, припустимо, що ви пишете гру, в якій гравець може мати один предмет, але цей предмет може бути декількох різних типів:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
#include <iostream> #include <string> enum ItemType { ITEMTYPE_GUN, ITEMTYPE_ARBALET, ITEMTYPE_SWORD }; std::string getItemName(ItemType itemType) { if (itemType == ITEMTYPE_GUN) return std::string("Gun"); if (itemType == ITEMTYPE_ARBALET) return std::string("Arbalet"); if (itemType == ITEMTYPE_SWORD) return std::string("Sword"); } int main() { // ItemType - це перелічуваний тип, який ми оголосили вище. // itemType (з маленької i) - це ім'я змінної, яку ми визначили (типу ItemType). // ITEMTYPE_GUN - це значення енумератора, яке ми присвоїли змінній itemType ItemType itemType(ITEMTYPE_GUN); std::cout << "You are carrying a " << getItemName(itemType) << "\n"; return 0; } |
Або, якщо ви пишете функцію для сортування групи значень:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
enum SortType { SORTTYPE_FORWARD, SORTTYPE_BACKWARDS }; void sortData(SortType type) { if (type == SORTTYPE_FORWARD) // сортування даних в одному порядку else if (type == SORTTYPE_BACKWARDS) // сортування даних в зворотному порядку } |
Багато мов програмування використовують перерахування для визначення логічних значень. По суті, логічний тип даних — це просте перерахування всього лише з двома енумераторами: true
і false
! Однак в мові C++ значення true
і false
визначені як ключові слова замість енумераторів.
Тест
Завдання №1
Напишіть перерахування з наступними енумераторами: ogre
, goblin
, skeleton
, orc
і troll
.
Відповідь №1
1 2 3 4 5 6 7 8 |
enum MonsterType { MONSTER_OGRE, MONSTER_GOBLIN, MONSTER_SKELETON, MONSTER_ORC, MONSTER_TROLL }; |
Завдання №2
Оголосіть змінну перелічуваного типу, який ви визначили у завданні №1, і присвойте їй тип ogre
.
Відповідь №2
1 |
MonsterType eMonsterType = MONSTER_OGRE; |
Завдання №3
Правда чи брехня:
Енумераторам можна:
присвоювати цілочисельні значення;
не присвоювати значення;
явно присвоювати значення типу з плаваючою крапкою;
присвоювати значення попередніх енумераторів (наприклад, COLOR_BLUE = COLOR_GRAY
).
Енумератори можуть бути:
від’ємними;
не унікальними.
Відповідь №3
Енумераторам можна:
Правда.
Правда. Енумератору без значення буде неявно присвоєно цілочисельне значення попереднього енумератора +1
. Якщо попереднього енумератора немає, то тоді присвоїться значення 0
.
Брехня.
Правда. Оскільки значеннями енумераторів є цілі числа, а цілі числа можна присвоювати енумераторам, то одні енумератори можуть бути присвоєні іншим енумераторам (хоча цього краще уникати).
Енумератори можуть бути:
Правда.
Правда.