malloc отвечает на один вопрос: дай мне столько-то байт. Два его напарника из <stdlib.h> отвечают на вопросы, которые возникают следом: дай мне столько-то байт, очищенных (calloc) и мне нужно, чтобы уже имеющийся блок стал больше (realloc).
calloc: количество, размер и нули
void *calloc(size_t count, size_t size);
Два аргумента вместо одного, а результат заполнен нулевыми байтами.
Это классический случай: гистограмма или массив подсчётов, где каждая ячейка должна начинаться с нуля. С malloc понадобился бы цикл; calloc делает это частью выделения памяти, причём часто бесплатно, потому что операционная система и так выдаёт свежие страницы уже обнулёнными.
Аргумент про переполнение
Разделение на два аргумента — не косметика. Представьте количество, прочитанное из файла:
size_t n = huge_value_from_input;
int *a = malloc(n * sizeof *a); /* произведение может переполниться */
int *b = calloc(n, sizeof *b); /* обязан обнаружить переполнение */
Если n * sizeof *a превышает то, что помещается в size_t, значение заворачивается в маленькое число, malloc успешно возвращает крошечный блок, и каждая последующая запись уходит далеко за его конец. Стандарт требует, чтобы calloc в такой ситуации завершился неудачей и вернул NULL. Когда размер приходит извне вашей программы, такая проверка дорогого стоит.
Что на самом деле значит «обнулённая»
calloc записывает нулевые байты. Для целых чисел и символов это и есть значение ноль — то, что вам нужно. Для указателей и чисел с плавающей точкой все нулевые биты на всех распространённых платформах дают нулевой указатель и 0.0, но стандарт C этого не обещает. Код, который обязан быть строго переносимым, присваивает такие значения явно.
calloc против malloc + memset
int *a = calloc(n, sizeof *a);
int *b = malloc(n * sizeof *b);
if (b != NULL) {
memset(b, 0, n * sizeof *b);
}
Результат одинаковый. Предпочитайте calloc: это одна строка, он делает проверку на переполнение, а для больших блоков может вообще не трогать память. Предпочитайте обычный malloc, когда вы всё равно собираетесь переписать каждый байт: обнулять мегабайт, который вы тут же заполните, — чистая трата времени.
realloc: изменение размера
void *realloc(void *p, size_t newsize);
realloc возвращает блок из newsize байт, содержимое которого совпадает со старым блоком в пределах меньшего из двух размеров. Он может расширить блок на месте, а может выделить новый, скопировать данные и освободить старый. Вы не можете узнать, что именно произошло, поэтому единственный указатель, которым можно пользоваться дальше, — возвращённый.
Идиома с временным указателем
Три строки вокруг tmp — вся причина, по которой у этой функции такая репутация. Соблазнительное сокращение сломано:
/* БАГ: при неудаче realloc теряется исходный блок */
p = realloc(p, newsize);
if (p == NULL) {
return;
}
Когда realloc терпит неудачу, он возвращает NULL и оставляет исходный блок выделенным и неизменным. Присваивание результата прямо в p затирает единственный указатель на этот блок, так что освободить его уже невозможно, — это утечка, и происходит она ровно тогда, когда памяти и так не хватает. Присвойте в tmp, проверьте tmp, а потом зафиксируйте результат:
void *tmp = realloc(p, newsize);
if (tmp == NULL) {
/* p по-прежнему валиден; обработайте сбой так, как удобно вызывающей стороне */
return 0;
}
p = tmp;
Ещё две особенности, которые стоит знать: realloc(NULL, n) ведёт себя в точности как malloc(n), что позволяет функции роста обрабатывать первый вызов без отдельной ветки. И при увеличении новые байты остаются неинициализированными — сохраняется только старое содержимое.
Рост массива: стандартная схема
Вот схема, которую использует почти любой контейнер на C: длина, ёмкость и удвоение, когда они сравниваются.
Здесь всю нагрузку несут два проектных решения.
Удвоение, а не прибавление единицы. Рост на фиксированную величину заставляет каждую вставку копировать весь массив, так что заполнение n элементов обходится примерно в n² копирований байт. Удвоение делает копирования достаточно редкими, чтобы каждая вставка в среднем стоила константное время. Начало cap == 0 ? 4 : cap * 2 обрабатывает первую вставку без отдельной ветки, потому что realloc(NULL, n) — это просто malloc.
Структура владеет ровно одним выделением. v.data освобождается ровно один раз — тем, кто владеет структурой. Записать это правило владения (в комментарии или спарив каждый vec_init с vec_free) — именно то, что не даёт растущему контейнеру потечь по мере путешествия между функциями.
Уменьшение
realloc с меньшим размером обрезает блок — это полезно после того, как вы прочитали неизвестный заранее объём данных в буфер с запасом:
Неудавшееся уменьшение — не та ошибка, ради которой стоит прерывать работу: старый, больший блок по-прежнему валиден и корректен. Это единственный случай, когда игнорировать неудачу правильно, — но заметьте, что и он идёт через tmp, потому что правило «не затирать живой указатель» не меняется.
К чему тянуться
| Что вам нужно | Что использовать |
|---|---|
| Байты, содержимое неважно | malloc(n * sizeof *p) |
| Количество элементов, очищенных в ноль | calloc(n, sizeof *p) |
| Количество из недоверенного источника | calloc, ради проверки переполнения |
| Имеющийся блок, больше или меньше | realloc через указатель tmp |
| Первое выделение в цикле роста | realloc(NULL, n) — без отдельной ветки |
Все четыре освобождаются одним и тем же free, ровно один раз.
Частые ошибки
p = realloc(p, n)— утечка при неудаче. Всегда идите черезtmp.- Сохранение старого указателя внутрь блока. После
reallocданные могли переехать, так что любой сохранённый указатель внутрь блока или адрес, полученный по индексу, устарел. Индексы переезд переживают, указатели — нет. - Предположение, что новые байты нулевые. Обнуляет только
calloc.reallocоставляет область роста неинициализированной. calloc(n * size, 1)— так вы выбрасываете ту самую проверку переполнения, ради которой существует форма с двумя аргументами.- Освобождение старого указателя после успешного
realloc.reallocуже освободил его; второйfree— это двойное освобождение.
Часто задаваемые вопросы
В чём разница между calloc и malloc в C?
Отличий два. calloc(n, size) принимает количество и размер элемента по отдельности и проверяет, что их произведение не переполнится, тогда как malloc(n * size) может тихо переполниться. И calloc заполняет блок нулями, а malloc оставляет в нём то, что там было раньше.
Как работает realloc в C?
realloc(p, newsize) возвращает блок нового размера, в котором сохранено ваше прежнее содержимое — в пределах меньшего из двух размеров. Блок может быть расширен на месте, а может быть перемещён, поэтому дальше можно пользоваться только возвращённым указателем: старый может быть уже освобождён.
Почему результат realloc нужно присваивать во временный указатель?
Потому что p = realloc(p, n) записывает в p значение NULL при неудаче, а исходный блок остаётся выделенным, и на него больше ничего не указывает — это утечка. Присвойте результат в tmp, проверьте его на NULL и только потом напишите p = tmp.
Когда стоит использовать calloc вместо malloc?
Когда вам действительно нужна обнулённая память (массив счётчиков, структура, поля которой должны начинаться пустыми, буфер, который вы заполните частично) или когда размер — это количество, умноженное на размер элемента, и оно может оказаться большим. Если вы всё равно собираетесь переписать каждый байт, malloc избавит вас от лишнего обнуления.