Урок №159. Контейнерні класи

  Юрій  | 

  Оновл. 20 Лют 2021  | 

 14

У реальному житті ми постійно використовуємо контейнери. Гречка з курячою грудкою в контейнері для їжі, сторінки в книзі з обкладинкою і палітуркою, речі в тумбочці/рюкзаку і т.д. Без цих контейнерів було б вкрай незручно працювати з об’єктами, що знаходяться всередині. Уявіть, що ви намагаєтеся читати книгу без палітурки і обкладинки, або намагаєтеся їсти гречку з грудкою, не використовуючи контейнер для їжі/тарілку/миску тощо. Непорядок! Цінність контейнерів полягає в тому, що вони допомагають належним чином організувати і зберігати об’єкти.

Контейнерні класи

Контейнерний клас (або “клас-контейнер”) в мові C++ — це клас, призначений для зберігання і організації декількох об’єктів певного типу даних (користувацьких чи фундаментальних). Існує багато різних контейнерних класів, кожен з яких має свої переваги, недоліки або обмеження у використанні. Безумовно, найбільш використовуваним контейнером в програмуванні є масив, який ми вже використовували у багатьох прикладах. Хоча в мові C++ є звичайні стандартні масиви, більшість програмістів використовують контейнерні класи-масиви: std::array або std::vector через переваги, які вони надають. На відміну від стандартних масивів, контейнерні класи-масиви мають можливість динамічної зміни свого розміру, коли елементи додаються або видаляються. Це не тільки робить їх більш зручними, ніж звичайні масиви, а й безпечнішими для використання.

Зазвичай, функціонал класів-контейнерів мови C++ наступний:

   Створення пустого контейнера (через конструктор).

   Додання нового об’єкта в контейнер.

   Видалення об’єкта з контейнеру.

   Перегляд кількості об’єктів, які знаходяться на даний момент в контейнері.

   Очистка контейнера від всіх об’єктів.

   Доступ до збережених об’єктів.

   Сортування об’єктів/елементів (не завжди).

Іноді функціонал контейнерних класів може бути не настільки великим, як це зазначено вище. Наприклад, контейнерні класи-масиви часто не мають функціоналу додання/видалення об’єктів, тому що вони і так повільні, і розробник просто не хоче збільшувати навантаження.

Типом відносин в класах-контейнерах є «член чогось». Наприклад, елементи масиву «є членами» масиву (належать йому). Зверніть увагу, ми тут використовуємо термін «член чогось» не в сенсі члена класу C++.

Типи контейнерних класів

Контейнерні класи зазвичай бувають двох типів:

   Контейнери значення — це композиції, які зберігають копії об’єктів (і відповідальні за створення/знищення цих копій).

   Контейнери посилання — це агрегації, які зберігають вказівники або посилання на інші об’єкти (і не відповідальні за створення/знищення цих об’єктів).

На відміну від реального життя, коли контейнери можуть зберігати будь-які типи об’єктів, які в них поміщають, в мові C++ контейнери зазвичай містять тільки один тип даних. Наприклад, якщо у вас цілочисельний масив, то він може містити тільки цілочисельні значення. На відміну від деяких інших мов програмування, C++ не дозволяє змішувати різні типи даних всередині одного контейнера. Якщо вам потрібні контейнери для зберігання значень типів int і double, то вам доведеться написати два окремих контейнери (або використовувати шаблони, про які ми поговоримо на відповідному уроці). Незважаючи на обмеження їх використання, контейнери надзвичайно корисні, так як роблять програмування простішим, безпечнішим і швидшим.

Контейнерний клас-масив

Зараз ми напишемо цілочисельний клас-масив з нуля, реалізуючи функціонал контейнерів в мові С++. Цей клас-масив буде типу контейнера значення, в якому зберігатимуться копії елементів, а не самі елементи.

Спочатку створимо файл ArrayInt.h:

Наш ArrayInt повинен відслідковувати два значення: дані і свою довжину. Оскільки ми хочемо, щоб наш масив міг змінювати свою довжину, то нам потрібно використовувати динамічне виділення пам’яті, що означає, що ми будемо використовувати вказівник для зберігання даних:

Тепер нам потрібно додати конструктори, щоб мати можливість створювати об’єкти класу ArrayInt. Ми додамо два конструктори: перший створюватиме порожній масив, другий — масив заданого розміру:

Нам також потрібні функції, які виконуватимуть очистку ArrayInt. По-перше, додамо деструктор, який просто звільнятиме будь-яку динамічно виділену пам’ять. По-друге, напишемо функцію erase(), яка виконуватиме очистку масиву і скидатиме його довжину на 0:

Тепер перевантажимо оператор індексації [], щоб мати доступ до елементів масиву. Ми також повинні виконати перевірку коректності переданого індексу, що найкраще зробити за допомогою стейтмента assert. Також додамо функцію доступу для повернення довжини масиву:

Тепер у нас вже є клас ArrayInt, який ми можемо використовувати. Ми можемо виділити масив певного розміру і використовувати оператор [] для вилучення або зміни значень елементів.

Тим не менш, є ще кілька речей, які ми не можемо виконати з нашим ArrayInt. Це автоматична зміна розміру масиву, додання/видалення елементів і сортування елементів.

По-перше, давайте реалізуємо можливість масиву змінювати свій розмір. Ми напишемо дві різні функції для цього. Перша функція — reallocate(), при зміні розміру масиву знищуватиме всі існуючі елементи (це швидко). Друга функція — resize(), при зміні розміру масиву зберігатиме всі існуючі елементи (це повільно).

Фух! Було непросто!

Функціонал більшості контейнерних класів-масивів на цьому закінчується. Однак, якщо ви хочете побачити, як реалізувати можливість додання/видалення елементів, то ми зараз це розглянемо. Наступні два алгоритми дуже схожі на функцію resize():

Ось наш контейнерний клас-масив ArrayInt повністю.

ArrayInt.h:

Тепер протестуємо програму:

Результат:

50 1 2 3 4 15 6 7 35

Хоча написання контейнерних класів може бути дещо складним, але хороша новина полягає в тому, що вам їх потрібно написати тільки один раз. Як тільки контейнерний клас працює, ви можете його повторно використовувати де-завгодно без будь-яких додаткових дій/зусиль з боку програмування.

Також варто відзначити, що, хоча наш контейнерний клас ArrayInt містить фундаментальний тип даних (int), ми також могли б легко використовувати і користувацький тип даних.

Примітка: Якщо клас зі Стандартної бібліотеки C++ повністю відповідає вашим потребам, то використовуйте його замість написання свого контейнерного класу. Наприклад, замість ArrayInt краще використовувати std::vector<int>, так як реалізація std::vector<int> протестована/перевірена вже багатьма роками, ефективна і відмінно працює з іншими класами зі Стандартної бібліотеки C++. Але так як не завжди може бути можливим використовувати класи зі Стандартної бібліотеки C++, то ви вже знаєте, як створювати свої власні контейнерні класи.

Оцінити статтю:

1 Зірка2 Зірки3 Зірки4 Зірки5 Зірок (Немає Оцінок)
Loading...

Залишити відповідь

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