Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Конспект С++ (Часть 2).doc
Скачиваний:
16
Добавлен:
10.09.2019
Размер:
816.64 Кб
Скачать

Возможные ошибки при работе с динамической памятью

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

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

Поведение функций malloc и calloc с точки зрения обнаружения таких ошибок более “разумно”. В случае невозможности выделить требуемый объем памяти в динамической области эти функции возвращают нулевой указатель. Контролируя это значение можно избежать ошибок в работе программы:

double *p = (double *) malloc (sizeof (double) ); // Пытаемся выделить память

if ( !p ) // Память выделить не удалось

{

// Принимаем меры по исправлению ситуации

}

// Продолжаем работу

Однако имеется другой вариант инструкции new, который работает так же, как и функции malloc и calloc (также возвращает нулевой указатель). Вот как его использовать:

double *p = new ( nothrow ) double; // Пытаемся выделить память

if ( !p ) // Память выделить не удалось

{

// Принимаем меры по исправлению ситуации

}

// Продолжаем работу

Другая категория ошибок называется “утечкой памяти”. Например:

int * p; // Объявляем указатель на целый тип данных

p = new int; // Выделяем память по некоторому адресу p

………

p = new int; // Еще раз выделяем память, и ее адрес записываем опять в p

В этом примере повторное присвоение переменной p другого адреса нового участка памяти приводит к потере адреса участка памяти, выделенного первой инструкцией new. Этот “забытый” участок памяти будет занят до конца работы программы, и его нельзя ни освободить, ни использовать для хранения данных – говорят, что произошла утечка памяти. Такие “утечки” могут привести к тому, что опять произойдет переполнение динамической области памяти. Для недопущения подобных ошибок необходимо внимательно следить за своевременным освобождением памяти, на которую ссылается переменная-указатель.

Еще одна категория ошибок связана с попытками обращения к динамической памяти через указатели, не инициализированные с помощью инструкции new и функциями malloc и calloc, а также при попытке обращения к динамической памяти через указатель после освобождения памяти с помощью инструкции delete или функции free.

Динамические массивы

Для того чтобы создать в динамической области некоторый объект необходима одна обычная (не динамическая переменная) переменная-указатель. Сколько таких объектов нам понадобится для одновременной обработки – столько необходимо иметь обычных переменных-указателей. Таким образом, проблема “задач неопределенной размерности” созданием одиночных динамических объектов решена быть не может.

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

Сначала рассмотрим одномерные динамические массивы.

Для создания одномерного динамического массива, элементами которого являются, например, действительные числа, используется следующий синтаксис инструкции new:

double *Arr = new double [1000];

Здесь в динамической области памяти будет выделено пространство на 1000 значений типа double, и адрес этой области будет присвоен переменной-указателю Arr. Таким образом, переменная-указатель Arr, как и переменная для обычного массива, будет содержать адрес первого элемента массива.

Освободить динамическую область от этого массива можно так:

delete [ ] Arr;

После этого участок памяти объемом 1000 * sizeof ( double ) байт будет возвращен в список свободной памяти и может быть повторно использован для размещения других динамических объектов.

С помощью функций malloc и calloc тот же самый одномерный динамический массив создается так:

double *Arr = (double *) malloc(1000 * sizeof (double ) );

или

double *Arr = (double *) сalloc(1000, sizeof (double ) );

Освобождение памяти в этих случаях осуществляется с помощью функции free:

Free ( Arr );

Работа с одномерным динамическим массивом осуществляется так же, как и с обычным. Рассмотрим пример, в котором создадим динамический массив целых с количеством элементов, введенном с клавиатуры; заполним его случайными значениями в диапазоне от 1 до 100; подсчитаем и выведем на экран среднее значение всех элементов этого массива:

int n; // Количество элементов массива

cin >> n; // Вводим количество элементов массива с клавиатуры

int *Arr = new int [ n ]; // Создаем массив Arr целых чисел на n элементов

for ( int i = 0; i < n; ++ i) // Заполняем массив случайными значениями

Arr [ i ] = rand ( ) % 100 + 1;

int Sum = 0; // Сумма элементов массива

for ( int i = 0; i < n; ++ i ) // Подсчитываем сумму элементов массива

Sum += Arr [ i ];

cout << (double) Sum / n << endl; // Выводим на экран среднее значение

delete [ ] Arr; // Освобождаем память

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

  1. создать исходный массив размерности N1 и заполнить его данными;

  2. создать промежуточный массив размерности N2 (пусть N2 > N1);

  3. скопировать данные из исходного массива в промежуточный массив;

  4. освободить память от исходного массива;

  5. переменной-указателю исходного массива присвоить значение переменной-указателя промежуточного массива;

  6. заполнить новые элементы массива данными.

Вот как можно решить эту задачу в стиле C++:

int N1 = 10, // Начальный размер массива

N2 = 20; // Новый размер массива

int *Arr = new int [N1]; // Создаем исходный массива из N1 элемента

for (int i = 0; i < N1; ++ i) // Заполняем исходный массив числами от 0 до 9

Arr[i] = i;

for (int i = 0; i < N1; ++ i) // Выводим исходный массив на экран

cout << Arr[i] << " ";

cout << endl;

int *Rez = new int [N2]; // Создаем промежуточный массив из N2 элементов

for (int i = 0; i < N1; ++ i) // Копируем данные из исходного массива

Rez[i] = Arr[i]; // в промежуточный

delete [ ] Arr; // Освобождаем память от исходного массива. Если этого не

// сделать, произойдет утечка памяти

Arr = Rez; // Изменяем переменную исходного массива

for (int i = N1; i < N2; ++ i) // Дополняем исходный массив числами от 10 до 19

Arr[i] = i;

for (int i = 0; i < N2; ++ i) // Снова выводим исходный массив на экран

cout << Arr[i] << " ";

cout << endl;

delete [ ] Arr; // Окончательно освобождаем память от исходного массива.

В результате работы этого фрагмента программы на экран будут выведены значения массива до его расширения и после расширения:

0 1 2 3 4 5 6 7 8 9

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

Если удалить из этого фрагмента строки, не имеющие непосредственного отношения к решаемой задаче (это ввод данных в массивы, вывод данных на экран), то “скелет” алгоритма будет таким:

int N1 = 10, // Начальный размер массива

N2 = 20; // Новый размер массива

int *Arr = new int [N1]; // Создаем исходный массива из N1 элемента

……. // Работаем с массивом старой длины

int *Rez = new int [N2]; // Создаем промежуточный массив из N2 элементов

for (int i = 0; i < N1; ++ i) // Копируем данные из исходного массива

Rez[i] = Arr[i]; // в промежуточный

delete [ ] Arr; // Освобождаем память от исходного массива. Если этого не

// сделать, произойдет утечка памяти

Arr = Rez; // Изменяем переменную исходного массива

……. // Работаем с массивом новой длины

delete [ ] Arr; // Окончательно освобождаем память от исходного массива.

А вот как решается эта же задача при использовании стиля языка C:

int N1 = 10, // Начальный размер массива

N2 = 20; // Новый размер массива

int *Arr = (int *) malloc( N1 * sizeof ( int ) ); // Создаем массив из N1 элемента

for (int i = 0; i < N1; ++ i) // Заполняем массив числами от 0 до 9

Arr[i] = i;

for (int i = 0; i < N1; ++ i) // Выводим массив на экран

cout << Arr[i] << " ";

cout << endl;

Arr = (int *) realloc( Arr, N2 * sizeof ( int ) ); // Изменяем размер массива

for (int i = N1; i < N2; ++ i) // Дополняем массив числами от 10 до 19

Arr[i] = i;

for (int i = 0; i < N2; ++ i) // Снова выводим массив на экран

cout << Arr[i] << " ";

cout << endl;

delete [ ] Arr; // Окончательно освобождаем память от исходного массива.

А вот, что представляет “скелет” алгоритма, в этом случае:

int N1 = 10, // Начальный размер массива

N2 = 20; // Новый размер массива

int *Arr = (int *) malloc( N1 * sizeof ( int ) ); // Создаем массив из N1 элемента

……. // Работаем с массивом старой длины

Arr = (int *) realloc( Arr, N2 * sizeof ( int ) ); // Изменяем размер массива

……. // Работаем с массивом новой длины

delete [ ] Arr; // Окончательно освобождаем память от исходного массива.

Не правда ли, существенно короче и понятнее.

Перейдем к рассмотрению двумерных массивов. С ними дело обстоит несколько сложнее, чем с одномерными динамическими массивами.

В стиле C++ двумерный массив целых чисел можно создать и удалить следующим образом:

int ( * Arr ) [ 10 ] = new int [ dim ] [ 10 ];

delete [ ] Arr;

В этом примере создается двумерный массив из dim строк и 10 столбцов. Элементами массива являются целые числа типа int. При таком способе создания двумерных массивов изменять в процессе выполнения программы можно только самую левую размерность массива (dim). Вторая размерность должна задаваться константным значением (в нашем случае 10). Таким образом, динамически изменяемым в таких массивах является только количество строк, а количество столбцов остается постоянным. Пример использования этого метода:

cin >> dim; // Вводим с клавиатуры количество строк массива

int ( * Arr ) [ 10 ] = new int [ dim ] [ 10 ]; // Выделяем память в динамической области

// Заполняем массив Arr значениями равными сумме индексов элементов массива

for ( int i = 0; i < dim; ++ i )

for ( int j = 0; j < 10; ++ j )

Arr [ i ] [ j ] = i + j;

// Выводим на экран элементы массива Arr в виде таблицы, содержащей dim строк и

// 10 столбцов

for (int i = 0; i < dim; ++ i)

{

for (int j = 0; j < 10; ++ j)

cout << setw(4) << Arr[i][j];

cout << endl;

}

delete [ ] Arr; // Освобождаем память от массива Arr

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