Текст книги "Linux программирование в примерах"
Автор книги: Арнольд Роббинс
Жанры:
Программирование
,сообщить о нарушении
Текущая страница: 7 (всего у книги 55 страниц)
realloc()
Динамическая память имеет существенное преимущество перед статически объявленными массивами, поскольку это позволяет использовать столько памяти, сколько нужно, и не больше. Не нужно объявлять глобальный, статический или локальный массив фиксированного размера и надеяться, что он: (а) достаточно большой и (б) не слишком большой. Вместо этого можно выделить ровно столько, сколько нужно, не больше и не меньше.
Вдобавок, можно изменять размер динамически выделенной области памяти. Хотя можно сократить размер блока памяти, обычно его увеличивают. Изменение размера осуществляется с помощью realloc()
. Продолжая пример с coordinates
, типичный код выглядит следующим образом:
int new_count;
size_t new_amount;
struct coord *newcoords; /* установить, например: */
new_count = count * 2; /* удвоить размер памяти */
new_amount = new_count * sizeof(struct coord);
newcoords =
(struct coord*)realloc(coordinates, new_amount);
if (newcoords == NULL) {
/* сообщить об ошибке, восстановить или прервать */
}
coordinates = newcoords;
/* продолжить использование coordinates ... */
Как и в случае с malloc()
, шаги стереотипны по природе и сходны по идее.
1. Вычислить новый выделяемый размер в байтах.
2. Вызвать realloc()
с оригинальным указателем, полученным от malloc()
(или от calloc()
или предыдущего вызова realloc()
) и с новым размером.
3. Привести тип и присвоить возвращенное realloc()
значение. Подробнее обсудим дальше.
4. Как и для malloc()
, проверить возвращенное значение, чтобы убедиться, что оно не равно NULL. Вызов любой функции выделения памяти может завершиться неудачей.
При увеличении размера блока памяти realloc()
часто выделяет новый блок нужного размера, копирует данные из старого блока в новый и возвращает указатель уже на новый блок. При сокращении размера блока данных realloc()
часто обновляет внутреннюю учетную информацию и возвращает тот же указатель. Это избавляет от необходимости копировать первоначальные данные. Однако, если это случится, не думайте, что можно использовать память за пределами нового размера!
В любом случае вы можете предположить, что если realloc()
не возвращает NULL
, старые данные были скопированы для вас в новый участок памяти. Более того, старый указатель больше недействителен, как если бы вы вызвали free()
с ним, и использовать его больше не следует. Это верно для всех указателей на этот блок данных, а не только для того, который использовался при вызове free()
.
Возможно, вы заметили, что в нашем примере для указания на измененный блок памяти использовалась отдельная переменная. Можно было бы (хотя это плохая идея) использовать ту же самую переменную, как здесь:
coordinates = realloc(coordinates, new_amount);
Это плохо по следующей причине. Когда realloc()
возвращает NULL
, первоначальный указатель все еще действителен; можно безопасно продолжить использовать эту память. Но если вы повторно используете ту же самую переменную и realloc()
возвращает NULL
, вы теряете указатель на первоначальную память. Эту память больше нельзя использовать. Что еще важнее, эту память невозможно освободить! Это создает утечку памяти, которую нужно избежать.
Для версии realloc()
в стандартном С есть некоторые особые случаи: когда аргумент ptr
равен NULL
, realloc()
действует подобно malloc()
и выделяет свежий блок памяти. Когда аргумент size
равен 0, realloc()
действует подобно free()
и освобождает память, на которую указывает ptr
. Поскольку (а) это может сбивать с толку и (б) более старые системы не реализуют эту возможность, мы рекомендуем использовать malloc()
, когда вы имеете в виду malloc()
, и free()
, когда вы имеете в виду free()
.
Вот другой довольно тонкий момент[42]42
Он получен от реальной практики работы с gawk
– Примеч. автора.
[Закрыть]. Рассмотрим процедуру, которая содержит статический указатель на динамически выделяемые данные, которые время от времени должны расти. Процедура может содержать также автоматические (т.е. локальные) указатели на эти данные. (Для краткости, мы опустим проверки ошибок. В коде продукта не делайте этого.) Например:
void manage_table(void) {
static struct table *table;
struct table *cur, *p;
int i;
size_t count;
...
table =
(struct table*)malloc(count * sizeof(struct table));
/* заполнить таблицу */
cur = &table[i]; /* указатель на 1-й элемент */
...
cur->i = j; /* использование указателя */
...
if (/* некоторое условие */) {
/* нужно увеличить таблицу */
count += count/2;
p =
(struct table*)realloc(table, count * sizeof(struct table));
table = p;
}
cur->i = j; /* ПРОБЛЕМА 1: обновление элемента таблицы */
other_routine(); /* ПРОБЛЕМА 2: см. текст */
cur->j = k; /* ПРОБЛЕМА 2: см. текст */
...
}
Это выглядит просто; manage_table()
размешает данные, использует их, изменяет размер и т.д. Но есть кое-какие проблемы, которые не выходят за рамки страницы (или экрана), когда вы смотрите на этот код.
В строке, помеченной 'ПРОБЛЕМА 1
', указатель cur используется для обновления элемента таблицы. Однако, cur
был инициализирован начальным значением table
. Если некоторое условие верно и realloc()
вернула другой блок памяти, cur
теперь указывает на первоначальный, освобожденный участок памяти! Каждый раз, когда table
меняется, нужно обновить также все указатели на этот участок памяти. Здесь после вызова realloc()
и переназначения table
недостает строки 'cur = &table[i];
'.
Две строки, помеченные 'ПРОБЛЕМА 2
', еще более тонкие. В частности, предположим, что other_routine()
делает рекурсивный вызов manage_table()
. Переменная table
снова может быть изменена совершенно незаметно! После возвращения из other_routine()
значение cur может снова стать недействительным.
Можно подумать (что мы вначале и сделали), что единственным решением является знать это и добавить после вызова функции переназначение cur
с соответствующим комментарием. Однако, Брайан Керниган (Brian Kernighan) любезно нас поправил. Если мы используем индексирование, проблема поддержки указателя даже не возникает:
table =
(struct table*)malloc(count * sizeof(struct table));
...
/* заполнить таблицу */
...
table[i].i = j; /* Обновить член i-го элемента */
...
if (/* некоторое условие */) {
/* нужно увеличить таблицу */
count += count/2;
p =
(struct table*)realloc(table, count * sizeof(struct table));
table = p;
}
table[i].i = j; /* ПРОБЛЕМА 1 устраняется */
other_routine();
/* Рекурсивный вызов, модифицирует таблицу */
table[i].j = k; /* ПРОБЛЕМА 2 также устраняется */
Использование индексирования не решает проблему, если вы используете глобальную копию первоначального указателя на выделенные данные; в этом случае, вам все равно нужно побеспокоиться об обновлении своих глобальных структур после вызова realloc()
.
ЗАМЕЧАНИЕ. Как и в случае с
malloc()
, когда вы увеличиваете размер памяти, вновь выделенная послеrealloc()
память не инициализируется нулями. Вы сами при необходимости должны очистить память с помощьюmemset()
, посколькуrealloc()
лишь выделяет новую память и больше ничего не делает.
calloc()
Функция calloc()
является простой оболочкой вокруг malloc()
. Главным ее преимуществом является то, что она обнуляет динамически выделенную память. Она также вычисляет за вас размер памяти, принимая в качестве параметра число элементов и размер каждого элемента:
coordinates = (struct coord*)calloc(count, sizeof(struct coord));
По крайней мере идейно, код calloc()
довольно простой. Вот одна из возможных реализаций:
void *calloc(size_t nmemb, size_t size) {
void *p;
size_t total;
total = nmemb * size; /* Вычислить размер */
p = malloc(total); /* Выделить память */
if (p != NULL) /* Если это сработало – */
memset(p, ' ', total); /* Заполнить ее нулями */
return p; /* Возвращаемое значение NULL или указатель */
}
Многие опытные программисты предпочитают использовать calloc()
, поскольку в этом случае никогда не возникает вопросов по поводу вновь выделенной памяти.
Если вы знаете, что вам понадобится инициализированная нулями память, следует также использовать calloc()
, поскольку возможно, что память, возвращенная malloc()
, уже заполнена нулями. Хотя вы, программист, не можете этого знать, calloc()
может это знать и избежать лишнего вызова memset()
.
Чтобы подвести итоги, процитируем, что говорит об использовании процедур выделения памяти GNU Coding Standards:
Проверяйте каждый вызов
malloc
илиrealloc
на предмет возвращенного нуля. Проверяйтеrealloc
даже в том случае, если вы уменьшаете размер блока; в системе, которая округляет размеры блока до степени двойки,realloc
может получить другой блок, если вы запрашиваете меньше памяти.В Unix
realloc
может разрушить блок памяти, если она возвращает ноль. GNUrealloc
не содержит подобной ошибки: если она завершается неудачей, исходный блок остается без изменений. Считайте, что ошибка устранена. Если вы хотите запустить свою программу на Unix и хотите избежать потерь в этом случае, вы можете использовать GNUmalloc
.Вы должны считать, что
free
изменяет содержимое освобожденного блока. Все, что вы хотите получить из блока, вы должны получать до вызоваfree
.
В этих трех коротких абзацах Ричард Столмен (Richard Stallman) выразил суть важных принципов управления динамической памятью с помощью malloc()
. Именно использование динамической памяти и принцип «никаких произвольных ограничений» делают программы GNU такими устойчивыми и более работоспособными по сравнению с их Unix-двойниками.
Мы хотим подчеркнуть, что стандарт С требует, чтобы realloc()
не разрушал оригинальный блок памяти, если она возвращает NULL
.
Набор функций с malloc()
является набором общего назначения по выделению памяти. Он должен быть способен обработать запросы на произвольно большие или маленькие размеры памяти и осуществлять все необходимые учетные действия при освобождении различных участков выделенной памяти. Если ваша программа выделяет значительную динамическую память, вы можете обнаружить, что она тратит большую часть своего времени в функциях malloc()
.
Вы можете написать персональную программу распределения – набор функций или макросов, которые выделяют большие участки памяти с помощью malloc()
, а затем дробят их на маленькие кусочки по одному за раз. Эта методика особенно полезна, если вы выделяете множество отдельных экземпляров одной и той же сравнительно небольшой структуры.
Например, GNU awk (gawk) использует эту методику. Выдержка из файла awk.h
в дистрибутиве gawk
(слегка отредактировано, чтобы уместилось на странице):
#define getnode(n) if (nextfree) n = nextfree,
nextfree = nextfree->nextp; else n = more_nodes()
#define freenode(n) ((n)->flags = 0, (n)->exec_count = 0,
(n)->nextp = nextfree, nextfree = (n))
Переменная nextfree
указывает на связанный список структур NODE. Макрос getnode()
убирает из списка первую структуру, если она там есть. В противном случае она вызывает more_nodes()
, чтобы выделить новый список свободных структур NODE
. Макрос freenode()
освобождает структуру NODE
, помещая его в начало списка.
ЗАМЕЧАНИЕ. Первоначально при написании своего приложения делайте это простым способом: непосредственно используйте
malloc()
иfree()
. Написание собственного распределителя вы должны рассмотреть лишь в том и только в том случае, если профилирование вашей программы покажет, что она значительную часть времени проводит в функциях выделения памяти.
Поскольку это, в конце концов, Программирование на Linux в примерах, настало время для примера из реальной жизни. Следующий код является функцией readline()
из GNU Make 3.80 (ftp://ftp.gnu.org/gnu/make/make-3.80.tar.gz
). Ее можно найти в файле read.c
.
Следуя принципу «никаких произвольных ограничений», строки в Makefile
могут быть любой длины. Поэтому главной задачей этой процедуры является чтение строк произвольной длины и гарантирование того, что они помещаются в используемый буфер.
Вторичной задачей является распоряжение продлением строк. Как и в С, строки, заканчивающиеся обратным слешем, логически продолжаются со следующей строки. Используется стратегия поддержания буфера. В нем хранится столько строк, сколько помещается в буфер, причем указатели отслеживают начало буфера, текущую строку и следующую строку. Вот структура:
struct ebuffer {
char *buffer; /* Начало текущей строки в буфере. */
char *bufnext; /* Начало следующей строки в буфере. */
char *bufstart; /* Начало всего буфера. */
unsigned int size; /* Размер буфера для malloc. */
FILE *fp; /* Файл или NULL, если это внутренний буфер. */
struct floc floc; /* Информация о файле в fp (если он есть). */
};
Поле size
отслеживает размер всего буфера, a fp
является указателем типа FILE
для файла ввода. Структура floc не представляет интереса при изучении процедуры.
Функция возвращает число строк в буфере. (Номера строк здесь даны относительно начала функции, а не исходного файла.)
1 static long
2 readline(ebuf) /* static long readline(struct ebuffer *ebuf) */
3 struct ebuffer *ebuf;
4 {
5 char *p;
6 char *end;
7 char *start;
8 long nlines = 0;
9
10 /* Использование строковых буферов и буферов потоков достаточно
11 различается, чтобы использовать разные функции. */
12
13 if (!ebuf->fp)
14 return readstring(ebuf);
15
16 /* При чтении из файла для каждой новой строки мы всегда
17 начинаем с начала буфера. */
18
19 p = start = ebuf->bufstart;
20 end = p + ebuf->size;
21 *p = ' ';
Для начала заметим, что GNU Make написан на С K&R для максимальной переносимости. В исходной части объявляются переменные, и если ввод осуществляется из строки (как в случае расширения макроса), код вызывает другую функцию, readstring()
(строки 13 и 14). Строка '!ebuf->fp
' (строка 13) является более короткой (и менее понятной, по нашему мнению) проверкой на пустой указатель; это то же самое, что и 'ebuf->fp==NULL
'.
Строки 19-21 инициализируют указатели и вводят байт NUL, который является символом завершения строки С в конце буфера. Затем функция входит в цикл (строки 23–95), который продолжается до завершения всего ввода.
23 while (fgets(p, end – р, ebuf->fp) != 0)
24 {
25 char *p2;
26 unsigned long len;
27 int backslash;
28
29 len = strlen(p);
30 if (len == 0)
31 {
32 /* Это случается лишь тогда, когда первый символ строки ' '.
33 Это довольно безнадежный случай, но (верите или нет) ляп Афины
34 бьет снова! (xmkmf помещает NUL в свои makefile.)
35 Здесь на самом деле нечего делать; мы создаем новую строку, чтобы
36 следующая строка не была частью данной строки. */
37 error (&ebuf->floc,
38 _("warning: NUL character seen; rest of line ignored"));
39 p[0] = 'n';
40 len = l;
41 }
Функция fgets()
(строка 23) принимает указатель на буфер, количество байтов для прочтения и переменную FILE*
для файла, из которого осуществляется чтение. Она читает на один байт меньше указанного, чтобы можно было завершить буфер символом '