Доброго времени суток! Сегодня речь пойдет о динамическом выделении памяти.
Вот недавно получил письмо, в котором мне предложили в каждом выпуске рассылки
давать ссылку на архив. Что ж, идея неполхая... Указанная ссылка будет
находиться в самом конце рассылки.
Итак, о теме.
Вы уже привыкли, что все переменные должны быть описаны до начала их работы.
Это действительно так, если речь идет о полноценных переменных, имеющих
имя, область видимости, и т.д. Но вы знаете, что в С есть указатели. Операция
раскрытия указателя (*) возвращает, как вы помните, L-выражение. У такого выражения
есть адрес, тип и, разумеется, значение. Но у него нет собственного имени
и нет области видимости. Таким образом, мы подходим к главной сегодняшней идее:
такого рода "переменные" можно создавать динамически, то есть во время
выполнения программы (а не на этапе компиляции).
Для создания такой переменной нам необязательно знать ее тип, но необходимо
знать ее размер. Это и понятно - надо же знать, сколько памяти выделять
под переменную. Размер переменной сообщает оператор
sizeof L_выражение или sizeof
(имя_типа). Но будьте осторожны! sizeof
возвращает не реальный, а предполагаемый размер. Как правило, эти два
размера совпадают, но не всегда. Например, если мы создали переменную типа
int, но размером 8 байт, то sizeof
возвратит 2 или 4, в зависимости от платформы, но никак не 8. Особенно
это отразится на работе с динамическими массивами (о них чуть позже).
Итак, размер мы подсчитали... А дальше что? А дальше нам на помощь приходит функция
void *malloc (unsigned long size);
Она выделяет в памяти блок величиной в size байт и возвращает указатель
на него. Как после этого его использовать? А как захотите. Единственное - должны быть
выполнены 2 требования:
Нельзя выходить за рамки отведенной области памяти. Программа не должна
никаких предположений относительно того, что хранится вне отведенной области
(равно как и в отведенной области до ее инициализации). Попытка записи
в память вне отведенной области приводит к непредсказуемому результату -
вплоть до аварийного завершения работы программы (т.н. crash). Более вероятно
другое: программа crash'нется, но не сразу, а существенно позже. Отследить
это существенно сложнее (по личному опыту - это самые труднонаходимые ошибки).
"Если что-нибудь открыли - закройте. Если не открывали - не закрывайте".
Эту цитату из "правил выживания в лаборатории" вам следует запомнить раз и
навсегда - она вам поможет выжить при программировании :-) Конкретно к данному
случаю - вы выделили в памяти блок, попользовались им, и теперь он вам
больше не нужен. В таком случае блок необходимо освободить функцией
void free (void* block);
В качестве параметра этой функции следует указать освобождаемый блок памяти.
После вызова этой функции блок больше программе не принадлежит и ничего с ним
делать нельзя (иначе - см. пункт 1). И не пытайтесь освобождать блоки,
которых не занимали! Самый вероятный вариант поведения программы в этом
случае - crash. С другой стороны, забыв освободить выделенный блок, вы
рано или поздно приведете систему к нехватке памяти.
Функции malloc и free объявлены в файле
<stdlib.h> или <alloc.h>
Итак, объединяя вышесказанное, получаем простенькую демонстрацию динамического
использования памяти:
#include <stdio.h>
#include <alloc.h>
int main (void)
{
int *a;
a = (int*) malloc (sizeof (int));
scanf ("%d", a);
printf ("Вы ввели %d\n", *a);
free (a);
}
Несколько замечаний. Во-первых, преобразование из void*
в int* требуется указывать явно - глупый
компилятор не понимает :-) А вот вvoid*
другие указатели преобразуются автоматически и без проблем. Во-вторых, так как
a теперь - указатель, то ставить оператор взятия адреса в scanf не
следует - она уже содержит адрес. Поэтому же в printf следует поставить
оператор раскрытия указателя.
А теперь представим на минуту, что мы написали так:
scanf ("%d", &a);
Что произойдет? Сразу - ничего. Как вы помните, scanf принимает любое
количество любых параметров - в том числе и указателей. Но теперь в
переменной a хранится "мусор" - указатель на невыделенный блок памяти
(а выделенный теперь недоступен, так как утрачены все указатели на него).
В результате, когда мы попытаемся его освободить, программа аварийно завершится.
Впрочем, завершиться она может и раньше. Так что следует быть осторожным и
постепенно дружиться с отладчиком - он вам очень сильно поможет в отыскании
ошибок такого рода.
А как с областью видимости такой переменной? Вы ее устанавливаете сами.
Переменная начинает существовать с того момента, когда ее создали, заканчивает -
когда ее освободили. А доступ к ней имеют те участки кода, которам вы сообщили ее
адрес.
А теперь главное. Напишем в примере так:
a = (int*) malloc (16 * sizeof (int));
Что получается? Мы выделили блок, достаточный для размещения 16 переменных типа
int. Что ж, остальные 15 будут теряться? А вот нет.
Вспомним, что массивы и указатели в С отождествленны. Теперь оценили эту
возможность? :-) Теперь мы с указателем можем обращаться, как с массивом из
16 элементов, то есть писать a [2] = 5; printf ("%d",
a [0]);, и т.д. Таким образом, ма получили очень мощное средство -
динамический массив.
Что будет, если мы теперь напишем a [16] = 3;? Программа честно запишет
число 3 туда, где находится (вернее, по идее должен находиться) 16-й элемент
массива по адресу a. А так как он принадлежит невыделенной области памяти
(помните, что индексация в С начинается с нуля!), то последствия будут... в общем,
см.выше. Могу даже сразу сказать, где программа crash'нется - при следующем
выделении/освобождении памяти (скорее всего).
Итак, а как быть, если массив должен быть двумерным, причем ни количество
строк, ни количество столбцов заранее не известно? Выход прост и ясен на примере:
#include <stdio.h>
#include <alloc.h>
int main (void)
{
int r, c, i, j, **a;
printf ("Введите размерность массива: ");
scanf ("%d %d", &r, &c);
a = (int**) malloc (r * sizeof (int*));
for (i = 0; i < r; i++)
a [i] = (int*) malloc (c * sizeof (int));
printf ("Вводите элементы массива\n");
for (i = 0; i < r; i++)
for (j = 0; j < c; j++)
scanf ("%d", &a [i][j]);
printf ("Вот ваш массив:\n");
for (i = 0; i < r; i++)
{
for (j = 0; j < c; j++)
printf ("%d ", a [i][j]);
printf ("\n");
}
for (i = 0; i < r; i++)
free (a [i]);
free (a);
}
То есть мы вместо двумерного массива создаем одномерный массив указателей на
int. А уж они в свою очередь интерпретируются как
массивы. Как будет раскрываться выражение a [i][j] - проследите сами.
После всего вышесказанного это достаточно очевидно. Также, я думаю, вам
понятно, как реализуются многомерные массивы (трехмерные, четырехмерные и т.д.)
Более того, как вы можете догадаться, количество элементов в каждой строке
массива не обязательно должно быть одинаковым, что позволяет вам создавать
массивы экзотической формы - треугольные и т.д.
Существует и иной способ реализации многомерных массивов - он применим, когда
число элементов в массиве известно на этапе компиляции. Он состоит в том, что
весь массив мы рассматриваем как одну переменную:
int (*a)[4][4];
a = (int (*)[4][4]) malloc (sizeof (int [4][4]));
/* пример работы с ним */
printf ("%d\n", (*a)[2][1]);
free (a);
Учтите, здесь a является указателем на двумерный массив, то есть, по
сути дела, тройным указателем. Еще учтите, что массивы указателей и двумерные
массивы - это не совсем одно и то же. В первом случае a [i][j]
интерпретируется как *(*(a + i) + j), а во втором - *(a + i *
количество_столбцов + j) Таким образом, две указанные схемы реализации
двумерных массивов несовместимы. Вы, конечно, можете преобразовать тип из
int (*)[4][4] в int
***. Но что будет? Первый указатель раскроется правильно, а вот раскрытие
второго возвратит элемент a[0][i] (т.к. размеры типов
int и int* совпадают, а
int ** интерпретируется как массив из
int*), после чего по нему как по адресу произойдет
обращение к памяти. Считается некоторый мусор (или не считается, если вы в Windows),
к нему прибавится j * sizeof
(int) и произойдет еще одно обращение. Что получится
в результате - даже подумать страшно... (Хотя программа скомпилируется!)
Иногда оказывается, что мы неправильно рассчитали размер массива. Что делать в таком
случае? Нам помогает функция
void* realloc (void* block, unsigned long newsize);
Она изменяет размер выделенного блока памяти. Учтите, что если блок увеличивается
в размерах, то функция может переместить его в новое место, если на старом он
не помещается. Так что вам следует сохранить результат ее вызова и все дальнейшие
обращения к блоку проводить по нему.
Функции malloc и realloc возвращают 0 (вообще-то язык С предусматривает
идентификатор NULL, объявленный в множестве файлов - это 0 любого типа), если
память по каким-либо причинам выделить не удалось.
И еще хочу заметить: не следует вызывать malloc, realloc и free
слишком часто: они увеличивают фрагментацию памяти. Если вам требуется выделить
блок, использовать, освободить, выделить еще раз, и так 20 раз, то лучше его
выделить один раз, 20 раз использовать и один раз освободить.
Ну вот и все на сегодня. Домашее задание:
10.1. Перепишите задачу 8.2 для трехмерного массива
произвольной размерности. 10.2.* Перепишите задачу 8.2 для массива произвольной
степени вложенности и произвольной размерности. (Совет: используйте рекурсию!)