Выделение памяти в Си (функция malloc). Динамическое выделение памяти в си Выделение памяти для программы в с
Последнее обновление: 28.05.2017
При создании массива с фиксированными размерами под него выделяется определенная память. Например, пусть у нас будет массив с пятью элементами:
Double numbers = {1.0, 2.0, 3.0, 4.0, 5.0};
Для такого массива выделяется память 5 * 8 (размер типа double) = 40 байт. Таким образом, мы точно знаем, сколько в массиве элементов и сколько он занимает памяти. Однако это не всегда удобно. Иногда бывает необходимо, чтобы количество элементов и соответственно размер выделяемой памяти для массива определялись динамически в зависимости от некоторых условий. Например, пользователь сам может вводить размер массива. И в этом случае для создания массива мы можем использовать динамическое выделение памяти.
Для управления динамическим выделением памяти используется ряд функций, которые определены в заголовочном файле stdlib.h :
malloc() . Имеет прототип
Void *malloc(unsigned s);
Выделяет память длиной в s байт и возвращает указатель на начало выделенной памяти. В случае неудачного выполнения возвращает NULL
calloc() . Имеет прототип
Void *calloc(unsigned n, unsigned m);
Выделяет память для n элементов по m байт каждый и возвращает указатель на начало выделенной памяти. В случае неудачного выполнения возвращает NULL
realloc() . Имеет прототип
Void *realloc(void *bl, unsigned ns);
Изменяет размер ранее выделенного блока памяти, на начало которого указывает указатель bl, до размера в ns байт. Если указатель bl имеет значение NULL , то есть память не выделялась, то действие функции аналогично действию malloc
free() . Имеет прототип
Void *free(void *bl);
Освобождает ранее выделенный блок памяти, на начало которого указывает указатель bl.
Если мы не используем эту функцию, то динамическая память все равно освободится автоматически при завершении работы программы. Однако все же хорошей практикой является вызов функции free() , который позволяет как можно раньше освободить память.
Рассмотрим применение функций на простой задаче. Длина массива неизвестна и вводится во время выполнения программы пользователем, и также значения всех элементов вводятся пользователем:
#include Консольный вывод программы: Size of array=5
block=23
block=-4
block=0
block=17
block=81
23 -4 0 17 81 Здесь для управления памятью для массива определен указатель block типа int
. Количество элементов массива заранее неизвестно,
оно представлено переменной n. Вначале пользователь вводит количество элементов, которое попадает в переменную n. После этого необходимо выделить память для данного количества
элементов. Для выделения памяти здесь мы могли бы воспользоваться любой из трех вышеописанных функций: malloc, calloc, realloc.
Но конкретно в данной ситуации воспользуемся функцией malloc
: Block = malloc(n * sizeof(int));
Прежде всего надо отметить, что все три выше упомянутые функции для универсальности возвращаемого значения в качестве результата возвращают указатель типа
void *
. Но в нашем случае создается массив типа int, для управления которым используется указатель типа int *
,
поэтому выполняется неявное приведение результата функции malloc к типу int *
. В саму функцию malloc передается количество байтов для выделяемого блока. Это количество подсчитать довольно просто: достаточно умножить количество элементов на размер одного элемента
n * sizeof(int) . После выполнения всех действий память освобождается с помощью функции free()
: Free(block);
Важно, что после выполнения этой функции мы уже не сможем использовать массив, например, вывести его значения на консоль: Free(block);
for(int i=0;i И если мы попытаемся это сделать, то получим неопределенные значения. Вместо функции malloc аналогичным образом мы могли бы использовать функцию calloc()
, которая принимает количество элементов и размер одного элемента: Block = calloc(n, sizeof(int));
Либо также можно было бы использовать функцию realloc()
: Int *block = NULL;
block = realloc (block, n * sizeof(int));
При использовании realloc желательно (в некоторых средах, например, в Visual Studio, обязательно) инициализировать указатель хотя бы значением NULL. Но в целом все три вызова в данном случае имели бы аналогичное действие: Block = malloc(n * sizeof(int));
block = calloc(n, sizeof(int));
block = realloc (block, n * sizeof(int));
Теперь рассмотрим более сложную задачу - динамическое выделение памяти для двухмерного массива:
#include Переменная table представляет указатель на массив указателей типа int*
. Каждый указатель table[i] в этом массиве представляет указатель на подмассив элементов типа int
, то есть отдельные строки таблицы.
А переменная table фактически представляет указатель на массив указателей на строки таблицы. Для хранения количества элементов в каждом подмассиве определяется указатель rows типа int
. Фактически он хранит количество столбцов для каждой строки таблицы. Сначала вводится количество строк в переменную rowscount . Количество строк - это количество указателей в массиве, на который указывает указатель table .
И кроме того, количество строк - это количество элементов в динамическом массиве, на который указывает указатель rows . Поэтому вначале необходимо для всех этих массивов
выделить память: Table = calloc(rowscount, sizeof(int*));
rows = malloc(sizeof(int)*rowscount);
Далее в цикле осуществляется ввод количества столбцов для каждый строки. Введенное значение попадает в массив rows. И в соответствии с введенным значением для каждой строки выделяется необходимый размер памяти: Scanf("%d", &rows[i]);
table[i] = calloc(rows[i], sizeof(int));
Затем производится ввод элементов для каждой строки. В конце работы программы при выводе происходит освобождение памяти. В программе память выделяется для строк таблицы, поэтому эту память надо освободить: Free(table[i]);
И кроме того, освобождается память, выделенная для указателей table и rows: Free(table);
free(rows);
Консольный вывод программы: Rows count=2
Columns count for 1=3
table=1
table=2
table=3
Columns count for 2=2
table=4
table=5
1 2 3
4 5 Для работы с массивами информации, программы должны выделять память для этих массивов. Для выделения памяти под массивы переменных используются соответствующие операторы, функции и т.п.. В языке программирования C++ выделяют следующие способы выделения памяти: 1. Статическое
(фиксированное
) выделение памяти. В этом случае память выделяется только один раз во время компиляции. Размер выделенной памяти есть фиксированным и неизменным до конца выполнения программы. Примером такого выделения может служить объявление массива из 10 целых чисел: 2. Динамическое
выделение памяти. В этом случае используется комбинация операторов new
и delete
. Оператор new
выделяет память для переменной (массива) в специальной области памяти, которая называется «куча» (heap). Оператор delete
освобождает выделенную память. Каждому оператору new
должен соответствовать свой оператор delete
. Динамическое выделение памяти по сравнению со статическим выделением памяти дает следующие преимущества: Преимущества статического способа выделения памяти: В зависимости от поставленной задачи, программист должен уметь правильно определить, какой способ выделения памяти подходит для той или другой переменной (массива). Общая форма выделения памяти для одиночной переменной оператором new
имеет следующий вид: Если память для переменной выделена оператором new, то после завершения использования переменной, эту память нужно освободить оператором delete
. В языке C++ это есть обязательным условием. Если не освободить память, то память останется выделенной (занятой), но использовать ее не сможет ни одна программа. В данном случае произойдет «утечка памяти»
(memory leak). В языках программирования Java, C# освобождать память после выделения не нужно. Этим занимается «сборщик мусора» (garbage collector
). Общая форма оператора delete
для одиночной переменной: где ptrName
– имя указателя, для которого была раньше выделена память оператором new
. После выполнения оператора delete
указатель ptrName
указывает на произвольный участок памяти, который не является зарезервированным (выделенным). В примерах демонстрируется использование операторов new
и delete
. Примеры имеют упрощенный вид. Пример 1.
Указатель на тип int
. Простейший пример Пример 2.
Указатель на тип double
«Утечка памяти
» – это когда память для переменной выделяется оператором new
, а по окончании работы программы она не освобождается оператором delete
. В этом случае память в системе остается занятой, хотя потребности в ее использовании уже нет, поскольку программа, которая ее использовала, уже давно завершила свою работу. «Утечка памяти» есть типичной ошибкой программиста. Если «утечка памяти» повторяется многократно, то возможная ситуация, когда будет «занята» вся доступная память в компьютере. Это приведет к непредсказуемым последствиям работы операционной системы. При использовании оператора new
возможна ситуация, когда память не выделится. Память может не выделиться в следующих ситуациях: В этом случае генерируется исключительная ситуация bad_alloc
. Программа может перехватить эту ситуацию и соответствующим образом обработать ее. Пример.
В примере учитывается ситуация, когда память может не выделиться оператором new
. В таком случае осуществляется попытка выделить память. Если попытка удачная, то работа программы продолжается. Если попытка завершилась неудачей, то происходит выход из функции с кодом -1. Оператор выделения памяти new
для одиночной переменной допускает одновременную инициализацию значением этой переменной. В общем, выделение памяти для переменной с одновременной инициализацией имеет вид Пример.
Выделение памяти для переменных с одновременной инициализацией. Ниже приводится функция main()
для консольного приложения. Продемонстрировано выделение памяти с одновременной инициализацией. Также учитывается ситуация, когда попытка выделить память завершается неудачей (критическая ситуация bad_alloc
). Второй способ, которым С++ может хранить информацию, заключается в использовании системы динамического распределения. При этом способе память
распределяется для информации из свободной области памяти по
мере необходимости. Область свободной памяти находится между кодом программы с ее постоянной областью памяти и стеком (
рис.
24.1). Динамическое размещение
удобно, когда неизвестно, сколько элементов данных будет обрабатываться.
По
мере использования программой стековая область увеличивается вниз, то есть программа
сама определяет объем стековой памяти. Например, программа
с большим числом рекурсивных функций
займет больше стековой памяти, чем программа
, не имеющая рекурсивных функций
, так как локальные переменные и возвращаемые адреса хранятся в стеках. Память
под саму программу и глобальные переменные
выделяется на все время выполнения
программы и является постоянной для конкретной среды. Память
, выделяемая в процессе выполнения программы, называется динамической. После выделения динамической
памяти она сохраняется до ее явного освобождения, что может быть выполнено только с помощью специальной операции
или библиотечной функции. Если динамическая
память
не освобождена до окончания программы, то она освобождается автоматически при завершении программы. Тем не менее, явное освобождение ставшей ненужной памяти является признаком хорошего стиля программирования. В процессе выполнения программы участок динамической памяти доступен везде, где доступен указатель
, адресующий этот участок. Таким образом, возможны следующие три варианта работы с динамической памятью
, выделяемой в некотором блоке (например, в теле неглавной функции). Все переменные, объявленные в программе размещаются в одной непрерывной области памяти, которую называют сегментом данных
. Такие переменные не меняют своего размера в ходе выполнения программы и называются статическими
. Размера сегмента данных может быть недостаточно для размещения больших объемов информации. Выходом из этой ситуации является использование динамической памяти. Динамическая память
– это память
, выделяемая программе для ее работы за вычетом
сегмента данных, стека, в котором размещаются локальные переменные подпрограмм и собственно тела программы. Для работы с динамической памятью используют указатели. С их помощью осуществляется доступ
к участкам динамической памяти, которые называются динамическими переменными
. Для хранения динамических переменных
выделяется специальная область памяти, называемая " кучей
". Динамические переменные
создаются с помощью специальных функций и операций. Они существуют либо до конца работы программы, либо до тех пор, пока не будет освобождена выделенная под них память
с помощью специальных функций или операций. То есть время жизни динамических переменных
– от точки создания до конца программы или до явного освобождения памяти
. В С++ используется два способа работы с динамической памятью: В языке программирования С++ для динамического распределения памяти
существуют операции
new
и delete
. Эти операции
используются для выделения и освобождения блоков памяти
. Область памяти, в которой размещаются эти блоки, называется свободной памятью
. Операция new
позволяет выделить и сделать доступным свободный участок в основной памяти, размеры которого соответствуют типу данных, определяемому именем типа. Синтаксис
: new ИмяТипа; new ИмяТипа [Инициализатор]; В выделенный участок заносится значение
, определяемое инициализатором
, который не является обязательным элементом. В случае успешного выполнения new
возвращает адрес
начала выделенного участка памяти. Если участок нужных размеров не может быть выделен (нет памяти), то операция new
возвращает нулевое значение
адреса (NULL
). Синтаксис
применения операции
: Указатель = new ИмяТипа [Инициализатор]; Операция new float
выделяет участок памяти размером 4 байта. Операция new int(15)
выделяет участок памяти 4 байта и инициализирует этот участок целым значением 15. Синтаксис
использования операций new
и delete
предполагает применение указателей. Предварительно каждый указатель
должен быть объявлен: тип *ИмяУказателя; Например: float *pi; //Объявление переменной pi
pi=new float; //Выделение памяти для переменной pi
* pi = 2.25; //Присваивание значения В качестве типа можно использовать, например, стандартные типы int, long, float, double, char
. Оператор new
чаще всего используется для размещения в памяти данных определенных пользователем типов, например, структур: struct Node {
char *Name;
int Value;
Node *Next
};
Node *PNode; //объявляется указатель
PNode = new Node; //выделяется память
PNode->Name = "Ata"; //присваиваются значения
PNode->Value = 1;
PNode->Next = NULL; В качестве имени типа в операции
new
может быть использован массив
: new ТипМассива При выделении динамической памяти для массива его размеры должны быть полностью определены. Например: ptr = new int ;//10 элементов типа int или 40 байт
ptr = new int ;//неверно, т.к. не определен размер Такая операция позволяет выделить в динамической памяти участок для размещения массива соответствующего типа, но не позволяет его инициализировать. В результате выполнения операция new
возвратит указатель
, значением которого служит адрес
первого элемента массива. Например: int *n = new int; Операция new
выполняет выделение достаточного для размещения величины типа int
участка динамической памяти и записывает адрес
начала этого участка в переменную n
. Память
под саму переменную n
(размера, достаточного для размещения указателя) выделяется на этапе компиляции. Программа может хранить информацию в основной памяти компьютера двумя основными способами. Первый из них использует глобальные и локальные переменные, включая массивы, структуры и классы. В случае глобальных и статических локальных переменных место хранения информации фиксируется на все время выполнения программы. В случае локальных переменных память выделяется в стеке. Хотя в Borland С++ работа с этими переменными реализована очень эффективно, их использование требует от программиста знать заранее размер памяти, который потребуется в ходе выполнения программы. Вторым способом хранения информации служит использование системы динамического выделения памяти Borland С++. В этом методе память для хранения информации выделяется из свободной области памяти по мере надобности и возвращается назад, т.е. освобождается, когда надобность в ней исчезла. Область свободной памяти лежит между областью памяти, где размещается программа, и стеком. Эта область называется кучей (heap) и используется для запросов на динамическое выделение памяти. Преимуществом использования динамической памяти служит то, что одна и та же память может быть использована для хранения различной информации в процессе исполнения программы. Поскольку память выделяется для определенной цели и освобождается, когда ее использование завершилось, то можно использовать ту же самую память в другой момент времени для других целей в другой части программы. Другим преимуществом динамического выделения памяти является возможность создания с ее помощью связанных списков, двоичных деревьев и других динамических структур данных. Ядром динамического выделения памяти языка С являются функции malloc() и free(), являющиеся частями стандартной библиотеки. Всякий раз, когда функцией malloc() осуществляется запрос на выделение памяти, выделяется порция имеющейся в наличии свободной памяти. Всякий раз, когда эта память освобождается с помощью функции free(), эта память возвращается назад системе. Язык С++ определяет два оператора динамического выделения памяти - new и delete. Стандарт ANSI С определяет только четыре функции динамического выделения памяти: calloc(), malloc(), free() и realloc(). Однако Borland С++ содержит несколько других функций динамического выделения памяти. При компиляции кода для современной 32-разрядной модели памяти, память является плоской и обычно используются только четыре стандартные функции выделения памяти. Стандарт ANSI С определяет, что заголовочная информация, необходимая для динамического выделения памяти, содержится в файле stdlib.h. Однако Borland С++ позволяет использовать заголовочные файлы stdlib.h или alloc.h. Здесь мы используем заголовочный файл stdlib.h, поскольку это обеспечивает переносимость. Некоторые другие функции динамического выделения памяти требуют заголовочных файлов alloc.h, malloc.h или dos.h. Необходимо обращать особое внимание на то, какой заголовочный файл необходим для использования каждой функции. Прежде чем углубиться в объектно-ориентированную разработку, нам придется сделать
небольшое отступление о работе с памятью в программе на С++. Мы не сможем написать
сколько-нибудь сложную программу, не умея выделять память во время выполнения
и обращаться к ней. Int ival = 1024;
заставляет компилятор выделить в памяти область, достаточную для хранения переменной
типа int, связать с этой областью имя ival и поместить туда значение 1024. Все
это делается на этапе компиляции, до выполнения программы. Int ival2 = ival + 1;
то обращаемся к значению, содержащемуся в переменной ival: прибавляем к нему
1 и инициализируем переменную ival2 этим новым значением, 1025. Каким же образом
обратиться к адресу, по которому размещена переменная? Int *pint; // указатель на объект типа int
Существует также специальная операция взятия адреса, обозначаемая символом
&. Ее результатом является адрес объекта. Следующий оператор присваивает
указателю pint адрес переменной ival: Int *pint;
pint = &ival; // pint получает значение адреса ival
Мы можем обратиться к тому объекту, адрес которого содержит pint (ival в нашем
случае), используя операцию разыменования
, называемую также косвенной
адресацией
. Эта операция обозначается символом *. Вот как можно косвенно
прибавить единицу к ival, используя ее адрес:
*pint = *pint + 1; // неявно увеличивает ival
Это выражение производит в точности те же действия, что и Ival = ival + 1; // явно увеличивает ival
В этом примере нет никакого реального смысла: использование указателя для косвенной
манипуляции переменной ival менее эффективно и менее наглядно. Мы привели этот
пример только для того, чтобы дать самое начальное представление об указателях.
В реальности указатели используют чаще всего для манипуляций с динамически размещенными
объектами. Оператор new имеет две формы. Первая форма выделяет память под единичный объект
определенного типа: Int *pint = new int(1024);
Здесь оператор new выделяет память под безымянный объект типа int, инициализирует
его значением 1024 и возвращает адрес созданного объекта. Этот адрес используется
для инициализации указателя pint. Все действия над таким безымянным объектом
производятся путем разыменовывания данного указателя, т.к. явно манипулировать
динамическим объектом невозможно. Int *pia = new int;
В этом примере память выделяется под массив из четырех элементов типа int.
К сожалению, данная форма оператора new не позволяет инициализировать элементы
массива.
// освобождение единичного объекта
delete pint;
// освобождение массива
delete pia;
Что случится, если мы забудем освободить выделенную память? Память будет расходоваться
впустую, она окажется неиспользуемой, однако возвратить ее системе нельзя, поскольку
у нас нет указателя на нее. Такое явление получило специальное название утечка
памяти
. В конце концов программа аварийно завершится из-за нехватки памяти
(если, конечно, она будет работать достаточно долго). Небольшая утечка трудно
поддается обнаружению, но существуют утилиты, помогающие это сделать. Объясните разницу между четырьмя объектами:
(a) int ival = 1024;
(b) int *pi = &ival;
(c) int *pi2 = new int(1024);
(d) int *pi3 = new int;
Что делает следующий фрагмент кода? В чем состоит логическая ошибка? (Отметим,
что операция взятия индекса () правильно применена к указателю pia. Объяснение
этому факту можно найти в разделе 3.9.2.) Int *pi = new int(10);
int *pia = new int;
Динамическое и статическое выделение памяти. Преимущества и недостатки. Выделение памяти для одиночных переменных операторами new
и delete
. Возможные критические ситуации при выделении памяти. Инициализация при выделении памяти
1. Динамическое и статическое (фиксированное) выделение памяти. Главные различия
2. Преимущества и недостатки использования динамического и статического способов выделения памяти
3. Как выделить память оператором new
для одиночной переменной? Общая форма.
4. Как освободить память, выделенную под одиночную переменную оператором delete
? Общая форма
5. Примеры выделения (new
) и освобождения (delete
) памяти для указателей базовых типов
6. Что такое «утечка памяти» (memory leak
)?
7. Каким образом выделить память оператором new
с перехватом критической ситуации, при которой память может не выделиться? Исключительная ситуация bad_alloc
. Пример
8. Выделение памяти для переменной с одновременной инициализацией. Общая форма. Пример
Рис.
24.1.
Работа с динамической памятью с помощью операций new и delete
В С++ объекты могут быть размещены либо статически – во время компиляции, либо
динамически – во время выполнения программы, путем вызова функций из стандартной
библиотеки. Основная разница в использовании этих методов – в их эффективности
и гибкости. Статическое размещение более эффективно, так как выделение памяти
происходит до выполнения программы, однако оно гораздо менее гибко, потому что
мы должны заранее знать тип и размер размещаемого объекта. К примеру, совсем
не просто разместить содержимое некоторого текстового файла в статическом массиве
строк: нам нужно заранее знать его размер. Задачи, в которых нужно хранить и
обрабатывать заранее неизвестное число элементов, обычно требуют динамического
выделения памяти.
До сих пор во всех наших примерах использовалось статическое выделение памяти.
Скажем, определение переменной ival
С объектом ival ассоциируются две величины: собственно значение переменной,
1024 в данном случае, и адрес той области памяти, где хранится это значение.
Мы можем обращаться к любой из этих двух величин. Когда мы пишем:
С++ имеет встроенный тип “указатель”, который используется для хранения адресов
объектов. Чтобы объявить указатель, содержащий адрес переменной ival, мы должны
написать:
Основные отличия между статическим и динамическим выделением памяти таковы:
Вторая форма оператора new выделяет память под массив заданного размера, состоящий
из элементов определенного типа:
Некоторую путаницу вносит то, что обе формы оператора new возвращают одинаковый
указатель, в нашем примере это указатель на целое. И pint, и pia объявлены совершенно
одинаково, однако pint указывает на единственный объект типа int, а pia – на
первый элемент массива из четырех объектов типа int.
Когда динамический объект больше не нужен, мы должны явным образом освободить
отведенную под него память. Это делается с помощью оператора delete, имеющего,
как и new, две формы – для единичного объекта и для массива:
Наш сжатый обзор динамического выделения памяти и использования указателей,
наверное, больше породил вопросов, чем дал ответов. В разделе 8.4
затронутые проблемы будут освещены во всех подробностях. Однако мы не могли
обойтись без этого отступления, так как класс Array, который мы собираемся спроектировать
в последующих разделах, основан на использовании динамически выделяемой памяти.Упражнение 2.3
Упражнение 2.4
while (*pi < 10) {
pia[*pi] = *pi;
*pi = *pi + 1;
}
delete pi;
delete pia;