Динамический способ выделения памяти

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

malloc

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

Для выделения памяти на куче в си используется функция 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 «подсматривает», сколько памяти необходимо удалить.

Функция 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 чисел меньше.

Пусть есть ряд
1, 4, 4, 6, 7, 8, 9, 11, 12, 11, 15
Тогда если период среднего будет 3, то мы получим ряд
(1+4+4)/3, (4+4+6)/3, (4+6+7)/3, (6+7+8)/3, (7+8+9)/3, (8+9+11)/3, (9+11+12)/3, (11+12+11)/3, (12+11+15)/3
Видно, что сумма находится в «окне», которое скользит по ряду. Вместо того, чтобы каждый раз в цикле находить сумму, можно найти её для первого периода, а затем вычитать из суммы крайнее левое значение предыдущего периода и прибавлять крайнее правое значение следующего.
Будем запрашивать у пользователя числа и период, а затем создадим новый массив и заполним его средними значениями.

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

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

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

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

Источник

BestProg

Динамическое и статическое выделение памяти. Преимущества и недостатки. Выделение памяти для одиночных переменных операторами new и delete . Возможные критические ситуации при выделении памяти. Инициализация при выделении памяти

Содержание

Поиск на других ресурсах:

1. Динамическое и статическое (фиксированное) выделение памяти. Главные различия

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

1. Статическое (фиксированное) выделение памяти. В этом случае память выделяется только один раз во время компиляции. Размер выделенной памяти есть фиксированным и неизменным до конца выполнения программы. Примером такого выделения может служить объявление массива из 10 целых чисел:

2. Динамическое выделение памяти. В этом случае используется комбинация операторов new и delete . Оператор new выделяет память для переменной (массива) в специальной области памяти, которая называется «куча» (heap). Оператор delete освобождает выделенную память. Каждому оператору new должен соответствовать свой оператор delete .

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

Динамическое выделение памяти по сравнению со статическим выделением памяти дает следующие преимущества:

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

Преимущества статического способа выделения памяти:

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

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

3. Как выделить память оператором new для одиночной переменной? Общая форма.

Общая форма выделения памяти для одиночной переменной оператором new имеет следующий вид:

  • ptrName – имя переменной (указателя), которая будет указывать на выделенную память;
  • type – тип переменной. Размер памяти выделяется достаточный для помещения в нее значения переменной данного типа type .
4. Как освободить память, выделенную под одиночную переменную оператором delete ? Общая форма

Если память для переменной выделена оператором new, то после завершения использования переменной, эту память нужно освободить оператором delete . В языке C++ это есть обязательным условием. Если не освободить память, то память останется выделенной (занятой), но использовать ее не сможет ни одна программа. В данном случае произойдет «утечка памяти» (memory leak).

В языках программирования Java, C# освобождать память после выделения не нужно. Этим занимается «сборщик мусора» ( garbage collector ).

Общая форма оператора delete для одиночной переменной:

где ptrName – имя указателя, для которого была раньше выделена память оператором new . После выполнения оператора delete указатель ptrName указывает на произвольный участок памяти, который не является зарезервированным (выделенным).

5. Примеры выделения ( new ) и освобождения ( delete ) памяти для указателей базовых типов

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

Пример 1. Указатель на тип int . Простейший пример

Пример 2. Указатель на тип double

6. Что такое «утечка памяти» ( memory leak )?

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

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

7. Каким образом выделить память оператором new с перехватом критической ситуации, при которой память может не выделиться? Исключительная ситуация bad_alloc . Пример

При использовании оператора new возможна ситуация, когда память не выделится. Память может не выделиться в следующих ситуациях:

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

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

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

8. Выделение памяти для переменной с одновременной инициализацией. Общая форма. Пример

Оператор выделения памяти new для одиночной переменной допускает одновременную инициализацию значением этой переменной.

В общем, выделение памяти для переменной с одновременной инициализацией имеет вид

  • ptrName – имя переменной-указателя, для которой выделяется память;
  • type – тип на который указывает указатель ptrName ;
  • value – значение, которое устанавливается для выделенного участка памяти (значение по указателю).

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

Источник

Оцените статью
Разные способы