Выбрать главу

 ...

 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(), а затем дробят их на маленькие кусочки по одному за раз. Эта методика особенно полезна, если вы выделяете множество отдельных экземпляров одной и той же сравнительно небольшой структуры.