Почему при завершении работы с динамической памятью ее необходимо освободить

Обновлено: 07.07.2024

Написанная в GWT, поддерживает все настольные браузеры, iOS и Android. Вам нужно будет предоставить свои собственные плитки (или найти того, кто позволит вам использовать их).

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

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

Однажды я столкнулся с win98 на встроенной платформе и, основываясь на этом опыте, могу сказать, что НЕ освобождает память, когда программы закрываются. – San Jacinto 6 February 2010 в 16:44 @Ken Это был пример. Кроме того, существует линия между YAGNI и неаккуратным кодированием. Это не освобождает ресурсы. Принцип YAGNI также должен применяться к функциям, а не к коду, который делает работу программы корректной. (И не освобождение памяти - это ошибка). – Yacoby 6 February 2010 в 16:53 +1: самое важное, что следует учитывать, - это то, что управление памятью происходит так, как Yacoby вполне корректно заявляет: & quot; особенность операционной системы & quot; . Если я не ошибаюсь, язык программирования не определяет, что происходит до или после выполнения программы. – D.Shawley 6 February 2010 в 16:57 Освобождение памяти вручную занимает больше времени, принимает больше кода и вводит возможность ошибок (скажите, что вы никогда не видели ошибку в коде освобождения!). Это не «небрежный». преднамеренно опустить что-то, что ухудшается во всех отношениях для вашего конкретного случая использования. Если вы не собираетесь запускать его на какой-то древней / крошечной системе, которая не может освобождать страницы после завершения процесса или интегрировать ее в более крупную программу (YAGNI), это выглядит как чистый убыток для меня. Я знаю, что это вредит эго программиста, чтобы думать о том, чтобы не очищать его самостоятельно, но каким практическим способом это на самом деле лучше? – Ken 6 February 2010 в 17:56 Любой, кто предлагает утечку памяти на SO, должен быть лишен всех репутаций и значков – Ulterior 17 May 2012 в 22:38

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

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

Все это сказало, как отмечают все остальные ответчики, полагаясь на это не очень хорошая практика:

  1. Вы всегда должны программировать забота о ресурсах, а также в C, что также означает память. Вы можете вставить свой код в библиотеку, или он может работать намного дольше, чем вы ожидаете.
  2. Некоторые ОС (более старые и, возможно, некоторые современные встроенные) могут не поддерживать такие жесткие границы процесса, и ваши распределения могут повлиять на адресные пространства других пользователей.

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

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

Да, операционная система освобождает всю память, когда процесс завершается.

Я не понимаю, почему это было приостановлено. Память malloc'ed будет выпущена, когда процесс замирает (определение wikipedia malloc так говорит) – Arve 6 February 2010 в 17:13 Википедия не является руководством для каждой ОС. Большинство современных ОС вернут память, но не все (и особенно не все старые). Добавьте к этому, malloc может только пообещать, что C будет делать с памятью; по дизайну, C не гарантирует многое из того, что касается поведения вне самой C. Если приложение неожиданно погибает, любые обещания, сделанные в библиотеке времени выполнения, являются недействительными, так как они больше не живы, чтобы соответствовать им. – cHao 23 July 2012 в 02:20

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

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

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

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

Как пример, существуют высокопроизводительные серверы, которые работают, создавая процесс для каждого запроса, а затем завершая его завершение; таким образом, им даже не нужно отслеживать выделение памяти , и никогда вообще не делать освобождение или сборку мусора, поскольку все просто возвращается в свободную память операционной системы в конце процесса. (То же самое можно сделать в процессе с использованием специализированного распределителя памяти, но требует очень тщательного программирования, по сути дела, создавая собственное понятие «легкие процессы» в процессе ОС.)

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

Решение

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

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

Посмотри на std::shared_ptr а также std::unique_ptr , У последнего нет накладных расходов.

Другие решения

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

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

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

Прежде всего, правило номер один в C ++:

Избегайте динамического распределения, если оно вам действительно не нужно!

Не использовать new легко; даже если это безопасно завернуто std::make_unique или же std::make_shared , Стандартный способ создания экземпляра типа в C ++:

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

Если и только если вам нужно динамически выделить объект, рассмотрите возможность использования std::shared_ptr или же std::unique_ptr , Они будут освобождены автоматически, когда объект больше не нужен.

можно ли пропустить их удаление в конце программы, чтобы
заставить код работать быстрее?

Абсолютно нет, из-за «чтобы заставить код работать быстрее» часть. Это было бы преждевременной оптимизацией.

Это основные моменты.

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

Вот плохая утечка памяти:

Это неплохо, потому что память «потеряна навсегда»; любая удаленно современная операционная система очистит все после того, как процесс завершится (см. ответ Kerrek SB в вашем связанном вопросе).

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

Вот еще одна плохая утечка памяти:

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

Теперь сравните это с:

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

Скорее всего, вы никогда не столкнетесь с ситуацией, когда стоит избегать освобождения. Но будьте осторожны с правилами «всегда» и «никогда» в разработке программного обеспечения, особенно в C ++. Хорошее управление памятью гораздо сложнее, чем сопоставлять каждый new с delete ,

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

Для выделения памяти на куче в си используется функция malloc (memory allocation) из библиотеки stdlib.h

Функция выделяет size байтов памяти и возвращает указатель на неё. Если память выделить не удалось, то функция возвращает NULL. Так как malloc возвращает указатель типа void, то его необходимо явно приводить к нужному нам типу. Например, создадим указатель, после этого выделим память размером в 100 байт.

После того, как мы поработали с памятью, необходимо освободить память функцией free.
Используя указатель, можно работать с выделенной памятью как с массивом. Пример: пользователь вводит число – размер массива, создаём массив этого размера и заполняем его квадратами чисел по порядку. После этого выводим и удаляем массив.

Здесь (int *) – приведение типов. Пишем такой же тип, как и у указателя.
size * sizeof(int) – сколько байт выделить. sizeof(int) – размер одного элемента массива.
После этого работаем с указателем точно также, как и с массивом. В конце не забываем удалять выделенную память.

Теперь представим на рисунке, что у нас происходило. Пусть мы ввели число 5.

Функция malloc выделила память на куче по определённому адресу, после чего вернула его. Теперь указатель p хранит этот адрес и может им пользоваться для работы. В принципе, он может пользоваться и любым другим адресом.
Когда функция malloc "выделяет память", то она резервирует место на куче и возвращает адрес этого участка. У нас будет гарантия, что компьютер не отдаст нашу память кому-то ещё. Когда мы вызываем функцию free, то мы освобождаем память, то есть говорим компьютеру, что эта память может быть использована кем-то другим. Он может использовать нашу память, а может и нет, но теперь у нас уже нет гарантии, что эта память наша. При этом сама переменная не зануляется, она продолжает хранить адрес, которым ранее пользовалась.

Это очень похоже на съём номера в отеле. Мы получаем дубликат ключа от номера, живём в нём, а потом сдаём комнату обратно. Но дубликат ключа у нас остаётся. Всегда можно зайти в этот номер, но в нём уже кто-то может жить. Так что наша обязанность – удалить дубликат.

Иногда думают, что происходит "создание" или "удаление" памяти. На самом деле происходит только перераспределение ресурсов.

Освобождение памяти с помощью free

Т еперь рассмотри, как происходит освобождение памяти. Переменная указатель хранит адрес области памяти, начиная с которого она может им пользоваться. Однако, она не хранит размера этой области. Откуда тогда функция free знает, сколько памяти необходимо освободить?

  • 1. Можно создать карту, в которой будет храниться размер выделенного участка. Каждый раз при освобождении памяти компьютер будет обращаться к этим данным и получать нужную информацию.
  • 2. Второе решение более распространено. Информация о размере хранится на куче до самих данных. Таким образом, при выделении памяти резервируется места больше и туда записывается информация о выделенном участке. При освобождении памяти функция free "подсматривает", сколько памяти необходимо удалить.

Работа с двумерными и многомерными массивами

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

  • 1. Создавать массивы "неправильной формы", то есть массив строк, каждая из которых имеет свой размер.
  • 2. Работать по отдельности с каждой строкой массива: освобождать память или изменять размер строки.

Создадим "треугольный" массив и заполним его значениями

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

calloc

Ф ункция calloc выделяет n объектов размером m и заполняет их нулями. Обычно она используется для выделения памяти под массивы. Синтаксис

realloc

Е щё одна важная функция – realloc (re-allocation). Она позволяет изменить размер ранее выделенной памяти и получает в качестве аргументов старый указатель и новый размер памяти в байтах:

Функция realloc может как использовать ранее выделенный участок памяти, так и новый. При этом не важно, меньше или больше новый размер – менеджер памяти сам решает, где выделять память.
Пример – пользователь вводит слова. Для начала выделяем под слова массив размером 10. Если пользователь ввёл больше слов, то изменяем его размер, чтобы хватило места. Когда пользователь вводит слово end, прекращаем ввод и выводим на печать все слова.

Хочу обратить внимание, что мы при выделении памяти пишем sizeof(char*), потому что размер указателя на char не равен одному байту, как размер переменной типа char.

Ошибки при выделении памяти

1. Бывает ситуация, при которой память не может быть выделена. В этом случае функция malloc (и calloc) возвращает NULL. Поэтому, перед выделением памяти необходимо обнулить указатель, а после выделения проверить, не равен ли он NULL. Так же ведёт себя и realloc. Когда мы используем функцию free проверять на NULL нет необходимости, так как согласно документации free(NULL) не производит никаких действий. Применительно к последнему примеру:

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

2. Изменение указателя, который хранит адрес выделенной области памяти. Как уже упоминалось выше, в выделенной области хранятся данные об объекте - его размер. При удалении free получает эту информацию. Однако, если мы изменили указатель, то удаление приведёт к ошибке, например

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

3. Использование освобождённой области. Почему это работает в си, описано выше. Эта ошибка выливается в другую – так называемые висячие указатели (dangling pointers или wild pointers). Вы удаляете объект, но при этом забываете изменить значение указателя на NULL. В итоге, он хранит адрес области памяти, которой уже нельзя воспользоваться, при этом проверить, валидная эта область или нет, у нас нет возможности.

Эта программа отработает и выведет мусор, или не мусор, или не выведет. Поведение не определено.

Если же мы напишем

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

4. Освобождение освобождённой памяти. Пример

Здесь дважды вызывается free для переменной a. При этом, переменная a продолжает хранить адрес, который может далее быть передан кому-нибудь для использования. Решение здесь такое же как и раньше - обнулить указатель явно после удаления:

5. Одновременная работа с двумя указателями на одну область памяти. Пусть, например, у нас два указателя p1 и p2. Если под первый указатель была выделена память, то второй указатель может запросто скомпрометировать эту область:

Рассмотрим код ещё раз.

Теперь оба указателя хранят один адрес.

А вот здесь происходит непредвиденное. Мы решили выделить под p2 новый участок памяти. realloc гарантирует сохранение контента, но вот сам указатель p1 может перестать быть валидным. Есть разные ситуации. Во-первых, вызов malloc мог выделить много памяти, часть которой не используется. После вызова ничего не поменяется и p1 продолжит оставаться валидным. Если же потребовалось перемещение объекта, то p1 может указывать на невалидный адрес (именно это с большой вероятностью и произойдёт в нашем случае). Тогда p1 выведет мусор (или же произойдёт ошибка, если p1 полезет в недоступную память), в то время как p2 выведет старое содержимое p1. В этом случае поведение не определено.

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

Различные аргументы realloc и malloc.

При вызове функции malloc, realloc и calloc с нулевым размером поведение не определено. Это значит, что может быть возвращён как NULL, так и реальный адрес. Им можно пользоваться, но к нему нельзя применять операцию разадресации.
Вызов realloc(NULL, size_t) эквиваленте вызову malloc(size_t).
Однако, вызов realloc(NULL, 0) не эквивалентен вызову malloc(0) :) Понимайте это, как хотите.

Примеры

1. Простое скользящее среднее равно среднему арифметическому функции за период n. Пусть у нас имеется ряд измерений значения функции. Часто эти измерения из-за погрешности "плавают" или на них присутствуют высокочастотные колебания. Мы хотим сгладить ряд, для того, чтобы избавиться от этих помех, или для того, чтобы выявить общий тренд. Самый простой способ: взять n элементов ряда и получить их среднее арифметическое. n в данном случае - это период простого скользящего среднего. Так как мы берём n элементов для нахождения среднего, то в результирующем массиве будет на n чисел меньше.

Это простой пример. Большая его часть связана со считыванием данных, вычисление среднего всего в девяти строчках.

2. Сортировка двумерного массива. Самый простой способ сортировки - перевести двумерный массив MxN в одномерный размером M*N, после чего отсортировать одномерный массив, а затем заполнить двумерный массив отсортированными данными. Чтобы не тратить место под новый массив, мы поступим по-другому: если проходить по всем элементам массива k от 0 до M*N, то индексы текущего элемента можно найти следующим образом:
j = k / N;
i = k - j*M;
Заполним массив случайными числами и отсортируем

3. Бином Ньютона. Создадим треугольную матрицу и заполним биномиальными коэффициентами

Если Вы желаете изучать этот материал с преподавателем, советую обратиться к репетитору по информатике

email

Всё ещё не понятно? – пиши вопросы на ящик

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

Автоматическое выделение памяти выполняется для параметров функции и локальных переменных. Память выделяется при входе в блок, в котором находятся эти переменные, и удаляется при выходе из него.

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

Динамическое выделение переменных

Как статическое, так и автоматическое распределение памяти имеют два общих свойства:

Размер переменной/массива должен быть известен во время компиляции.

Выделение и освобождение памяти происходит автоматически (когда переменная создается/уничтожается).

В большинстве случаев с этим всё ОК. Однако, когда дело доходит до работы с пользовательским вводом, то эти ограничения могут привести к проблемам.

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

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

char name [ 30 ] ; // будем надеяться, что пользователь введет имя длиной менее 30 символов! Polygon rendering [ 40000 ] ; // этому 3D-рендерингу лучше состоять из менее чем 40000 полигонов!

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

Во-первых, теряется память, если переменные фактически не используются или используются, но не все. Например, если мы выделим 30 символов для каждого имени, но имена в среднем будут занимать по 15 символов, то потребление памяти получится в два раза больше, чем нам нужно на самом деле. Или рассмотрим массив rendering : если он использует только 20 000 полигонов, то память для других 20 000 полигонов фактически тратится впустую (т.е. не используется)!

В Visual Studio это можно проверить, запустив следующий фрагмент кода:

int array [ 1000000000 ] ; // выделяем 1 миллиард целочисленных значений

Лимит в 1МБ памяти может быть проблематичным для многих программ, особенно где используется графика.

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

new int ; // динамически выделяем целочисленную переменную и сразу же отбрасываем результат (так как нигде его не сохраняем)

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

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

int * ptr = new int ; // динамически выделяем целочисленную переменную и присваиваем её адрес ptr, чтобы затем иметь доступ к ней

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

* ptr = 8 ; // присваиваем значение 8 только что выделенной памяти

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

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

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

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

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

Освобождение памяти

Когда вы динамически выделяете переменную, то вы также можете её инициализировать посредством прямой инициализации или uniform-инициализации (в С++11):

int * ptr1 = new int ( 7 ) ; // используем прямую инициализацию int * ptr2 = new int < 8 >; // используем uniform-инициализацию

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

// Предположим, что ptr ранее уже был выделен с помощью оператора new delete ptr ; // возвращаем память, на которую указывал ptr, обратно в операционную систему ptr = 0 ; // делаем ptr нулевым указателем (используйте nullptr вместо 0 в C++11)

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

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

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

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

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

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

int * ptr = new int ; // динамически выделяем целочисленную переменную * ptr = 8 ; // помещаем значение в выделенную ячейку памяти delete ptr ; // возвращаем память обратно в операционную систему, ptr теперь является висячим указателем std :: cout << * ptr ; // разыменование висячего указателя приведет к неожиданным результатам delete ptr ; // попытка освободить память снова приведет к неожиданным результатам также

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

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

int * ptr = new int ; // динамически выделяем целочисленную переменную int * otherPtr = ptr ; // otherPtr теперь указывает на ту же самую выделенную память, что и ptr delete ptr ; // возвращаем память обратно в операционную систему. ptr и otherPtr теперь висячие указатели // Однако, otherPtr по-прежнему является висячим указателем!

Есть несколько рекомендаций, которые могут здесь помочь:

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

Правило: Присваивайте удаленным указателям значение 0 (или nullptr в C++11), если они не выходят из области видимости сразу же после удаления.

Оператор new

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

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

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

int * value = new ( std :: nothrow ) int ; // указатель value станет нулевым, если динамическое выделение целочисленной переменной не выполнится

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

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

int * value = new ( std :: nothrow ) int ; // запрос на выделение динамической памяти для целочисленного значения if ( ! value ) // обрабатываем случай, когда new возвращает null (т.е. память не выделяется)

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

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

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

// Если для ptr до сих пор не выделено памяти, то выделяем её

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

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

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

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

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

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

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

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

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