Очистка динамической памяти c

Обновлено: 06.07.2024

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

Статическая память

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

Существует два типа статических переменных:

  • глобальные переменные - это переменные, определенные вне функций, в описании которых отсутствует слово static . Обычно описания глобальных переменных, включающие слово extern , выносятся в заголовочные файлы (h-файлы). Слово extern означает, что переменная описывается, но не создается в данной точке программы. Определения глобальных переменных, т.е. описания без слова extern , помещаются в файлы реализации (c-файлы или cpp-файлы). Пример: глобальная переменная maxind описывается дважды:
    • в h-файле с помощью строки
    • Статическую переменную можно описать и внутри функции, хотя обычно так никто не делает. Переменная размещается не в стеке, а в статической памяти, т.е. ее нельзя использовать при рекурсии, а ее значение сохраняется между различными входами в функцию. Область видимости такой переменной ограничена телом функции, в которой она определена. В остальном она подобна статической или глобальной переменной. Заметим, что ключевое слово static в языке Си используется для двух различных целей:
      • как указание типа памяти: переменная располагается в статической памяти, а не в стеке;
      • как способ ограничить область видимости переменной рамками одного файла (в случае описания переменной вне функции).

      Стековая, или локальная, память

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

      Локальные переменные можно использовать при рекурсии, поскольку при повторном входе в функцию в стеке создается новый набор локальных переменных, а предыдущий набор не разрушается. По этой же причине локальные переменные безопасны при использовании нитей в параллельном программировании (см. раздел 2.6.2). Программисты называют такое свойство функции реентерабельностью, от англ. re-enter able - возможность повторного входа. Это очень важное качество с точки зрения надежности и безопасности программы! Программа, работающая со статическими переменными, этим свойством не обладает, поэтому для защиты статических переменных приходится использовать механизмы синхронизации (см. 2.6.2), а логика программы резко усложняется. Всегда следует избегать использования глобальных и статических переменных, если можно обойтись локальными.

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

      Динамическая память, или куча

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

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

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

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

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

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

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

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

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

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

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

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

      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 уничтожится, больше не останется указателей на динамически выделенную память. Это означает, что программа «потеряет» адрес динамически выделенной памяти. И в результате эту динамически выделенную целочисленную переменную нельзя будет удалить.

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

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

      Как известно, в языке С для динамического выделения и освобождения памяти используются фун­кции malloc() и free(). Вместе с тем С++ содержит два оператора, выполняющих выделение и освобождение памяти более эффективно и более просто. Этими операторами являются new и delete. Их общая форма имеет вид:

      переменная_указатель = new тип_переменной;

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

      Если операция выделения памяти не может быть выполнена, то оператор new генерирует ис­ключение типа xalloc. Если программа не перехватит это исключение, тогда она будет снята с выполнения. Хотя для коротких программ такое поведение по умолчанию является удовлетвори­тельным, для реальных прикладных программ обычно требуется перехватить исключение и обра­ботать его соответствующим образом. Для того чтобы отследить это исключение, необходимо вклю­чить заголовочный файл except.h.

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

      Есть ряд преимуществ использования new перед использованием malloc(). Во-первых, оператор new автоматически вычисляет размер необходимой памяти. Нет необходимости в использовании оператора sizeof(). Более важно то, что он предотвращает случайное выделение неправильного количества памяти. Во-вторых, оператор new автоматически возвращает указатель требуемого типа, так что нет необходимости в использовании оператора преобразования типа. В-третьих, как ско­ро будет описано, имеется возможность инициализации объекта при использовании оператора new. И наконец, имеется возможность перегрузить оператор new и оператор delete глобально или по отношению к тому классу, который создается.

      Ниже приведен простой пример использования операторов new и delete. Следует обратить вни­мание на использование блока try/catch для отслеживания ошибок выделения памяти.

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

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

      С помощью new можно размещать массивы. Общая форма для одномерного массива имеет вид:

      переменная_указатель = new тип_переменной [размер];

      Здесь размер определяет число элементов в массиве. Необходимо запомнить важное ограничение при размещении массива: его нельзя инициализировать.

      Для освобождения динамически размещенного массива необходимо использовать следующую форму оператора delete:

      Здесь скобки [] информируют оператор delete, что необходимо освободить память, выделенную для массива.

      В следующей программе выделяется память для массива из 10 элементов типа float. Элементам массива присваиваются значения от 100 до 109, а затем содержимое массива выводится на экран:

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

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

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

      new Оператор

      Компилятор преобразует инструкцию, такую как this, в вызов функции operator new :

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

      Можно написать подпрограммы, которая пытается освободить память и повторить выделение памяти. Дополнительные сведения см. на веб-сайте _set_new_handler . Дополнительные сведения о схеме восстановления см. в разделе Обработка нехватки памяти .

      operator new В следующей таблице описаны две области для функций.

      Область действия для operator new функций

      Оператор Область
      ::operator new Глобальный
      имя класса** ::operator new ** Класс

      Первый аргумент для operator new должен иметь тип size_t , определенный в <stddef.h> , а тип возвращаемого значения — Always void* .

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

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

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

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

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

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

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

      delete Оператор

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

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

      Глобальная operator delete функция. Для глобальных operator delete функций и членов класса существуют две формы operator delete :

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

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

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

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

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

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

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