В прошлом видео мы выяснили, что настоящий вектор должен конструировать свои объекты в сырой памяти, которой он владеет. Что же это такое — сырая память? Давайте рассмотрим следующий пример. В этой заготовке у меня уже написана структура "Student", в которой есть два поля: имя студента и его рейтинг. Есть вспомогательная функция "Print", которая печатает эти поля на экране и функция "Main": у меня пока два понятных простых примера. В первом случае, у меня создается объект этого типа на стеке — такой объект, как вы помните, живет до конца блока. Я вызываю функцию "Print". Во втором случае, я создаю объект в динамической памяти. В этом случае я должен сам, с помощью конструкции "Delete", освободить этот объект, когда он мне станет не нужен. Эта конструкция "New", которая здесь используется, — это транзакции. Она делает две вещи: во-первых, она запрашивает блок памяти у операционной системы, а затем, если это произошло успешно, в этом блоке памяти конструирует объект. Соответственно, парная ей конструкция "Delete" тоже ведет себя, как транзакция. Она сначала уничтожает объект, а после этого возвращает уже ненужную память обратно операционной системе. В первом случае, например, эта память была взята на стеке. Посмотрим, нельзя ли эту память выделить как-то иначе. Попробуем в качестве такой памяти для хранения объекта использовать статический буфер. Статический буфер — это будет просто массив какого-то достаточно большого размера, скажем одного килобайта нам хватит. Сделаем такой массив на стеке. Можем написать, чтоб он был выровнен подходящим образом, чтобы структура типа "Student" там хорошо размещалась. Это пока просто сырая память, которая создается на стеке, и в которой никакого "Student" пока нет. Для того, чтобы создать его там, нам надо воспользоваться так называемым размещающим оператором "New". Напишем вот такую конструкцию. Это так называемый размещающий оператор "New". Это особая форма оператора "New", где я специально подсказываю этому оператору, в каком уже готовом месте памяти надо сконструировать объект. В качестве этого параметра, я передаю указатель на этот статический буфер. Размещающий оператор "New" — это единственный случай, который используется вместе с явным ручным вызовом деструктора. Объект, который был создан через размещающий оператор "New", в каком-то уже имеющемся блоке памяти, вы должны уничтожить самостоятельно, вызвав его деструктор. В конце, после того как мы с объектом работали, напечатали на экране его содержимое, мы должны явно позвать его деструктор. Проверим, работает ли этот код. Да, он работает. Я вижу на экране три надписи из каждого из блоков. Тот буфер, который мы выделяли, необязательно должен быть на стеке. С таким же успехом его можно выделить динамически, поэтому давайте модифицируем наш пример так, чтобы этот буфер выделялся уже в динамической памяти. Раз он выделяется в динамической памяти, то и освобождать мы его уже должны отдельной командой. В данном случае, я выделяю килобайт памяти под массив типа "Char" и в конце вызываем "Delete". Давайте убедимся, что и такая конструкция тоже работает. Не очень правильно пользоваться здесь двумя конструкциями "New". "Char" — это, конечно, примитивный тип. Для "Char" нет такого понятия, как конструктор, но считается, что более правильно выделять сырую динамическую память надо с помощью специальной конструкции, которая называется "Operator new" — не самое удачное название. Многие путают его с обычным словом "New". Давайте запомним, что "Operator new" — это просто такая функция, которую можно перегрузить, кстати, для ваших нужд, и которая просто запрашивает сырой блок памяти у операционной системы в куче, с целью размещать дальше там объекты вручную. Заметим, что 1024 — это, наверное, много. Мы можем сразу передавать сюда необходимый нам размер — размер типа T, точнее, типа "Student", в качестве размера этого буфера. Парной конструкцией к "Operator new", как вы догадались, будет оператор "Delete". Ему можно просто передать этот буфер. Есть лишь одна тонкость: тот указатель, который возвращает "Operator new", уже не привязан к типу "Char" — это указатель на "Void", поэтому переменной "Buf" будет иметь тип "Void*", но это не страшно, мы могли бы вот здесь, при вызове плейсмент "New", не запоминать тот адрес, тот указатель, который вернулся нам обратно, а использовать этот самый буфер, просто приводя его к правильному типу. Нам удобнее получить этот результат, в размещающем "New", сразу с правильным типом. Проверим, что и такой код работает. Да, он работает. Я вам покажу интересную конструкцию, которую мне хочется назвать словом "Феникс", которая в принципе допустима по стандарту. Представим, что я создал объект типа "Student" на стеке. И стандарт не запрещает сделать так: можем вызвать явно деструктор, руками для этого объекта, но потом обязательно должны породить этот объект снова — он как-будто бы Феникс, воскресает из пепла. Проверим, что и в этом случае с объектом мы работаем нормально. Обратите внимание, что деструктор, для вновь созданного объекта, мне уже звать не надо, поскольку он и так автоматически позовется, когда мы покинем пределы этого блока. Да, и эта конструкция тоже работает. Вот такой подход, с размещающим "Operator new", который заранее зарезервированный в сырой памяти по требованию, при необходимости, будет конструировать объекты, мы будем использовать при написании нашей версии вектора. Для начала, я предлагаю вам решить более простую задачу: написать свою версию стандартного класса "Optional". "Optional" можно рассматривать в каком-то приближении, как вектор, в котором не более одного элемента. Посмотрим на предлагаемый вам шаблон этой задачи. Здесь видно, что объект типа "Optional" должен хранить внутри у себя какой-то буфер, и этот буфер хранится на стеке. Такая специальная конструкция здесь нужна только для того, чтобы обеспечить правильное выравнивание. Вместе с этим буфером у нас есть "Bool" флажок, который будет показывать: действительно ли мы создали в этом месте объект, заполнен ли этот "Optional". Забегая вперед, хочу сказать, что настоящий "Optional" написан еще сложнее: в нем используется "Union", но давайте не будем уходить в эти дебри. Для нас вполне достаточно будет использовать вот такой статический буфер. Итак, ваша задача будет написать класс "Optional", а потом мы перейдем к написанию класса "Vector".