...
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()
лишь выделяет новую память и больше ничего не делает.
3.2.1.5. Выделение с инициализацией нулями: 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, '\0', total); /* Заполнить ее нулями */
return p; /* Возвращаемое значение NULL или указатель */
}
Многие опытные программисты предпочитают использовать calloc()
, поскольку в этом случае никогда не возникает вопросов по поводу вновь выделенной памяти.
Если вы знаете, что вам понадобится инициализированная нулями память, следует также использовать calloc()
, поскольку возможно, что память, возвращенная malloc()
, уже заполнена нулями. Хотя вы, программист, не можете этого знать, calloc()
может это знать и избежать лишнего вызова memset()
.
3.2.1.6. Подведение итогов из GNU Coding Standards
Чтобы подвести итоги, процитируем, что говорит об использовании процедур выделения памяти GNU Coding Standards:
Проверяйте каждый вызов malloc
или realloc
на предмет возвращенного нуля. Проверяйте realloc
даже в том случае, если вы уменьшаете размер блока; в системе, которая округляет размеры блока до степени двойки, realloc
может получить другой блок, если вы запрашиваете меньше памяти.
В Unix realloc
может разрушить блок памяти, если она возвращает ноль. GNU realloc
не содержит подобной ошибки: если она завершается неудачей, исходный блок остается без изменений. Считайте, что ошибка устранена. Если вы хотите запустить свою программу на Unix и хотите избежать потерь в этом случае, вы можете использовать GNU malloc
.
Вы должны считать, что free
изменяет содержимое освобожденного блока. Все, что вы хотите получить из блока, вы должны получать до вызова free
.
В этих трех коротких абзацах Ричард Столмен (Richard Stallman) выразил суть важных принципов управления динамической памятью с помощью malloc()
. Именно использование динамической памяти и принцип «никаких произвольных ограничений» делают программы GNU такими устойчивыми и более работоспособными по сравнению с их Unix-двойниками.
Мы хотим подчеркнуть, что стандарт С требует, чтобы realloc()
не разрушал оригинальный блок памяти, если она возвращает NULL
.
3.2.1.7. Использование персональных программ распределения
Набор функций с malloc()
является набором общего назначения по выделению памяти. Он должен быть способен обработать запросы на произвольно большие или маленькие размеры памяти и осуществлять все необходимые учетные действия при освобождении различных участков выделенной памяти. Если ваша программа выделяет значительную динамическую память, вы можете обнаружить, что она тратит большую часть своего времени в функциях malloc()
.
Вы можете написать персональную программу распределения — набор функций или макросов, которые выделяют большие участки памяти с помощью malloc()
, а затем дробят их на маленькие кусочки по одному за раз. Эта методика особенно полезна, если вы выделяете множество отдельных экземпляров одной и той же сравнительно небольшой структуры.