Динамическое выделение памяти необходимо для эффективного использования памяти компьютера. Например, мы написали какую-то программку, которая обрабатывает массив. При написании данной программы необходимо было объявить массив, то есть задать ему фиксированный размер (к примеру, от 0 до 100 элементов). Тогда данная программа будет не универсальной, ведь может обрабатывать массив размером не более 100 элементов. А если нам понадобятся всего 20 элементов, но в памяти выделится место под 100 элементов, ведь объявление массива было статическим, а такое использование памяти крайне не эффективно.
В С++ операции new и delete предназначены для динамического распределения памяти компьютера. Операция new выделяет память из области свободной памяти, а операция delete высвобождает выделенную память. Выделяемая память, после её использования должна высвобождаться, поэтому операции new и delete используются парами. Даже если не высвобождать память явно, то она освободится ресурсами ОС по завершению работы программы. Рекомендую все-таки не забывать про операцию delete .
// пример использования операции new int *ptrvalue = new int; //где ptrvalue – указатель на выделенный участок памяти типа int //new – операция выделения свободной памяти под создаваемый объект.
Операция new создает объект заданного типа, выделяет ему память и возвращает указатель правильного типа на данный участок памяти. Если память невозможно выделить, например, в случае отсутствия свободных участков, то возвращается нулевой указатель, то есть указатель вернет значение 0. Выделение памяти возможно под любой тип данных: int , float ,double , char и т. д.
// пример использования операции delete: delete ptrvalue; // где ptrvalue – указатель на выделенный участок памяти типа int // delete – операция высвобождения памяти
Разработаем программу, в которой будет создаваться динамическая переменная.
// new_delete.cpp: определяет точку входа для консольного приложения.
#include "stdafx.h"
#include
// код Code::Blocks
// код Dev-C++
// new_delete.cpp: определяет точку входа для консольного приложения.
#include
В строке 10 показан способ объявления и инициализации девяткой динамического объекта, все, что нужно так это указать значение в круглых скобочках после типа данных. Результат работы программы показан на рисунке 1.
Ptrvalue = 9 Для продолжения нажмите любую клавишу. . .
Рисунок 1 — Динамическая переменная
Создание динамических массивов
Как было сказано раньше, массивы также могут быть динамическими. Чаще всего операции new и delete применяются для создания динамических массивов, а не для создания динамических переменных. Рассмотрим фрагмент кода создания одномерного динамического массива.
// объявление одномерного динамического массива на 10 элементов: float *ptrarray = new float ; // где ptrarray – указатель на выделенный участок памяти под массив вещественных чисел типа float // в квадратных скобочках указываем размер массива
После того как динамический массив стал ненужным, нужно освободить участок памяти, который под него выделялся.
// высвобождение памяти отводимой под одномерный динамический массив: delete ptrarray;
После оператора delete ставятся квадратные скобочки, которые говорят о том, что высвобождается участок памяти, отводимый под одномерный массив. Разработаем программу, в которой создадим одномерный динамический массив, заполненный случайными числами.
// new_delete_array.cpp: определяет точку входа для консольного приложения.
#include "stdafx.h"
#include
// код Code::Blocks
// код Dev-C++
// new_delete_array.cpp: определяет точку входа для консольного приложения.
#include
Созданный одномерный динамический массив заполняется случайными вещественными числами, полученными c помощью функций генерации случайных чисел, причём числа генерируются в интервале от 1 до 10, интервал задается так — rand() % 10 + 1 .
Чтобы получить случайные вещественные числа, выполняется операция деления, с использованием явного приведения к вещественному типу знаменателя — float((rand() % 10 + 1)) . Чтобы показать только два знака после запятой используем функцию setprecision(2) ,
прототип данной функции находится в заголовочном файле
Array = 0.8 0.25 0.86 0.5 2.2 10 1.2 0.33 0.89 3.5 Для продолжения нажмите любую клавишу. . .
Рисунок 2 — Динамический массив в С++
По завершению работы с массивом, он удаляется, таким образом, высвобождается память, отводимая под его хранение.
Как создавать и работать с одномерными динамическими массивами мы научились. Теперь рассмотрим фрагмент кода, в котором показано, как объявляется двумерный динамический массив.
// объявление двумерного динамического массива на 10 элементов: float **ptrarray = new float* ; // две строки в массиве for (int count = 0; count < 2; count++) ptrarray = new float ; // и пять столбцов // где ptrarray – массив указателей на выделенный участок памяти под массив вещественных чисел типа float
Сначала объявляется указатель второго порядка float **ptrarray , который ссылается на массив указателей float* ,где размер массива равен двум. После чего в цикле for каждой строке массива объявленного в строке 2 выделяется память под пять элементов. В результате получается двумерный динамический массив ptrarray .Рассмотрим пример высвобождения памяти отводимой под двумерный динамический массив.
// высвобождение памяти отводимой под двумерный динамический массив: for (int count = 0; count < 2; count++) delete ptrarray; // где 2 – количество строк в массиве
Объявление и удаление двумерного динамического массива выполняется с помощью цикла, так как показано выше, необходимо понять и запомнить то, как это делается. Разработаем программу, в которой создадим двумерный динамический массив.
// new_delete_array2.cpp: определяет точку входа для консольного приложения.
#include "stdafx.h"
#include // код Code::Blocks // код Dev-C++ // new_delete_array2.cpp: определяет точку входа для консольного приложения.
#include При выводе массива была использована функция setw() , если вы не забыли, то она отводит место заданного размера под выводимые данные. В нашем случае, под каждый элемент массива по четыре позиции, это позволяет выровнять, по столбцам, числа разной длинны (см. Рисунок 3). 2.7 10 0.33 3 1.4
6 0.67 0.86 1.2 0.44
Для продолжения нажмите любую клавишу. . . Рисунок 3 — Динамический массив в С++ Итак. третий тип, самый интересный в этой теме для нас – динамический тип памяти. Как мы работали с массивами раньше? int a Как мы работаем сейчас?
Выделяем столько, сколько нужно: #include
<
stdio.h>
#include
<
stdlib.h>
int
main
() {
size_t
size;
//
Создаём указатель на int
//
– по сути, пустой массив.
int
*list;
scanf
("
%lu
"
, &size);
//
Выделяем память для size элементов размером int
//
и наш "пустой массив" теперь ссылается на эту память.
list = (int
*)malloc
(size * sizeof
(int
));
for
(int
i = 0
; i < size; ++i) {
scanf
("
%d
"
< size; ++i) {
printf
("
%d
"
, *(list + i));
}
//
Не забываем за собой прибраться!
free
(list);
} //
*
Void * malloc(size_t size);
Но в общем и целом это функция,
выделяет size байт неинициализированной памяти (не нули, а мусор). Если выделение прошло успешно, то возвращается указатель на самый первый байт выделенной памяти. Если неуспешно – NULL. Также errno будет равен ENOMEM (эту замечательную переменную
мы рассмотрим позднее).
То есть правильнее было написать: #include
<
stdio.h>
#include
<
stdlib.h>
int
main
() {
size_t
size;
int
*list;
scanf
("
%lu
"
, &size);
list = (int
*)malloc
(size * sizeof
(int
));
if
(list == NULL
) {
goto
error;
}
for
(int
i = 0
; i < size; ++i) {
scanf
("
%d
"
, list + i);
}
for
(int
i = 0
; i < size; ++i) {
printf
("
%d
"
, *(list + i));
}
free
(list);
return
0
;
error:
return
1
;
} //
*
Очищать NULL указатель не нужно #include
<
stdlib.h>
int
main
() {
free
(NULL
);
} – в том же clang всё пройдёт нормально (сделает ничто), но в более экзотических
случаях вполне может крэшнуть программу. Рядом с malloc и free в мане можно увидеть ещё: void * calloc (size_t count, size_t size); Равно как и malloc выделит память под count объектов размером по size байт.
Выделяемая память инициализируется нулями. void * realloc (void *ptr, size_t size); Перевыделяет (если может) память, на которую указывает ptr , в размере size байт.
Если не хватает места для увеличения выделенной памяти, на которое указывает ptr , realloc создает новое выделение (аллокацию),
копирует старые данные, на которые указывает ptr ,
освобождает старое выделение и возвращает указатель на выделенную память. Если ptr равен NULL , realloc идентичен вызову malloc . Если size равен нулю, а ptr не NULL , выделяется кусок памяти
минимального размера, а исходная освобождается. void * reallocf (void *ptr, size_t size); Придумка из FreeBSD API. Как и realloc , но если не сможет перевыделить, очищает
принятый указатель. void * valloc (size_t size); Как и malloc , но выделенная память выравнивается по границе страницы. Динамическое выделение памяти
Основные проблемы применения
Нулевой указатель
Нулевой указатель − это указатель, хранящий специальное значение, используемое для того, чтобы показать, что данная переменная-указатель не ссылается (не указывает) ни на какой объект. В различных языках программирования представлен различными константами. ·В языках C# и Java: null ·В языках Си и C++: 0 или макрос NULL. Кроме того, в стандарте C++11 для обозначения нулевого указателя предложено новое ключевое слово nullptr ·В языках Паскаль и Ruby: nil ·В языке Компонентный Паскаль:NIL ·В языке Python: None Указателями сложно управлять. Достаточно легко записать в указатель неправильное значение, что может вызвать трудно воспроизводимую ошибку. Например, вы случайно поменяли адрес указателя в памяти, или неправильно выделили под информацию память и тут вас может ожидать сюрприз: другая очень важная переменная, которая используется только внутри программы будет перезаписана. В таком случае вам будет сложно воспроизвести баг. Нелегко будет и понять, где именно находится ошибка. И не всегда тривиально будет её устранить (иногда придётся переписать существенную часть программы). Для решения части проблем есть методы предохранения и страховки:
Изучив указатели в языке Си, мы открыли для себя возможности динамического выделения памяти. Что это значит? Это значит то, что при динамическом выделении памяти, память резервируется не на этапе компиляции а на этапе выполнения программы. И это дает нам возможность выделять память более эффективно, в основном это касается массивов. С динамическим выделением память, нам нет необходимости заранее задавать размер массива, тем более, что не всегда известно, какой размер должен быть у массива. Далее рассмотрим каким же образом можно выделять память. Функция malloc() определена в заголовочном файле stdlib.h, она используется для инициализации указателей необходимым объемом памяти. Память выделяется из сектора оперативной памяти доступного для любых программ, выполняемых на данной машине. Аргументом функции malloc() является количество байт памяти, которую необходимо выделить, возвращает функция - указатель на выделенный блок в памяти. Функция malloc() работает также как и любая другая функция, ничего нового. Так как различные типы данных имеют разные требования к памяти, мы как-то должны научиться получить размер в байтах для данных разного типа. Например, нам нужен участок памяти под массив значений типа int - это один размер памяти, а если нам нужно выделить память под массив того же размера, но уже типа char - это другой размер. Поэтому нужно как-то вычислять размер памяти. Это может быть сделано с помощью операции sizeof(), которая принимает выражение и возвращает его размер. Например, sizeof(int) вернет количество байтов, необходимых для хранения значения типа int. Рассмотрим пример: Яндекс.Директ В этом примере, встроке 3
указателю ptrVar присваивается адрес на участок памяти, размер которого соответствует типу данных int. Автоматически, этот участок памяти становится недоступным для других программ. А это значит, что после того, как выделенная память станет ненужной, её нужно явно высвободить. Если же память не будет явно высвобождена, то по завершению работы программы, память так и не освободится для операционной системы, это называется утечкой памяти. Также можно определять размер выделяемой памяти, которую нужно выделить передавая пустой указатель, вот пример: Как видите, в такой записи есть одна очень сильная сторона, мы не должны вызывать функцию malloc() с использованиемsizeof(float). Вместо этого мы передали в malloc() указатель на тип float, в таком случае, размер выделяемой памяти автоматически определится сам! Особенно это пригодится, если выделять память потребуется далеко от определения указателя: Если бы вы использовали конструкцию выделения памяти с операцией sizeof(), то вам бы пришлось находить в коде определение указателя, смотреть его тип данных и уже потом вы бы смогли правильно выделить память. Программа может хранить информацию в основной памяти компьютера двумя основными способами. Первый из них использует глобальные и локальные переменные, включая массивы, структуры и классы. В случае глобальных и статических локальных переменных место хранения информации фиксируется на все время выполнения программы. В случае локальных переменных память выделяется в стеке. Хотя в 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. Необходимо обращать особое внимание на то, какой заголовочный файл необходим для использования каждой функции. Для работы с массивами информации, программы должны выделять память для этих массивов. Для выделения памяти под массивы переменных используются соответствующие операторы, функции и т.п.. В языке программирования 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
).
#include
float *ptrVar;
/* .
.
.
сто строк кода */
.
.
.
ptrVar = malloc(sizeof(*ptrVar));
Динамическое и статическое выделение памяти. Преимущества и недостатки. Выделение памяти для одиночных переменных операторами new
и delete
. Возможные критические ситуации при выделении памяти. Инициализация при выделении памяти
1. Динамическое и статическое (фиксированное) выделение памяти. Главные различия
2. Преимущества и недостатки использования динамического и статического способов выделения памяти
3. Как выделить память оператором new
для одиночной переменной? Общая форма.
4. Как освободить память, выделенную под одиночную переменную оператором delete
? Общая форма
5. Примеры выделения (new
) и освобождения (delete
) памяти для указателей базовых типов
6. Что такое «утечка памяти» (memory leak
)?
7. Каким образом выделить память оператором new
с перехватом критической ситуации, при которой память может не выделиться? Исключительная ситуация bad_alloc
. Пример
8. Выделение памяти для переменной с одновременной инициализацией. Общая форма. Пример