Книга: Linux программирование в примерах
3.2.1.4. Изменение размера: realloc()
3.2.1.4. Изменение размера: 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]. Рассмотрим процедуру, которая содержит статический указатель на динамически выделяемые данные, которые время от времени должны расти. Процедура может содержать также автоматические (т.е. локальные) указатели на эти данные. (Для краткости, мы опустим проверки ошибок. В коде продукта не делайте этого.) Например:
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()
лишь выделяет новую память и больше ничего не делает.
- 3.2.1.1. Исследование подробностей на языке С
- 3.2.1.2. Начальное выделение памяти: malloc()
- 3.2.1.3. Освобождение памяти: free()
- 3.2.1.4. Изменение размера: realloc()
- 3.2.1.5. Выделение с инициализацией нулями: calloc()
- 3.2.1.6. Подведение итогов из GNU Coding Standards
- 3.2.1.7. Использование персональных программ распределения
- 3.2.1.8. Пример: чтение строк произвольной длины
- 3.2.1.9. Только GLIBC: чтение целых строк: getline() и getdelim()
- 3.2.1. Библиотечные вызовы: malloc(), calloc(), realloc(), free()
- Увеличение размера теневой копии
- Уменьшение размера, занимаемого индексами
- Добавление, изменение и удаление элементов списка
- Добавление, изменение и удаление столбцов списка
- Добавление и изменение представления списка
- Часть II Изменение способов ведения бизнеса
- Изменение состояния виртуальной машины
- 8.2. Изменение пароля доступа к маршрутизатору
- 3.6.3. Изменение параметров сетевого подключения
- 4.3.2. Изменение настроек по умолчанию
- Изменение начала координат