Решение задач на динамические массивы
Цель лекции: изучить алгоритмы и приемы чтения-записи, перестановок, поиска и сортировок в динамических массивах, научиться решать задачи с использованием алгоритмов чтения-записи, перестановок, поиска и сортировок в динамических массивах на языке C++.
Динамическая память, называемая также "кучей", выделяется явно по запросу программы из ресурсов операционной системы и контролируется указателем. Она не инициализируется автоматически и должна быть явно освобождена. В отличие от статической и автоматической памяти динамическая память практически не ограничена (ограничена лишь размером оперативной памяти) и может динамически меняться в процессе работы программы. Выделим основные преимущества и недостатки динамического управления памятью.
Преимущества:
- разумное использование динамических структур данных приводит к сокращению объёма памяти, необходимого для работы программы;
- динамические данные не требуют объявлений их как данных фиксированного размера;
- ряд алгоритмов более эффективен при реализации их с использованием динамических структур. Например, вставка элемента в статический массив на определенное место требует перемещения части элементов массива. При вставке в середину динамического списка достаточно несколько операторов присваивания.
Недостатки:
- алгоритмы для динамических структур обычно более сложны, трудны для отладки по сравнению с аналогичными для статических данных;
- использование динамических структур требует затрат на память для ссылок. В некоторых задачах объём памяти, отводимой для ссылок, превосходит объём памяти, выделяемой непосредственно для данных;
- существуют алгоритмы, реализация которых более эффективна на статических данных.
Типичные ошибки при работе с динамической памятью
При работе с динамической памятью можно совершить большое количество ошибок, которые имеют различные последствия и различную степень тяжести. Большинство этих ошибок проявляется не сразу, а через некоторое время в процессе выполнения программы. Следовательно, такие ошибки трудно находимы и потому особенно опасны. Перечислим наиболее часто встречающиеся варианты ошибок при работе с динамической памятью.
1) Попытка воспользоваться неинициализированным указателем.
float *pi; *pi=3.14;//использование неинициализированного указателя
Если pi – глобальная переменная, то она автоматически инициализируется нулевым значением, т.е. имеет значение NULL. Разыменование нулевого указателя приводит к ошибке времени выполнения. Если pi – локальная переменная, то она по умолчанию не инициализируется, а поэтому содержит непредсказуемое значение. Это значение трактуется как адрес вещественной переменной, к которой осуществляется доступ. По чистой случайности может оказаться, что указатель pi содержит истинный адрес переменной программы, тогда значение переменной будет изменено, выполнение программы продолжится дальше, а факт изменения переменной непредсказуемым образом повлияет на дальнейшее выполнение программы.
2) "Висячие" указатели.
После освобождения динамической памяти указатель продолжает указывать на прежний адрес памяти. Такие указатели называются "висячими". Попытка записи по такому указателю не приводит к немедленной ошибке. Однако память, на которую он указывает, могла быть уже выделена другой динамической переменной, и попытка записи приведет к порче этой переменной.
int *p; p= new int; *p=55; delete p; // указатель становится "висячим" *p=8; // использование "висячего" указателя
Если после delete p; сразу написать p=NULL;, то в дальнейшем при попытке разыменовать нулевой указатель p возникнет исключение, что является более предпочтительным, чем скрытая ошибка изменения другой переменной. Данный прием следует иметь ввиду и после освобождения динамической переменной обнулять указатель:
delete p; p=NULL;
3) "Утечка" памяти.
Данная ошибка возникает, когда память не освобождается, но перестает контролироваться указателем. Подобную ошибку называют "утечкой" памяти, поскольку такую память невозможно освободить. Такая ошибка труднонаходима, поскольку практически не сказывается на работе приложения. Однако при систематических утечках программа требует все больше памяти у операционной системы, замедляя работу других приложений. Далее приводятся две распространенные ситуации, в которых возникает утечка памяти.
Пример. Повторное выделение памяти.
Если выделить память повторно для того же указателя, то ранее выделенная память "утечет":
int *p; p= new int; *p=55; p= new int; //выделяется новый участок памяти под тот же указатель
Пример. Выделение памяти под локальную переменную без освобождения.
function pp (int n) { int *p; p= new int; *p=n; }
Такой фрагмент кода наиболее опасен при использовании локальных переменных в функциях. При неоднократных вызовах функция выделяет новую область памяти, не освобождая ее после использования. В результате может возникнуть ситуация нехватки памяти.
4) Попытка освободить динамическую память, не выделенную ранее.
int *p; delete p;
Вызов операции delete для неинициализированного указателя игнорируется, не приводя к генерации ошибки.
5) Попытка освободить нединамическую память.
int *p,i=55; p=&i; delete p;
При вызове delete для нединамической переменной будет сгенерирована ошибка.
Проверка на выделение памяти
Существует единственное числовое значение, которое можно присвоить непосредственно указателю – это NULL. Нулевой адрес – особый, по этому адресу не может храниться ни одна переменная. То есть указатель, имеющий нулевое значение указывает в "никуда", к такому указателю нельзя применить оператор разыменования.
Библиотечные функции malloc (calloc) или оператор new используют функцию операционной системы для выделения памяти. Если затребованный размер памяти слишком большой (а также при попытке создать массив из нуля или отрицательного числа элементов), операционная система не будет выделять память и тогда функции или оператору вернет нулевое значение ( NULL ). Если это нулевое значение будет присвоено указателю, к которому впоследствии будет применен оператор разыменования или оператор обращения к элементу массива, то программа аварийно завершит работу с ошибкой "segmentation fault" (ошибка сегментации). Для того чтобы избежать таких ошибок необходимо сразу после выделения памяти проверить ее значение, которое возвращает функция или оператор. Проверку можно осуществить с помощью одного из условий: if (pi==NULL) или if (!pi). В случае если это значение равно NULL, выполнить какие-либо действия, например, вывести сообщение о невозможности выделения необходимого объема памяти.
Например:
int n=1000000000; int *pi=new int[n]; if (pi==NULL) { // if (!pi) printf ("Требуемая память не выделена!"); return; }
Все рассмотренные функции по работе с динамической памятью могут выделять память размером не более одного сегмента, то есть не более 64K в 16-ти разрядных моделях и не более 4G в 32-х разрядных моделях памяти.
Многомерные динамические массивы
Для многомерных динамических массивов память распределяется аналогичным образом, что и для двумерных динамических массивов. Следует только помнить, что в дальнейшем ненужную для выполнения программы память следует освобождать. Приведем пример распределения памяти для трехмерного массива размером n, m, k.
#include <stdlib.h> void main () { long ***a; int n, m, k, i, j,l; scanf("%d", &n); scanf("%d", &m); scanf("%d", &k); //распределение памяти a=(long ***) calloc(n,sizeof(long **)); for (i=0; i<n; i++) { a[i]=(long **) calloc(m,sizeof(long *)); for (j=0; j<m; j++) { a[i][j]=(long *) calloc(k,sizeof(long)); for (l=0; l<k; l++) a[i][j][l]=rand(); } } . . . . . . . . . . . . //освобождение памяти for (i=0; i<n; i++) { for (j=0; j<m; j++) free (a[i][j]); free (a[i]); } free (a); }