Menu

calloc i realloc w C: wyzerowana pamięć i powiększanie tablicy

calloc daje wyzerowaną pamięć i bezpiecznie mnoży rozmiar; realloc powiększa blok, który jest już wypełniony. Ta strona omawia obie funkcje, idiom z tymczasowym wskaźnikiem, który chroni przed wyciekiem po nieudanym realloc, oraz to, kiedy calloc wygrywa z malloc plus memset.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

malloc odpowiada na jedno pytanie: daj mi tyle bajtów. Dwie towarzyszące mu funkcje z <stdlib.h> odpowiadają na kolejne pytania: daj mi tyle bajtów, wyczyszczonych (calloc) oraz potrzebuję, żeby blok, który już mam, był większy (realloc).

calloc: liczba, rozmiar i zera

void *calloc(size_t count, size_t size);

Dwa argumenty zamiast jednego, a wynik jest wypełniony bajtami zerowymi.

To klasyczny przypadek: histogram albo tablica liczników, w której każde pole musi zaczynać od zera. Z malloc potrzebna byłaby pętla; calloc robi to w ramach alokacji, często za darmo, bo system operacyjny i tak wydaje świeże strony pamięci już wyzerowane.

Argument o przepełnieniu

Podział na dwa argumenty nie jest kosmetyczny. Weźmy liczbę elementów odczytaną z pliku:

size_t n = huge_value_from_input;

int *a = malloc(n * sizeof *a);     /* iloczyn może się przekręcić */
int *b = calloc(n, sizeof *b);      /* musi wykryć przepełnienie */

Jeśli n * sizeof *a przekroczy to, co mieści size_t, wynik przekręca się do małej liczby, malloc kończy się sukcesem z malutkim blokiem, a każdy kolejny zapis wychodzi daleko za jego koniec. Standard wymaga, żeby calloc w takiej sytuacji zawiódł i zwrócił NULL. Gdy rozmiar pochodzi spoza twojego programu, to sprawdzenie jest warte zachodu.

Co naprawdę znaczy "wyzerowane"

calloc zapisuje zerowe bajty. Dla liczb całkowitych i znaków to wartość zero, czyli to, czego chcesz. Dla wskaźników i liczb zmiennoprzecinkowych wszystkie bity równe zero to wskaźnik pusty i 0.0 na każdej popularnej platformie, ale standard C tego nie obiecuje. Kod, który musi być ściśle przenośny, przypisuje te wartości jawnie.

calloc a malloc + memset

int *a = calloc(n, sizeof *a);

int *b = malloc(n * sizeof *b);
if (b != NULL) {
    memset(b, 0, n * sizeof *b);
}

Dają ten sam wynik. Wybieraj calloc: to jedna linia, robi sprawdzenie przepełnienia, a przy dużych blokach może w ogóle nie dotykać pamięci. Wybieraj zwykły malloc, gdy i tak zaraz nadpiszesz każdy bajt: zerowanie megabajta, który zaraz wypełnisz, to czyste marnotrawstwo.

realloc: zmiana rozmiaru

void *realloc(void *p, size_t newsize);

realloc zwraca blok o rozmiarze newsize bajtów, którego zawartość zgadza się ze starym blokiem do mniejszego z dwóch rozmiarów. Może rozszerzyć blok tam, gdzie leży, albo zaalokować nowy, skopiować dane i zwolnić stary. Nie da się stwierdzić, co zrobił, więc jedynym wskaźnikiem, którego możesz potem używać, jest ten, który zwrócił.

Idiom ze wskaźnikiem tmp

Trzy linie wokół tmp to cały powód, dla którego ta funkcja ma swoją reputację. Kuszący skrót jest błędny:

/* BUG: gubi oryginalny blok (wyciek), gdy realloc zawiedzie */
p = realloc(p, newsize);
if (p == NULL) {
    return;
}

Gdy realloc zawodzi, zwraca NULL i zostawia oryginalny blok zaalokowany i niezmieniony. Przypisanie wyniku prosto do p nadpisuje jedyny wskaźnik na ten blok, więc nie da się go już zwolnić: to wyciek, i to taki, który zdarza się dokładnie wtedy, gdy pamięci już brakuje. Przypisz do tmp, sprawdź tmp, a potem zatwierdź:

void *tmp = realloc(p, newsize);
if (tmp == NULL) {
    /* p nadal jest ważny; obsłuż błąd tak, jak pasuje wywołującemu */
    return 0;
}
p = tmp;

Dwa kolejne zachowania warte poznania: realloc(NULL, n) działa dokładnie jak malloc(n), dzięki czemu funkcja powiększająca obsłuży pierwsze wywołanie bez specjalnego przypadku. Poza tym przy powiększaniu nowe bajty pozostają niezainicjalizowane: zachowywana jest tylko stara zawartość.

Powiększanie tablicy: standardowy wzorzec

Oto wzorzec, którego używa prawie każdy kontener w C: długość, pojemność i podwajanie, gdy się zrównają.

Całą robotę wykonują tu dwie decyzje projektowe.

Podwajanie, a nie dodawanie jednego. Powiększanie o stałą wartość sprawia, że każde dodanie kopiuje całą tablicę, więc wypełnienie n elementów kosztuje mniej więcej n² kopiowań. Podwajanie sprawia, że kopiowania są na tyle rzadkie, że każde dodanie kosztuje średnio stały czas. Start cap == 0 ? 4 : cap * 2 obsługuje pierwsze dodanie bez osobnej gałęzi, bo realloc(NULL, n) to po prostu malloc.

Struktura jest właścicielem jednej alokacji. v.data jest zwalniane dokładnie raz, przez tego, kto jest właścicielem struktury. Zapisanie tej reguły własności, w komentarzu albo przez łączenie każdego vec_init z vec_free, to właśnie to, co chroni rosnący kontener przed wyciekiem, gdy wędruje między funkcjami.

Zmniejszanie

realloc z mniejszym rozmiarem przycina blok, co przydaje się po wczytaniu nieznanej ilości danych do zbyt dużego bufora:

Nieudane zmniejszenie nie jest błędem, dla którego warto przerywać program: stary, większy blok nadal jest ważny i nadal poprawny. To jedyny przypadek, w którym zignorowanie błędu jest właściwą decyzją, ale zauważ, że i tak idzie to przez tmp, bo reguła o nienadpisywaniu żywego wskaźnika się nie zmienia.

Po co sięgnąć

PotrzebujeszUżyj
Bajtów, zawartość bez znaczeniamalloc(n * sizeof *p)
Określonej liczby elementów, wyzerowanychcalloc(n, sizeof *p)
Liczby elementów z niezaufanego wejściacalloc, ze względu na sprawdzenie przepełnienia
Bloku, który już masz, większego albo mniejszegorealloc przez wskaźnik tmp
Pierwszej alokacji w pętli powiększaniarealloc(NULL, n): bez specjalnego przypadku

Wszystkie cztery zwalnia ten sam free, dokładnie raz.

Częste błędy

  • p = realloc(p, n): wyciek przy niepowodzeniu. Zawsze idź przez tmp.
  • Trzymanie starego wskaźnika do wnętrza bloku. Po realloc dane mogły się przenieść, więc każdy zapamiętany wskaźnik do środka bloku czy adres wyliczony z indeksu jest nieaktualny. Indeksy przetrwają przeprowadzkę; wskaźniki nie.
  • Założenie, że nowe bajty są zerami. Zeruje tylko calloc. realloc zostawia dodany obszar niezainicjalizowany.
  • calloc(n * size, 1): to wyrzuca sprawdzenie przepełnienia, dla którego istnieje forma z dwoma argumentami.
  • Zwalnianie starego wskaźnika po udanym realloc. realloc już go zwolnił; drugi free to podwójne zwolnienie.

Najczęściej zadawane pytania

Jaka jest różnica między calloc a malloc w C?

Są dwie różnice. calloc(n, size) przyjmuje liczbę elementów i rozmiar elementu osobno i sprawdza, czy ich iloczyn się nie przepełnia, a malloc(n * size) może się po cichu przekręcić. Poza tym calloc wypełnia blok zerami, a malloc zostawia w nim to, co było tam wcześniej.

Jak działa realloc w C?

realloc(p, newsize) zwraca blok o nowym rozmiarze z zachowaną dotychczasową zawartością, do mniejszego z dwóch rozmiarów. Może powiększyć blok w miejscu albo go przenieść, więc jedynym wskaźnikiem, którego możesz dalej używać, jest ten zwrócony: stary mógł już zostać zwolniony.

Dlaczego wynik realloc trzeba przypisać do tymczasowego wskaźnika?

Bo p = realloc(p, n) nadpisuje p wartością NULL, gdy wywołanie się nie powiedzie, a oryginalny blok nadal jest zaalokowany i nic na niego nie wskazuje: to wyciek. Przypisz najpierw do tmp, sprawdź, czy nie jest NULL, i dopiero wtedy napisz p = tmp.

Kiedy używać calloc zamiast malloc?

Gdy naprawdę potrzebujesz wyzerowanej pamięci (tablica liczników, struktura, której pola mają zaczynać puste, bufor, który wypełnisz częściowo) albo gdy rozmiar to liczba elementów razy rozmiar elementu, która może być duża. Jeśli i tak nadpiszesz każdy bajt, malloc oszczędzi ci pracy zerowania.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ