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

Обновлено: 21.11.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 и C++ может быть очень удобно выделять и освобождать блоки памяти по мере необходимости. Это, безусловно, стандартная практика для обоих языков и почти неизбежная в C++. Однако обработка такой динамической памяти может быть проблематичной и неэффективной. Для настольных приложений, где память находится в свободном доступе, на эти трудности можно не обращать внимания. Для встроенных приложений, обычно работающих в режиме реального времени, игнорировать проблемы нельзя.

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

Пространства памяти C/C++

Возможно, полезно представить, что память данных в C и C++ разделена на три отдельных пространства:

Статическая память. Здесь находятся переменные, которые определены вне функций. Ключевое слово static обычно не влияет на расположение таких переменных; он определяет их область действия как локальную по отношению к текущему модулю. Переменные, определенные внутри функции, явно объявленные статическими, также хранятся в статической памяти. Обычно статическая память располагается в начале области ОЗУ. Фактическое распределение адресов по переменным выполняется встроенным набором инструментов для разработки программного обеспечения: сотрудничество между компилятором и компоновщиком. Обычно секции программы используются для управления размещением, но более продвинутые методы, такие как мелкозернистое распределение, дают больше контроля. Обычно вся оставшаяся память, которая не используется для статического хранения, используется для создания области динамического хранения, в которой размещаются два других пространства памяти.

Автоматические переменные. Переменные, определенные внутри функции, которые не объявлены статическими, являются автоматическими. Для явного объявления такой переменной есть ключевое слово auto, но оно почти никогда не используется. Автоматические переменные (и параметры функций) обычно хранятся в стеке. Стек обычно размещается с помощью компоновщика. Конец области динамического хранения обычно используется для стека. Оптимизация компилятора может привести к тому, что переменные будут храниться в регистрах часть или все время их существования; это также может быть предложено с помощью регистра ключевого слова.

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

Динамическая память в C

В C динамическая память выделяется из кучи с помощью некоторых стандартных библиотечных функций. Двумя ключевыми функциями динамической памяти являются malloc() и free().

Функция malloc() принимает единственный параметр — размер запрошенной области памяти в байтах. Он возвращает указатель на выделенную память. Если выделение не удается, возвращается NULL. Прототип стандартной библиотечной функции выглядит следующим образом:

void *malloc(size_t size);

Функция free() берет указатель, возвращенный функцией malloc(), и освобождает память. Никаких признаков успеха или неудачи не возвращается. Прототип функции выглядит следующим образом:

void free(void *pointer);

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

целый_массив[10];
мой_массив[3] = 99;

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

инт *указатель;
указатель = malloc(10 * sizeof(int));
*(указатель+3) = 99;

Синтаксис разыменования указателя трудно читать, поэтому можно использовать обычный синтаксис ссылки на массив, поскольку [ и ] — это просто операторы:

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

свободно(указатель);
указатель = NULL;

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

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

Есть еще два варианта функции malloc(): calloc() и realloc().

Функция calloc() в основном выполняет ту же работу, что и malloc(), за исключением того, что она принимает два параметра — количество элементов массива и размер каждого элемента — вместо одного параметра (который является произведением этих двух значения). Выделенная память также инициализируется нулями. Вот прототип:

void *calloc(size_t elements, size_t elementSize);

Функция realloc() изменяет размер памяти, выделенной ранее функцией malloc(). Он принимает в качестве параметров указатель на область памяти и новый требуемый размер. При уменьшении размера данные могут быть потеряны. Если размер увеличен и функция не может расширить существующее выделение, она автоматически выделит новую область памяти и скопирует данные в нее. В любом случае он возвращает указатель на выделенную память. Вот прототип:

void *realloc(void *pointer, size_t size);

Динамическая память в C++

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

p_var = новое имя типа;
p_var = новый тип(инициализатор);
p_array = новый тип [размер];

В первых двух случаях выделяется место для одного объекта; второй включает инициализацию. Третий случай — это механизм выделения места под массив объектов.

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

удалить p_var;
удалить[] p_array;

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

Не существует оператора, обеспечивающего функциональность функции C realloc().

Вот код для динамического выделения массива и инициализации четвертого элемента:

int* указатель;
указатель = новый int[10];
указатель[3] = 99;

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

удалить[] указатель;
указатель = NULL;

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

Вопросы и проблемы

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

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

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

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

Фрагментация памяти

Лучший способ понять фрагментацию памяти — посмотреть на пример. В этом примере предполагается, что существует куча размером 10 КБ. Во-первых, запрашивается область размером 3 КБ, таким образом:

Затем запрашивается еще 4 КБ:

3 КБ памяти теперь свободно.

Некоторое время спустя первое выделение памяти, на которое указывает p1, освобождается:

При этом остается 6 КБ свободной памяти, разбитой на два фрагмента по 3 КБ. Выдается дополнительный запрос на выделение 4K:

Это приводит к сбою — в p1 возвращается NULL — потому что, хотя доступно 6 КБ памяти, нет доступного непрерывного блока размером 4 КБ. Это фрагментация памяти.

Память с RTOS

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

Обычно предоставляются средства управления памятью, совместимые с требованиями реального времени, т. е. детерминированные. Чаще всего это схема, которая выделяет блоки или «разделы» памяти под управлением ОС.

Блокирование/разделение памяти

Обычно выделение блочной памяти выполняется с использованием «пула разделов», который определяется статически или динамически и настраивается таким образом, чтобы содержать определенное количество блоков определенного фиксированного размера. Для ОС Nucleus вызов API для определения пула разделов имеет следующий прототип:

STATUS
NU_Create_Partition_Pool (NU_PAR TITION_POOL *pool, CHAR *name, VOID *start_address, UNSIGNED pool_size, UNSIGNED partition_size, OPTION suspend_type);

Наиболее наглядно это можно понять на примере:

При этом создается пул разделов с дескриптором MyPool, содержащий 2000 байт памяти, заполненный разделами размером 40 байт (т. е. существует 50 разделов). Пул расположен по адресу 0xB000. Пул настроен таким образом, что, если задача пытается выделить блок, когда его нет в наличии, и запрашивает приостановку при вызове API выделения, приостановленные задачи будут разбужены в порядке «первым поступил — первым вышел». . Другим вариантом был бы порядок приоритета задач.

Для запроса выделения раздела доступен другой вызов API. Вот пример использования ОС Nucleus:

Это запрашивает выделение раздела из MyPool. В случае успеха указатель на выделенный блок возвращается в ptr. Если памяти нет, задача приостанавливается, так как указано NU_SUSPEND; другими вариантами, которые могли быть выбраны, были бы приостановка с тайм-аутом или просто возврат с ошибкой.

Когда раздел больше не нужен, его можно освободить следующим образом:

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

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

Обнаружение утечки памяти

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

Решения для оперативной памяти

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

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

Можно использовать выделение памяти разделов для надежной и детерминированной реализации malloc(). Идея состоит в том, чтобы определить серию пулов разделов с размерами блоков в геометрической прогрессии; например 32, 64, 128, 256 байт.Функция malloc() может быть написана для детерминированного выбора правильного пула, чтобы обеспечить достаточно места для данного запроса на выделение. В этом подходе используется детерминированное поведение вызова API выделения разделов, надежная обработка ошибок (например, приостановка задачи) и иммунитет к фрагментации, обеспечиваемый блочной памятью.

Выводы

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

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

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

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

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

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

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

Во-первых, это приводит к напрасной трате памяти, если переменные на самом деле не используются. Например, если мы выделяем 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 для выделения и удаления массивов.

До сих пор вы, вероятно, мало задумывались о том, как работает программа или как она работает. Что ж, большинству из нас известно, что программа — это файл, который загружается в память в какой-то неопределенный момент. Итак, учитывая приведенный ниже код, где хранятся a , b и c?

a , b и c хранятся в стеке, что является причудливым словом для небольшой области памяти, где временные переменные добавляются и удаляются в специальном способ. Все добавляется и удаляется, начиная сверху, как стопка карт. Обычно вы берете карты сверху, а иногда можете положить их сверху; эти операции со стеком называются pop и push соответственно. Таким образом, мы можем использовать дедуктивное рассуждение, чтобы сказать, что в стек помещается a, за которым следуют b и c. Отличительной особенностью распределения памяти в стеке является простота использования. Память выделяется достаточно быстро с минимальными накладными расходами. Когда вы закончите использовать память, она автоматически освободится. Это означает, что нам не нужно управлять памятью.

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

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

Выделение памяти [ редактировать | изменить источник ]

Прежде чем мы начнем с некоторых примеров, важно знать, что все функции выделения памяти хранятся в заголовочном файле stdlib.h. Первая функция, с которой мы будем работать, это malloc, которая отвечает за выделение памяти. Эта функция принимает один параметр — размер выделенной памяти в size_t. size_t — это максимальное число, используемое для адресации памяти на компьютере, поэтому, например, на 32-разрядном компьютере size_t может выражать 4 ГБ.

Примечание. Фактический размер самого типа size_t будет составлять всего 4 байта на 32-разрядном компьютере, а не 4 ГиБ! size_t может, например, хранить адрес второго ГиБ в пределах своих 4 байтов. С рациональной точки зрения можно сказать, что size_t может адресовать любое место в памяти компьютера.

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

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

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

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

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

Предупреждение. Прежде чем вы начнете выделять память как сумасшедшую, вам нужно убедиться, что вы не выделяете «ничего». Это происходит, когда вы пытаетесь что-то вроде malloc(0) , которое не выделяет вам никакой памяти. Хуже того, он может вернуть нулевой указатель, но это не гарантируется. Это означает, что можно получить указатель на память, к которой не следует прикасаться, что, безусловно, поможет при сбое вашей программы.

Освобождение выделенной памяти [ edit | изменить источник ]

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

Предупреждение. Высвобождение незанятого указателя вызовет неопределенное поведение. К счастью, free ничего не сделает, если вы попытаетесь освободить нулевой указатель. Никогда не пытайтесь освободить то, что никогда не выделялось изначально!

Изменение размера выделенной памяти [ edit | изменить источник ]

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

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

Предупреждение. Хотя можно освободить выделенную память, установив размер равным нулю, реализации стандартных библиотек могут отличаться. Следовательно, вы всегда должны использовать free для освобождения памяти.

Выделение чистой памяти [ edit | изменить источник ]

Использование malloc для выделения памяти — это здорово, но у него все еще есть свои недостатки. malloc не беспокоится о состоянии выделенной памяти, а это означает, что каждый байт может иметь значение undefined. Это может быть проблематично с массивом, который мы использовали. При выделении массив может содержать неопределенные значения. Обычно, когда мы выделяем массив, мы хотим, чтобы все было равно нулю. К счастью, последняя функция, обсуждаемая в этом уроке, выделяет память и гарантирует, что вся память будет обнулена. Эта функция называется calloc и принимает два параметра: количество элементов, которые вы хотите выделить, и их размер. calloc демонстрируется в примере ниже.

Вывод должен быть:

Проблемы с управлением памятью [ редактировать | изменить источник ]

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

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

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

  1. Только не используйте их! Это легче сказать, чем сделать, особенно в больших приложениях.
  2. Установите висячий указатель на NULL . Сравнивая с NULL , вы теперь знаете, подходит ли указатель для использования.

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

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

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