Урок №151. Контейнерные классы

  Юрий  | 

  |

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

 69530

 ǀ   9 

В реальной жизни мы постоянно используем контейнеры. Гречка с куриной грудкой в контейнере для еды, страницы в книге с обложкой и переплетом, вещи в тумбочке/рюкзаке и т.д. Без этих контейнеров было бы крайне неудобно работать с объектами, находящимися внутри. Представьте, что вы пытаетесь читать книгу без переплета и обложки, или пытаетесь есть гречку с грудкой, не используя контейнер для еды/миску/тарелку и т.д. Непорядок! Ценность контейнеров заключается в том, что они помогают должным образом организовать и хранить объекты.

Контейнерные классы

Контейнерный класс (или «класс-контейнер») в языке 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 (228 оценок, среднее: 4,98 из 5)
Загрузка...

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

  1. KILYAV:

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

  2. Vlad:

    Господа, а ни у кого не появляется вернинга на этой строчке ?

    warning c6386 buffer overrun while writing to

  3. Val:

    Добрый день

    Интересный курс.
    Спасибо.
    Есть вопрос по теме.
    А почему мы не удаляем указатель *data на массив в функции remove и в других?
    Это разве не приведёт к утечке памяти?
    Надо ли использовать delete[] data?

    1. Mikasey:

      Уже поздно но отвечу: m_data и data это указатели на один и тот же элемент в памяти, следовательно при уничтожении m_data уничтожается и data, и наоборот. Следовательно утечки не возникает

  4. DenMark:

    Добрый день!
    Спасибо за ваши туториалы очень помогли при изучении С++. Скажите у вас будут туториалы по TCP/IP сокетам?

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

      Привет. В ближайшем времени не планируется. В будущем — возможно.

    2. Владимир:

      По сокетам есть годная, небольшая книжка (20 стр), кратко и по делу — kpnc.opennet.ru/sock.pdf.

  5. Shom:

    "Польза кувшина в его пустоте". Лао Цзы.

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

      Эт точно 🙂

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

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