[БЕЗ_ЗВУКА] Давайте продвигаться дальше в изучении модели памяти в языке С++. Я напомню, что мы изучаем модель памяти для того, чтобы затем изучить внутреннее устройство вектора, map'а, set'а и так далее. Лучший способ узнать, как что-то устроено внутри, это сделать самому. Поэтому давайте мы начнем писать свой собственный вектор. И начнем мы с вот такого простого интерфейса. Мы объявим шаблон класса. Назовем его SimpleVector, потому что наш вектор будет очень простой. Он будет гораздо проще стандартного. И в интерфейсе нашего класса пока что будут только конструктор и деструктор. Конструктор принимает размер — это количество элементов, которое будет в нашем векторе. Ну а деструктор выполняется при разрушении объекта. Ещё у нас будет приватное поле data, которое будет являться указателем на на тип T, собственно, на тип элементов нашего объекта. Ну мы с вами уже изучили, что вектор хранит свои данные и свои объекты в куче, ну а для того чтобы нам к этой куче обращаться, нам нужен указатель. Ну и вот пример использования нашего вектора: мы здесь в функции main создаем объект класса SimpleVector <int> и передаем туда 5, говорим, нам нужен вектор из пяти элементов. Вот такой интерфейс мы хотим реализовать. И давайте приступим. Давайте пока что реализуем пустой деструктор, мы к нему потом вернемся, и сосредоточимся на конструкторе. Смотрите, нам нужно в куче выделить столько объектов, сколько у нас хранится в переменной size. То есть вот в нашем примере 5. Нам нужно в куче выделить память для пяти объектов типа int. Но пока мы умеем только один объект создавать. То есть пока мы можем написать вот так: data = new T. T — это тип наших элементов, и мы можем выделить в куче память для хранения одного объекта типа T и присвоить его указателю data. Но нам нужно не один, нам нужно size — столько, сколько просят. Как это сделать? Это делается с помощью специальной отдельной формы оператора new, который называется new[]. И вот в этих квадратных скобках мы указываем size. То есть сколько объектов типа T мы хотим разместить в куче. Давайте мы это скомпилируем. Вот оно компилируется. На что здесь сразу стоит обратить внимание? Что new[] возвращает точно такой же указатель, как и обычный new. То есть у нас было вот так, это компилировалось; и мы теперь добавили size, это также компилируется. Это первая вещь. Вторая вещь. Оператор new[] выделяет один непрерывный блок памяти для хранения такого количества объектов, какое от него попросили. Ну давайте мы... Значит, мы этот свой код уже скомпилировали, давайте его запустим. Вот он запустился, отработал, и все нормально. Хорошо. Но сейчас, как мы с вами уже знаем, в нашей программе есть утечка памяти. Потому что мы ее выделяем, но нигде не освобождаем. Где нам эту память надо освобождать? Конечно, в деструкторе, когда наш вектор разрушается, его содержимое уже никому не нужно, память надо освободить. Мы с вами пока умеем удалять указатели с помощью оператора delete. Простого delete. Но delete — это оператор, парный к обычному new. А здесь — особый new с квадратными скобочками. Так вот у него и свой собственный delete тоже с квадратными скобочками. Вот так вот он записывается. Давайте мы это скомпилируем. Запустим. Вот у нас все работает. Хорошо. На что здесь надо обратить внимание в случае с оператором delete[]? Во-первых, вы можете видеть, что мы здесь не указываем размер. То есть вот здесь, когда мы создавали, мы в квадратных скобках размер указали, а здесь мы просто говорим delete[]. И все. Размер указывать не нужно. И точно так же delete[] принимает указатель. Указатель на память, а не какой-то особый тип. То есть это тоже обычный указатель на T. При этом если мы все-таки вместо delete[] напишем простой delete, наша программа скомпилируется. И даже отработает. Вот у меня программа отработала и корректно завершилась. Вот только корректно отработает она не всегда. Например, мы заменим int на string. Запустим нашу программу, и она завершится с ошибкой. Что-то пошло не так. Если же мы заменим, поставим здесь правильный оператор delete, delete с квадратными скобками, снова запустим нашу программу, то все прекрасно отработает. Никаких проблем не возникнет. Почему так? Почему нам нужно обязательно следить за тем, чтобы оператор... Чтобы указатель, созданный с помощи new[], удалялся с помощью delete[], и наоборот. Дело здесь вот в чем. Оператор new[], как мы уже сказали, возвращает точно такой же указатель, как и в случае обычного оператора new. Соответственно, этот указатель ничего не знает о том, сколько объектов было создано. И эту информацию приходится размещать где-то в выделенной памяти. При этом, как эта информация размещается, никак не диктуется стандартом, то есть это зависит от конкретного компилятора, конкретной реализации. Это никак не задано жестко, это решают разработчики конкретного компилятора. И поэтому оператор delete и delete[] тоже работают по-разному. Обычный delete получает указатель. Он по этому указателю идет в кучу. Указатель у нас типизированный, то есть мы знаем, на какой тип этот указатель указывает. Он идет в кучу, вызывает деструктор у этого объекта и освобождает вот столько памяти. Собственно, столько памяти, сколько занимает тип нашего указателя. Как работает new[] и соответствующий delete[]? Когда мы выделяем память с помощью new[], выделяется чуть больше памяти, чем нужно для хранения в данном случае трех объектов типа string, и в эту дополнительную память помещается, собственно, их количество. В данном случае 3. Тогда delete[] берет указатель data, в данном случае, и от этого указателя отступает в нужную сторону. То есть у нас data указывает вот сюда на первый объект типа string, вот этой стрелкой демонстрируется. Оператор delete[] отступает, видит, сколько объектов надо удалить, потом идет по памяти, вызывает правильное, нужное количество деструкторов и удаляет весь блок памяти. Весь вот этот вот блок выделенной памяти он удаляет. Если же мы для вот этого указателя вызовем простой delete, то что он сделает? Он пойдет вот в эту ячейку, вызовет мне деструктор, и попытается удалить вот эту память. Только вот этот маленький кусочек, а не весь блок. И это может привести к падению программы. А может и не привести. Мы только что видели, что для int'ов все вроде нормально отработало, а для вектора строк уже произошла ошибка. Поэтому давайте подытожим. В этом видео мы узнали, что для создания в куче n объектов типа T нужно использовать специальную форму оператора new, который записывается как new T [N] — количество объектов. Для освобождения объектов, созданных с помощью new[], есть специальный оператор delete[]. Если вместо delete[] вызывать простой delete, то программа может работать некорректно.