Операции создания и удаления, так как выделенная память должна быть освобождена после ее использования

Обновлено: 02.07.2024

В этом руководстве мы научимся эффективно управлять памятью в C++, используя операции создания и удаления с помощью примеров.

C++ позволяет нам выделять память для переменной или массива во время выполнения. Это известно как динамическое выделение памяти.

В других языках программирования, таких как Java и Python, компилятор автоматически управляет памятью, выделенной для переменных. Но это не так в C++.

В C++ нам нужно вручную освободить динамически выделенную память после того, как мы перестанем использовать переменную.

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

Новый оператор C++

Операция new выделяет память для переменной. Например,

Здесь мы динамически выделили память для переменной int с помощью оператора new.

Обратите внимание, что мы использовали указатель pointVar для динамического выделения памяти. Это связано с тем, что оператор new возвращает адрес ячейки памяти.

В случае массива оператор new возвращает адрес первого элемента массива.

Из приведенного выше примера видно, что синтаксис использования оператора new следующий:

удалить оператора

Как только нам больше не нужно использовать переменную, которую мы объявили динамически, мы можем освободить память, занимаемую этой переменной.

Для этого используется оператор удаления. Он возвращает память операционной системе. Это называется освобождением памяти.

Синтаксис этого оператора

Рассмотрите код:

Здесь мы динамически выделили память для переменной типа int с помощью указателя pointVar .

После печати содержимого pointVar мы освободили память с помощью удаления.

Примечание. Если программа использует большой объем ненужной памяти с помощью new , может произойти сбой системы, поскольку для операционной системы не будет доступной памяти. В этом случае оператор удаления может спасти систему от сбоя.

Пример 1. Динамическое выделение памяти C++

Вывод

В этой программе мы динамически выделяли память для двух переменных типов int и float. После присвоения им значений и вывода их на печать мы, наконец, освобождаем память с помощью кода

Примечание. Динамическое выделение памяти может повысить эффективность управления памятью.

Особенно для массивов, где во многих случаях мы не знаем размер массива до момента выполнения.

Пример 2. Оператор создания и удаления C++ для массивов

Вывод

В этой программе мы попросили пользователя ввести количество студентов и сохранить его в переменной num.

Затем мы динамически выделили память для массива с плавающей запятой, используя new .

Мы вводим данные в массив (а затем печатаем их), используя нотацию указателя.

После того, как массив нам больше не нужен, мы освобождаем память массива с помощью кода delete[] ptr; .

Обратите внимание на использование [] после удаления. Мы используем квадратные скобки [], чтобы обозначить, что освобождение памяти происходит из массива.

Чем это отличается от памяти, выделенной для обычных переменных?
Для обычных переменных, таких как «int a», «char str[10]» и т. д., память автоматически выделяется и освобождается. Для динамически выделяемой памяти, такой как «int *p = new int[10]», программисты несут ответственность за освобождение памяти, когда она больше не нужна. Если программист не освобождает память, это вызывает утечку памяти (память не освобождается, пока программа не завершится).
Как выделяется/освобождается память в C++?
C использует функции malloc() и calloc() для динамического выделения памяти во время выполнения и использует функцию free() для освобождения динамически выделяемой памяти. C++ поддерживает эти функции, а также имеет два оператора new и delete, которые лучше и проще выполняют задачу выделения и освобождения памяти.
Эта статья посвящена операторам new и delete.

новый оператор

Оператор new обозначает запрос на выделение памяти в Free Store. Если доступно достаточно памяти, оператор new инициализирует память и возвращает адрес вновь выделенной и инициализированной памяти в переменную-указатель.

  • Синтаксис для использования оператора new: для выделения памяти любого типа данных используется следующий синтаксис:
  • Здесь переменная-указатель — это указатель типа data-type. Тип данных может быть любым встроенным типом данных, включая массив, или любым пользовательским типом данных, включая структуру и класс.
    Пример:
  • Инициализировать память. Мы также можем инициализировать память для встроенных типов данных с помощью оператора new. Для пользовательских типов данных требуется конструктор (с типом данных в качестве входных данных) для инициализации значения. Вот пример инициализации обоих типов данных:
  • Выделить блок памяти: оператор new также используется для выделения блока (массива) памяти типа data-type.
  • где размер (переменная) указывает количество элементов в массиве.
  • Динамически выделяет память для 10 целых чисел непрерывного типа int и возвращает указатель на первый элемент последовательности, который назначен p(указатель). p[0] относится к первому элементу, p[1] относится ко второму элементу и так далее.


Обычное объявление массива и использование new
Есть разница между объявлением обычного массива и выделением блока памяти с помощью new. Самое важное отличие состоит в том, что обычные массивы освобождаются компилятором (если массив локальный, то освобождается, когда функция возвращается или завершается). Однако динамически выделяемые массивы всегда остаются там до тех пор, пока они не будут освобождены программистом или программа не завершится.
Что делать, если во время выполнения недостаточно памяти?
Если в куче недостаточно памяти для выделения, новый запрос указывает на сбой, вызывая исключение типа std::bad_alloc, если с оператором new не используется «nothrow», и в этом случае он возвращает NULL указатель (перейдите к разделу «Обработка исключений нового оператора» в этой статье). Следовательно, может быть хорошей идеей проверить переменную-указатель, созданную новой, перед использованием ее программы.

удалить оператор

Поскольку ответственность за освобождение динамически выделенной памяти лежит на программисте, программистам предоставляется оператор удаления на языке C++.
Синтаксис:

Здесь переменная-указатель — это указатель, указывающий на объект данных, созданный new.
Примеры:

Чтобы освободить динамически выделенный массив, на который указывает переменная-указатель, используйте следующую форму delete:

C++ поддерживает динамическое выделение и освобождение объектов с помощью операторов new и delete. Эти операторы выделяют память для объектов из пула, называемого свободным хранилищем. Оператор new вызывает оператор специальной функции new , а оператор удаления вызывает оператор специальной функции delete .

Новая функция в стандартной библиотеке C++ поддерживает поведение, указанное в стандарте C++, которое заключается в создании исключения std::bad_alloc в случае сбоя выделения памяти. Если вам по-прежнему нужна версия new без генерации, свяжите свою программу с nothrownew.obj. Однако при связывании с nothrownew.obj оператор new по умолчанию в стандартной библиотеке C++ больше не работает.

Список файлов библиотеки в библиотеке времени выполнения C и стандартной библиотеке C++ см. в разделе Возможности библиотеки CRT.

Новый оператор

Компилятор переводит такой оператор в вызов оператора функции new :

Если запрашивается ноль байтов памяти, оператор new возвращает указатель на отдельный объект. То есть повторные вызовы оператора new возвращают разные указатели. Если памяти недостаточно для запроса выделения, оператор new выдает исключение std::bad_alloc. Или он возвращает nullptr, если вы связались с новой поддержкой оператора, не вызывающего создание.

Вы можете написать процедуру, которая пытается освободить память и повторяет выделение. Для получения дополнительной информации см. _set_new_handler. Подробнее о схеме восстановления см. в разделе Обработка нехватки памяти.

Две области действия новых функций оператора описаны в следующей таблице.

Объем новых функций оператора

< /таблица>

Первый аргумент оператора new должен иметь тип size_t , определенный в , а возвращаемый тип всегда равен void* .

Глобальный оператор new вызывается, когда оператор new используется для размещения объектов встроенных типов, объектов типа класса, не содержащих определяемых пользователем функций оператора new, и массивов любого типа. Когда оператор new используется для размещения объектов типа класса, в котором определен оператор new, вызывается оператор new этого класса.

Функция оператора new, определенная для класса, является статической функцией-членом (которая не может быть виртуальной), которая скрывает глобальную функцию оператора new для объектов этого типа класса. Рассмотрим случай, когда new используется для выделения и установки памяти на заданное значение:

Аргумент, указанный в скобках для new, передается в Blanks::operator new в качестве аргумента chInit. Однако функция new глобального оператора скрыта, в результате чего следующий код генерирует ошибку:

Компилятор поддерживает операторы new и delete массива членов в объявлении класса. Например:

Обработка нехватки памяти

Есть еще один способ обработки неудачных запросов на выделение памяти. Напишите собственную процедуру восстановления для обработки такого сбоя, а затем зарегистрируйте свою функцию, вызвав функцию времени выполнения _set_new_handler.

Оператор удаления

Память, которая динамически выделяется с помощью оператора new, может быть освобождена с помощью оператора удаления. Оператор удаления вызывает функцию удаления оператора, которая освобождает память обратно в доступный пул. Использование оператора удаления также вызывает вызов деструктора класса (если он существует).

Существуют глобальные и классовые операторные функции удаления. Для данного класса может быть определена только одна функция удаления оператора; если он определен, он скрывает функцию удаления глобального оператора. Функция удаления глобального оператора всегда вызывается для массивов любого типа.

Функция удаления глобального оператора. Существуют две формы для функций удаления глобального оператора и оператора удаления члена класса:

Для данного класса может присутствовать только одна из двух предыдущих форм. Первая форма принимает единственный аргумент типа void *, который содержит указатель на объект, который необходимо освободить. Вторая форма, размер освобождения, принимает два аргумента: первый — это указатель на блок памяти, который нужно освободить, а второй — количество байтов, которые необходимо освободить. Тип возвращаемого значения обеих форм — void (оператор delete не может возвращать значение).

Вторая форма предназначена для ускорения поиска нужной категории размера удаляемого объекта. Эта информация часто не хранится рядом с самим выделением и, вероятно, не кэшируется. Вторая форма полезна, когда функция удаления оператора из базового класса используется для удаления объекта производного класса.

Функция удаления оператора является статической, поэтому она не может быть виртуальной. Функция удаления оператора подчиняется управлению доступом, как описано в разделе Управление доступом к участникам.

В следующем примере показаны пользовательские операторы new и операторы delete, предназначенные для регистрации выделения и освобождения памяти:

Предыдущий код можно использовать для обнаружения "утечки памяти", то есть памяти, которая выделена в свободном хранилище, но никогда не освобождалась. Для обнаружения утечек глобальные операторы new и delete переопределены для подсчета выделения и освобождения памяти.

Компилятор поддерживает операторы new и delete массива членов в объявлении класса. Например:

В большинстве случаев это нормально. Однако вы столкнетесь с ситуациями, когда одно или оба из этих ограничений вызывают проблемы, обычно при работе с внешним (пользовательским или файловым) вводом.

Например, мы можем захотеть использовать строку для хранения чьего-то имени, но мы не знаем, какой длины это имя, пока он его не введет. Или мы можем захотеть прочитать некоторое количество записей с диска, но мы не знаем заранее, сколько там записей. Или мы можем создавать игру с переменным количеством монстров (которое меняется со временем, когда некоторые монстры умирают и появляются новые), пытающихся убить игрока.

Если нам нужно объявить размер всего во время компиляции, лучшее, что мы можем сделать, это попытаться угадать максимальный размер переменных, которые нам понадобятся, и надеяться, что этого достаточно:

Это плохое решение как минимум по четырем причинам:

Во-первых, это приводит к напрасной трате памяти, если переменные на самом деле не используются. Например, если мы выделяем 25 символов для каждого имени, но имена в среднем имеют длину всего 12 символов, мы используем в два раза больше, чем нам действительно нужно. Или рассмотрите массив рендеринга выше: если рендеринг использует только 10 000 полигонов, у нас не используется память на 20 000 полигонов!

Во-вторых, как узнать, какие биты памяти фактически используются? Со строками все просто: строка, начинающаяся с \0, явно не используется. А как же монстр[24]? Он сейчас жив или мертв? Это требует какого-то способа отличить активные элементы от неактивных, что усложняет процесс и может занимать дополнительную память.

В-третьих, большинство обычных переменных (включая фиксированные массивы) размещаются в части памяти, называемой стеком. Объем памяти стека для программы, как правило, довольно мал — Visual Studio по умолчанию устанавливает размер стека равным 1 МБ. Если вы превысите это число, произойдет переполнение стека, и операционная система, вероятно, закроет программу.

В Visual Studio это происходит при запуске этой программы:

Ограничение всего 1 МБ памяти было бы проблематичным для многих программ, особенно для тех, которые работают с графикой.

В-четвертых, и это самое главное, это может привести к искусственным ограничениям и/или переполнениям массива. Что происходит, когда пользователь пытается прочитать 600 записей с диска, но мы выделили память только для максимум 500 записей? Либо мы должны сообщить пользователю об ошибке, прочитать только 500 записей, либо (в худшем случае, когда мы вообще не обрабатываем этот случай) переполнить массив записей и наблюдать, как происходит что-то плохое.

К счастью, эти проблемы легко решаются с помощью динамического выделения памяти.Динамическое выделение памяти — это способ запуска программ запрашивать память у операционной системы, когда это необходимо. Эта память не берется из ограниченной памяти стека программы — вместо этого она выделяется из гораздо большего пула памяти, управляемого операционной системой, который называется кучей. На современных машинах размер кучи может достигать гигабайт.

Динамическое размещение отдельных переменных

Чтобы динамически выделить одну переменную, мы используем скалярную (не массивную) форму оператора new:

В приведенном выше случае мы запрашиваем целое число памяти у операционной системы. Новый оператор создает объект, используя эту память, а затем возвращает указатель, содержащий адрес выделенной памяти.

Чаще всего мы присваиваем возвращаемое значение нашей собственной переменной-указателю, чтобы иметь доступ к выделенной памяти позже.

Затем мы можем выполнить косвенное обращение через указатель для доступа к памяти:

Если раньше этого не было, то теперь должно быть ясно хотя бы одно дело, в котором указатели полезны. Без указателя на адрес только что выделенной памяти у нас не было бы возможности получить доступ к памяти, которая была только что выделена для нас!

Как работает динамическое выделение памяти?

На вашем компьютере есть память (вероятно, большая ее часть), доступная для использования приложениями. Когда вы запускаете приложение, ваша операционная система загружает приложение в часть этой памяти. Эта память, используемая вашим приложением, разделена на разные области, каждая из которых служит своей цели. Одна область содержит ваш код. Другая область используется для обычных операций (отслеживание того, какие функции были вызваны, создание и уничтожение глобальных и локальных переменных и т. д.). Мы поговорим о них позже. Однако большая часть доступной памяти просто находится там, ожидая передачи программам, которые ее запрашивают.

Когда вы динамически выделяете память, вы просите операционную систему зарезервировать часть этой памяти для использования вашей программой. Если он сможет выполнить этот запрос, он вернет адрес этой памяти вашему приложению. С этого момента ваше приложение может использовать эту память по своему усмотрению. Когда ваше приложение закончит работу с памятью, оно может вернуть память обратно в операционную систему, чтобы передать ее другой программе.

В отличие от статической или автоматической памяти, сама программа отвечает за запрос и удаление динамически выделяемой памяти.

Инициализация динамически выделяемой переменной

Когда вы динамически выделяете переменную, вы также можете инициализировать ее с помощью прямой инициализации или универсальной инициализации:

Удаление отдельных переменных

Когда мы закончим с динамически выделяемой переменной, нам нужно явно указать C++, чтобы освободить память для повторного использования. Для одиночных переменных это делается с помощью скалярной (не массивной) формы оператора удаления:

Что значит удалить память?

Операция удаления фактически ничего не удаляет. Он просто возвращает память, на которую указывает, операционной системе. Затем операционная система может переназначить эту память другому приложению (или этому приложению позже).

Хотя кажется, что мы удаляем переменную, это не так! Переменная-указатель по-прежнему имеет ту же область действия, что и раньше, и ей можно присвоить новое значение, как и любой другой переменной.

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

Висячие указатели

C++ не дает никаких гарантий относительно того, что произойдет с содержимым освобожденной памяти или со значением удаляемого указателя. В большинстве случаев память, возвращенная операционной системе, будет содержать те же значения, что и до ее возврата, а указатель останется указывать на уже освобожденную память.

Указатель, указывающий на освобожденную память, называется висячим указателем. Косвенное прохождение или удаление висячего указателя приведет к неопределенному поведению. Рассмотрим следующую программу:

В приведенной выше программе значение 7, которое ранее было присвоено выделенной памяти, вероятно, все еще будет там, но возможно, что значение по этому адресу памяти могло измениться. Также возможно, что память может быть выделена для другого приложения (или для собственного использования операционной системой), и попытка доступа к этой памяти приведет к закрытию программы операционной системой.

Освобождение памяти может создать несколько оборванных указателей. Рассмотрим следующий пример:

В этом могут помочь несколько рекомендаций.

Во-первых, старайтесь избегать того, чтобы несколько указателей указывали на один и тот же фрагмент динамической памяти. Если это невозможно, четко определите, какой указатель «владеет» памятью (и отвечает за ее удаление), а какие просто обращаются к ней.

Во-вторых, при удалении указателя, если этот указатель не выходит за пределы области действия сразу же после этого, установите для указателя значение nullptr. Чуть позже мы еще поговорим о нулевых указателях и о том, чем они полезны.

Установите для удаленных указателей значение nullptr, если только они не выходят за пределы области видимости сразу после этого.

Новый оператор может дать сбой

При запросе памяти у операционной системы в редких случаях у операционной системы может не быть памяти для предоставления запроса.

По умолчанию при сбое нового создается исключение bad_alloc. Если это исключение не будет обработано должным образом (а оно не будет обработано, поскольку мы еще не рассмотрели исключения или обработку исключений), программа просто завершится (вылетит) с ошибкой необработанного исключения.

Во многих случаях создание исключения new (или сбой вашей программы) нежелательно, поэтому существует альтернативная форма new, которую можно использовать вместо этого, чтобы указать new вернуть нулевой указатель, если память не может быть выделена. Это делается путем добавления константы std::nothrow между новым ключевым словом и типом распределения:

В приведенном выше примере, если new не удалось выделить память, он вернет нулевой указатель вместо адреса выделенной памяти.

Обратите внимание, что если вы затем попытаетесь косвенно использовать этот указатель, это приведет к неопределенному поведению (скорее всего, ваша программа выйдет из строя). Следовательно, перед использованием выделенной памяти рекомендуется проверять все запросы к памяти, чтобы убедиться, что они действительно выполнены успешно.

Поскольку запрос нового объема памяти дает сбой редко (и почти никогда в среде разработки), эту проверку часто забывают!

Нулевые указатели и динамическое выделение памяти

Нулевые указатели (указатели, установленные в nullptr) особенно полезны при работе с динамическим выделением памяти. В контексте динамического выделения памяти нулевой указатель в основном говорит, что «память для этого указателя не выделена». Это позволяет нам делать такие вещи, как условное выделение памяти:

Удаление нулевого указателя не влияет. Таким образом, нет необходимости в следующем:

Вместо этого вы можете просто написать:

Если ptr не равен нулю, динамически выделяемая переменная будет удалена. Если он равен нулю, ничего не произойдет.

Утечки памяти

Динамически выделенная память остается выделенной до тех пор, пока она не будет явно освобождена или пока программа не завершится (и операционная система очистит ее, если ваша операционная система сделает это). Однако указатели, используемые для хранения динамически выделяемых адресов памяти, подчиняются обычным правилам области видимости для локальных переменных. Это несоответствие может создать интересные проблемы.

Рассмотрите следующую функцию:

Эта функция динамически выделяет целое число, но никогда не освобождает его с помощью удаления. Поскольку переменные-указатели — это обычные переменные, когда функция завершится, ptr выйдет за пределы области видимости. А поскольку ptr — единственная переменная, содержащая адрес динамически выделенного целого числа, при уничтожении ptr больше не остается ссылок на динамически выделенную память. Это означает, что программа «потеряла» адрес динамически выделенной памяти. В результате это динамически выделенное целое число не может быть удалено.

Это называется утечкой памяти. Утечки памяти происходят, когда ваша программа теряет адрес некоторого бита динамически выделенной памяти, прежде чем вернуть ее операционной системе. Когда это происходит, ваша программа не может удалить динамически выделенную память, потому что она больше не знает, где она находится. Операционная система также не может использовать эту память, поскольку считается, что эта память все еще используется вашей программой.

Утечки памяти поглощают свободную память во время работы программы, делая меньше памяти доступной не только для этой программы, но и для других программ. Программы с серьезными проблемами утечки памяти могут потреблять всю доступную память, что приводит к замедлению работы всей машины или даже к сбою. Только после завершения вашей программы операционная система сможет очистить и «восстановить» всю утечку памяти.

Несмотря на то, что утечка памяти может быть вызвана выходом указателя за пределы области видимости, существуют и другие причины утечки памяти. Например, утечка памяти может произойти, если указателю, содержащему адрес динамически выделенной памяти, будет присвоено другое значение:

Это можно исправить, удалив указатель перед его повторным назначением:

Кстати, утечка памяти также возможна через двойное выделение:

Адрес, возвращенный из второго распределения, перезаписывает адрес первого распределения. Следовательно, первое выделение становится утечкой памяти!

Аналогичным образом этого можно избежать, удалив указатель перед повторным назначением.

Заключение

Операторы new и delete позволяют нам динамически выделять отдельные переменные для наших программ.

Динамически выделяемая память имеет динамическую продолжительность и останется выделенной до тех пор, пока вы не освободите ее или программа не завершится.

Будьте осторожны, чтобы не выполнять косвенное обращение с помощью висячих или нулевых указателей.

В следующем уроке мы рассмотрим использование операторов new и delete для выделения и удаления массивов.

Предположим, я выделил немного памяти для хранения значения типа int, например:

Здесь я создал необходимую память с помощью оператора new и назначил адрес этого блока памяти, чтобы иметь доступ к этому блоку памяти.

Теперь я могу контролировать, что я храню в этом блоке памяти.

Но когда я пишу подобное заявление:

мы говорим, что я удалил динамически выделенную память.

Но если я действительно удалил или освободил эту память, не должен ли я больше получить доступ к этой памяти после операции удаления? Но я смог получить доступ к этому блоку памяти, используя ту же переменную-указатель. Итак, в чем была цель удаления блока памяти, если мы можем получить доступ к блоку после его удаления?

Вот пример кода:

Что означает выражение "удалить/освободить динамически выделяемую память" в этом контексте?

Объявляется указатель типа int, затем этот указатель делается так, чтобы он указывал на вновь выделенное место в памяти, имеющее целое число. эта ячейка памяти, которая содержит целое число, была выделена динамически как используемое ключевое слово «новое». Оператор «delete p» не удаляет сам указатель, НО освобождает память, выделенную «new». Теперь указатель p все еще существует, но теперь не гарантируется, что он все еще указывает на ту же ячейку памяти. Он может указывать на один и тот же адрес ИЛИ, следовательно, на неопределенное поведение. указатель p теперь является висячим указателем. Рекомендуется после удаления указателя присвоить ему значение nullptr.

@StoryTeller Не уверен, что аналогия работает, потому что у вас нет шансов сломать носы другим игрокам; или сделать небольшой прокол мяча, который, как вы поймете, вызывает проблемы только через час, когда вы подумаете, что это действительно важно

5 ответов 5

Другие ответы о том, что разыменование указателя после удаления является неопределенным поведением, верны. Но я думаю, что полезно получить некоторое представление о том, что происходит, вместо того, чтобы просто говорить «все может случиться».

При удалении указателя программа выполняет несколько действий:

  1. Вызов деструктора объекта, на который указывает указатель
  2. Вызов функции диспетчера памяти, которая выполнит все операции, необходимые для освобождения памяти.

Второй шаг, в зависимости от реализации, может фактически вернуть блок памяти в ОС, или просто пометить, что блок доступен для последующего выделения, или сделать что-то еще. В вашем случае, видимо, блок просто помечается как свободный, но не возвращается в ОС. Дальнейшее выделение может вернуть адрес в этом блоке, и тогда неопределенное поведение, скорее всего, изменится с "работает нормально" на "работает странно".

Но даже если вы все еще можете получить доступ к объекту через удаленный вами указатель, компилятор все равно может предположить, что объект больше не существует, и выполнить некоторые оптимизации, основанные на этом. Это может нарушить вашу программу очень неожиданным и, казалось бы, нелогичным образом, поэтому вам лучше избегать неопределенного поведения.

Дальнейшее выделение может вернуть адрес в этом блоке. Это самый важный момент, о котором следует помнить. :)

Когда вы выделили эту память для своего указателя, это место в памяти стало вашим, то есть другие приложения не могли его переопределить. Теперь, когда вы освободили это место, его можно было использовать для других приложений, поэтому ваш текущий код больше не имеет эксклюзивности, ваши значимые данные стали случайными данными с точки зрения другого приложения. Вполне логично, что фактическое значение не изменяется, так как с точки зрения вашего приложения это потребовало бы дополнительной работы с фрагментом памяти, о котором оно не заботится. Что касается достижимости адреса, то, по сути, вы освободили эту память, поэтому другие приложения, в том числе и это, доберутся до нее, так что неудивительно, что вы можете до нее добраться.

Это объяснение имеет смысл только в ОС без защиты памяти, например. DOS, где ваше приложение, например. TSR, а "другие приложения" – любое приложение, запускаемое впоследствии.

@Руслан, мы говорим о неопределенном поведении. Этот адрес по-прежнему можно использовать, хотя у приложения больше нет исключительных прав на него.

Да, я имею в виду, что ваше объяснение о том, что «другие приложения могут использовать этот адрес, к которому у вас все еще есть доступ после удаления», не применимо к современным ОС, которые 1) обычно не разделяют кучу и 2) делают одинаковые виртуальные адреса относятся к разным физическим адресам.

Когда я освободил память, если в это время другая программа выделяет эту память, могу ли я по-прежнему иметь доступ к блоку памяти из моей программы. Будет ли это выдавать ошибку или поведение будет неопределенным

@S.M.TusharIbneSalam Я не уверен в этом на 100%, но, по моему мнению, это должно означать, что ваше текущее приложение не будет иметь доступа к этому местоположению.

Но если мы действительно удалим или освободим эту память, мы не сможем получить доступ к этой памяти после операции удаления.

Удаление указателя освобождает память, на которую он указывает. Это цель удаления. Вы несете ответственность за то, чтобы не обращаться к удаленной памяти, а не к компиляторам. Если ваша программа обращается к удаленной памяти, то в соответствии со стандартом C++ ее поведение не определено, что означает именно то, что она говорит.

После удаления p вы получаете доступ к *p . delete p завершает время жизни объекта, и вы получаете доступ к объекту после того, как его время жизни закончилось. Это неопределенное поведение. Когда ваша программа содержит неопределенное поведение, все ставки сняты — нет никакой гарантии, что программа будет делать. «Работает нормально» — один из примеров «нет гарантии». Еще одним примером являются носовые демоны. Фактически, одна версия GCC (1.17) пыталась запустить игры NetHack, Rogue и Towers of Hanoi при обнаружении определенных видов неопределенного поведения. [1] Так что на это можно не рассчитывать.

Неопределенное поведение — это один из тех аспектов языка C и C++, который может удивить программистов, пришедших с других языков (другие языки стараются скрыть это лучше). В принципе, можно писать программы на C++, которые ведут себя непредсказуемым образом, даже несмотря на то, что многие компиляторы C++ не сообщат об ошибках в программе!

[. ]

Это как раз проблема с неопределенным поведением. По сути, стандарт допускает все, что угодно, когда вы вызываете неопределенное поведение (даже гнусавых демонов). Если существует «правильное» поведение в соответствии с вашей ментальной моделью языка, эта модель просто неверна; Только стандарт C++ имеет право голоса, и точка.

Сразу после вызова функции удаления ваша программа записывает несколько символов в стандартный вывод. Возможно, просто возможно, чтобы выполнить эту операцию write(), системе ввода-вывода C++ пришлось выделить несколько байтов, поэтому она запросила несколько байтов у системы управления памятью C++. Не повезло, система памяти отдала системе ввода-вывода именно эту маленькую область, которая только что стала доступной благодаря вашей операции удаления.

Система ввода-вывода, действуя полностью в рамках своих прав, сохраняет полезный указатель на некоторую вспомогательную структуру в этом маленьком месте. Затем, сохраняя 20 в только что «удаленном» месте, ваша программа разрушает этот полезный указатель. После этого структуры данных системы ввода-вывода повреждены.

Обратите внимание, что аппаратное обеспечение или операционная система никак не могут защитить область памяти от неправильного использования. Это связано с тем, что разрешения на запись в память предоставляются для каждого процесса, а не для каждой функции, а система ввода-вывода C++ является частью того же процесса, что и ваша функция main().

Если позднее система ввода-вывода начнет удалять или изменять файлы без предварительного уведомления, вы не можете жаловаться поставщику компилятора C++, потому что вы записываете данные в память, которая вам больше не принадлежит.

Если ваши программисты склонны к такого рода ошибкам, вы должны настаивать на том, чтобы они писали что-то вроде: "delete p ; p = nullptr;". Таким образом, сбой, вызванный последующим неправильным использованием указателя, происходит немедленно, и его очень легко отладить, в отличие от сбоя (возможно, намного позже), вызванного скрытым повреждением структур данных системы ввода-вывода.

Но дух современного C++ заключается в том, чтобы заменить "необработанные указатели", которые вы здесь используете, объектами, называемыми "умными указателями". Поэтому вам, возможно, придется познакомиться с классами std::shared_ptr и std::unique_ptr. Вот небольшой пример, где вы можете видеть, что числовое значение указателя было сброшено на NULL:

Если позволите, я попытаюсь пошутить над программистом: после того, как функция main запишет 20, состояние вашей программы можно описать как "пока все хорошо". Я не знаю, знакомы ли вы с индустрией финансовых услуг.

Есть классический анекдот о легендарном трейдере с Уолл-Стрит, который совершил несколько очень неудачных сделок с низкокачественными финансовыми инструментами. Итак, торговец решает прыгнуть вниз на улицу с 94-го этажа здания. Достигнув уровня 5-го этажа, он видит секретаршу, которая спрашивает его: «Как дела?». А трейдер отвечает: "Пока все хорошо".

Читайте также:

Оператор Область
::operator new Global
имя-класса ::operator new Класс