Ну что ж, давайте, начнем писать нашу новую версию класса "Vector". У меня уже есть вот такая заготовка, она похожа на шаблон "SimpleVector". Как и в том случае, наш класс будет шаблонным, у него будут внутри три поля: первое — это указатель на ту область динамической памяти, где начинаются данные, реальное количество элементов, которые хранит наш вектор и количество зарезервированных ячеек для того, чтобы сохранить дополнительный элемент. Я написал четыре вспомогательных функции — это функция "Size", функция "Capaсity" и две версии оператора "Квадратные скобки" для того, чтобы обращаться к элементам, они совершенно очевидны и в "SimpleVector" были точно такие же. Ну, а дальше наша задача — реализовать конструктор, который получает количество элементов, конструктор копирования, деструктор, функцию "Reserve". И потом в следующих видео мы напишем конструктор перемещения, версию оператора присваивания, функцию "Resize", "PushBack", "PopBack", и наконец напишем функцию "EmplaceBack", которая конструирует элементы вектора прямо на месте. Разумеется, интерфейс вектора не исчерпывается только этими функциями. Если вы посмотрите его документацию, вы увидите такие функции, как "Insert", просто "Emplace", "Shrink_to_fit" и так далее. Но после того, как мы напишем эти функции, я думаю, вам будет совершенно понятно, как написать все остальные. Ну, что ж, давайте приступим. И начнем мы с того, что, на самом деле, сделаем некоторый вспомогательный набор функций. Мы уже видели, как с помощью размещающего оператора "new" создавать объекты в сырой памяти. Вот, давайте, такое создание объектов, а также резервирование этой сырой памяти вынесем во вспомогательные статические функции. Я напишу функцию "Allocate", которая будет получать на вход количество объектов, парную ей функцию "Deallocate", которая будет получать на вход какой-то буфер. И эта функция будет вызывать оператор "new", который будет на вход получать "n" умножить на "sizeof(T)", "n" — это количество объектов, "sizeof(T)" — это размер одного объекта и, значит, это будет общее количество байтов. Но наша функция должна вернуть не void-звездочку, а указатель на T, поэтому нам придется написать еще "static_cast" от этого указателя и его вернуть. Функция "Deallocate" просто позовет оператор "delete" от этого буфера. И так же я напишу функцию "Construct", которая на вход получит какой-то буфер, ну, и давайте, напишем версию, которая в этом месте памяти конструирует новый элемент со значением по умолчанию. Она будет просто вызывать размещающий оператор "new". На самом деле, мне будет удобно иметь несколько перегруженных версий этой функции для тех случаев, когда на вход подается какой-то образец, к которому надо применить конструктор копирования. Ну, давайте, значит, здесь мы просто подставим этот образец в конструктор, а в третьей версии функции мы предусмотрим ситуацию, когда на вход подается какой-то временный объект, а, значит, можно воспользоваться функцией "std::move", и если наш тип поддерживает "move" семантику, то просто из этого временного объекта забрать владение. И напишем функцию "Destroy", которая на вход тоже получит буфер и просто вызовет деструктор, наш тип называется "Т", поэтому деструктор называется Тильда Т. Именно в терминах этих функций мне будет удобнее дальше писать все остальные функции вектора. Давайте для того, чтобы это все было перед глазами, я вот эти простые функции перенесу куда-нибудь пониже, в самый конец и начну с реализации конструктора. Сначала конструктор зарезервирует сырую память в необходимом объеме и сохранит то, что получилось в поле "data". В этой памяти пока не сконструировано ни одного элемента, мы владеем этой памятью, но элементов там в реальности нет. Мне надо создать n элементов в этой памяти. Я запускаю цикл. И на каждом шаге цикла в очередной ячейке, которая получается из указателя "data" сдвигом на "i", я конструирую элемент, в данном случае, с помощью конструктора по умолчанию, поэтому я пользуюсь вот этой версией своей функции "Construct". Ну и, конечно же, мне надо переменные "sz" и "cp" установить равными "n". Конструктор копирования будет написан аналогично, с той лишь разницей, что у меня будет задано не "n", а будет задан другой образец, и поэтому вместо "n" я здесь пишу "other.sz". Обратите внимание, что в настоящем векторе, когда вы копируете какой-то вектор в другой, то у вас "Capaсity" не становится такими же, как было у старого вектора, "Capaсity" становится равным размеру. И вот здесь мы пишем "other[i]", то есть берем i-тые элементы из другого вектора. Теперь нам надо написать деструктор, ну, деструктор будет состоять из двух шагов. Сначала мы должны вручную удалить, то есть вызвать деструкторы для всех элементов, которые у нас хранились в векторе, а после этого вернуть сырую память операционной системе. Поэтому тоже пишем цикл. Перебираем все элементы, которые лежат в векторе, для каждого из них вызываем "Destroy(data + i)". "Destroy" требует адрес, поэтому здесь я оперирую указателями, в конце вызываю "Deallocate(data)" и возвращаю память операционной системе. Ну, и давайте напишем функцию "Reserve", мы договорились, что мы именно эти четыре функции напишем сейчас. Что должна делать "Reserve"? "Reserve" должна резервировать память от n будущих элементов. Мы должны сначала сравнить это n с нашей емкостью, с нашим "Capaсity". Если наша "Capaсity" больше чем эта n, то ничего делать не надо, а что-то надо делать только, если новое n больше чем текущая "Capaсity". Ну, что же нам надо сделать? Нам надо где-то в другом месте зарезервировать блок данных размера n. Давайте, сделаем переменную "data2", которая будет равна результату работы функции "Allocate" для n. А после этого в цикле скопировать туда элементы, которые у нас до этого имелись: "data2 + i", мы копируем туда элементы "data[i]". И вот здесь нам как раз можно задуматься, на самом деле, при реаллокации в предположении, что "move" конструкторы наших типов не генерируют исключений, это достаточно разумное предположение, мы можем здесь как раз воспользоваться третьей перегруженной версией функции "Construct" и переместить из старого места этот объект, написав вот здесь "std::move". Он тогда будет рассматриваться, как временный объект. Ну, а раз мы его переместили, то можно уже из старого места его удалить с помощью вызова "Destroy" и в конце, когда все прошло удачно, мы вызываем "Deallocate" на текущие данные и заменяем наши данные новыми. Ну, конечно же, настраиваем переменную "cp" равной новому n. Не сделали мы где-то ошибки? Давайте себя проверим. Для того, чтобы себя проверить, я буду использовать вот такую тестовую программу. Посмотрите, пожалуйста, здесь уже знакомая вам структура "С", которая подсчитывает количество созданных объектов, а сам тест такой — я сделаю вектор из 10 элементов, а потом, с помощью функции "Reserve" зарезервирую память побольше, для 20 элементов. После каждого такого шага я напечатаю, сколько у меня объектов типа "С" вообще было создано с самого начала, а также, каков размер и какая емкость у моего вектора. Запускаем нашу программу. Она не компилируется. Давайте, посмотрим почему. Во-первых, мы забыли подключить заголовочный файл "utility", в котором объявлена функция "move", а, во-вторых, она где-то ругалась на переменую "n", которая вот здесь, конечно же, должна быть равна "other.sz". Да. Программа успешно скомпилировалась. И что же мы видим? Что сначала у нас было 10 объектов и вектор был размера 10, емкости 10, а после реаллокации у нас стало 20 объектов, 10 старых и 10 новых, а не 30 как, было с SimpleVector-ом, как мы видели в первом видео. Размер вектора остался неизменным, а емкость увеличилась в два раза. Это как раз ожидаемое поведение.