Menu

malloc w C: dynamiczna alokacja pamięci, free i sizeof

Po co istnieje sterta, jak malloc daje ci pamięć, której rozmiar ustala się w czasie działania, idiom p = malloc(n * sizeof *p), sprawdzanie NULL, zwalnianie dokładnie raz i dynamiczna tablica omówiona od początku do końca.

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

Każda tablica z dotychczasowych przykładów miała rozmiar zaszyty w czasie kompilacji: int scores[10]; rezerwuje dziesięć miejsc niezależnie od tego, czy potrzebujesz trzech, czy trzystu. To działa, dopóki rozmiar nie jest znany dopiero w czasie działania programu: ile linii ma plik, ile rekordów wprowadzi użytkownik. Pamięć dynamiczna rozwiązuje ten problem: prosisz o tyle bajtów, ile potrzebujesz, wtedy, gdy ich potrzebujesz, i oddajesz je po zakończeniu pracy.

Narzędzia są w <stdlib.h>:

#include <stdlib.h>

Po co istnieje sterta

C daje twojemu programowi dwa główne obszary pamięci. Zmienne lokalne żyją w pamięci automatycznej (na stosie): kompilator ustala ich rozmiar, a znikają w chwili, gdy ich funkcja zwraca wartość. Alokacje dynamiczne żyją na stercie: to ty decydujesz o rozmiarze w czasie działania, a blok żyje, dopóki go jawnie nie zwolnisz.

Ta druga właściwość to prawdziwy powód, by używać malloc. Funkcja może zaalokować blok, zwrócić wskaźnik, a pamięć nadal jest ważna u wywołującego, czego lokalna tablica nigdy nie potrafi. Oba obszary są szczegółowo porównane w artykule o stosie i stercie.

malloc: prośba o bajty

malloc przyjmuje liczbę bajtów i zwraca void * wskazujący na tyle bajtów niezainicjalizowanej pamięci albo NULL, jeśli żądanie się nie powiedzie.

Cztery rzeczy w tym krótkim programie to cała dyscyplina:

  1. Wyrażenie rozmiaru to n * sizeof *scores. Czytaj je jako "n sztuk tego, na co wskazuje scores".
  2. Wynik jest sprawdzany względem NULL przed jakimkolwiek użyciem.
  3. Bloku używa się dokładnie jak tablicy: scores[i] działa, bo indeksowanie to arytmetyka wskaźników.
  4. Jest zwalniany raz, a wskaźnik jest potem ustawiany na NULL.

Zwróć uwagę, że zwracana wartość nie jest rzutowana. W C void * automatycznie zamienia się na dowolny wskaźnik do obiektu, a zapis (int *)malloc(...) dodaje szumu i ukrywa brakujące dołączenie <stdlib.h>. (C++ wymaga rzutowania; C nie.)

Idiom z sizeof

Dlaczego sizeof *scores, a nie sizeof(int)? Bo nie może się zdezaktualizować.

int  *a = malloc(n * sizeof *a);     /* n intów, jakikolwiek jest tu int */
long *b = malloc(n * sizeof *b);     /* ten sam kształt linii, właściwy rozmiar */

/* krucha forma */
long *c = malloc(n * sizeof(int));   /* kompiluje się, alokuje za mało */

Ostatnia linia to prawdziwy błąd, przed którym nie ostrzega żaden kompilator: c to long *, alokacja ma rozmiar liczony dla int, a każdy zapis poza pierwszą połową wychodzi za koniec bloku. Z sizeof *c rozmiar automatycznie podąża za deklaracją.

sizeof *p nie dereferencjonuje p: sizeof jest obliczany w czasie kompilacji na podstawie samego typu, więc jest bezpieczny nawet wtedy, gdy p jest niezainicjalizowany albo równy NULL.

Niezainicjalizowana pamięć

malloc nie czyści tego, co ci daje. Bajty zawierają to, co było tam wcześniej.

Gdy chcesz dostać blok już wyzerowany, calloc robi to w jednym kroku; zobacz artykuł o calloc i realloc.

free: oddawanie pamięci

free(p) zwraca blok do alokatora. Trzy zasady:

  • Przekaż dokładnie ten wskaźnik, który zwrócił malloc, a nie przesunięty. free(p + 1) to niezdefiniowane zachowanie.
  • Zwalniaj każdy blok dokładnie raz. Dwa razy to double free, które psuje wewnętrzną ewidencję alokatora.
  • Po zwolnieniu wskaźnik jest nieaktualny. Użycie go to use-after-free.
int *p = malloc(sizeof *p);
free(p);
*p = 5;        /* use-after-free: niezdefiniowane zachowanie */
free(p);       /* double free: niezdefiniowane zachowanie */

Żadna z tych linii nie musi od razu spowodować awarii i właśnie dlatego są niebezpieczne: program może działać przez kilka minut, a potem wywrócić się w zupełnie niezwiązanym miejscu. Tania obrona to wyczyszczenie wskaźnika:

free(p);
p = NULL;

free(NULL) jest jawnie zdefiniowane jako nierobiące nic, więc drugie free(p) po czymś takim jest nieszkodliwe, a *p staje się natychmiastową awarią w miejscu prawdziwego błędu zamiast cichego uszkodzenia danych.

Tablica dynamiczna od początku do końca

Oto kształt prawdziwej alokacji: wczytaj liczbę w czasie działania, ustal na jej podstawie rozmiar bloku, użyj go, zwolnij.

Rzutowanie liczby elementów na size_t ma znaczenie, gdy rozmiary robią się duże: n * sizeof *values ze zwykłym int jako n może się przepełnić, zanim mnożenie w ogóle dotrze do malloc, dając blok znacznie mniejszy niż zamierzony. Mnożenie w size_t tego unika.

Alokacja wewnątrz funkcji

Reguła czasu życia sterty sprawia, że to jest dozwolone, a zwracanie lokalnej tablicy nie:

len + 1 zostawia miejsce na terminator zerowy: to ta sama reguła rozmiaru co wszędzie w stringach. Zwróć też uwagę na komentarz nad funkcją: gdy funkcja zwraca zaalokowaną pamięć, to, kto ją zwalnia, jest częścią jej kontraktu. Zapisanie tego to nie biurokracja; to jedyne, co chroni przed wyciekiem.

Struktury na stercie

Ten sam idiom działa dla każdego typu:

Zauważ, że druga gałąź obsługi błędu zwalnia p przed wyjściem. Gdy funkcja trzyma więcej niż jedną alokację, każda ścieżka błędu musi zwolnić te, które już zajęła: to najczęstsze źródło wycieków opisanych w artykule o wyciekach pamięci.

Lista kontrolna

  • Zawsze sprawdzaj wynik malloc względem NULL przed pierwszym użyciem.
  • Wyliczaj rozmiar jako n * sizeof *p, mnożąc w size_t.
  • W C nie rzutuj zwracanej wartości.
  • Pamięć z malloc jest niezainicjalizowana; zapisz, zanim odczytasz.
  • Zwalniaj dokładnie raz, oryginalnym wskaźnikiem, a potem ustaw go na NULL.
  • Każda ścieżka błędu po alokacji musi zwolnić to, co już trzyma.

Najczęściej zadawane pytania

Co robi malloc w C?

malloc(n) prosi o n bajtów pamięci ze sterty i zwraca wskaźnik na początek tego bloku albo NULL, jeśli żądania nie da się spełnić. Blok żyje, dopóki nie przekażesz tego samego wskaźnika do free: w przeciwieństwie do zmiennej lokalnej nie jest zwalniany, gdy funkcja zwraca wartość.

Jak używać malloc i free w C?

Zaalokuj przez int *p = malloc(n * sizeof *p);, sprawdź if (p == NULL), zanim go dotkniesz, używaj go jak tablicy, a po zakończeniu wywołaj free(p); dokładnie raz. Ustawienie potem p = NULL zamienia późniejsze przypadkowe użycie w czytelną awarię zamiast cichego uszkodzenia danych.

Dlaczego malloc(n * sizeof *p) jest lepsze niż malloc(n * sizeof(int))?

Bo sizeof *p automatycznie podąża za typem wskaźnika. Jeśli p stanie się później long * albo struct Point *, rozmiar alokacji sam się zaktualizuje; wpisanie sizeof(int) zostawia rozmiar, który teraz jest błędny, a kompilator tego nie zgłosi.

Co się stanie, jeśli nie zwolnisz pamięci w C?

Blok pozostaje zaalokowany przez cały czas życia procesu: to wyciek pamięci. Krótkiemu programowi to uchodzi na sucho, bo system operacyjny odzyskuje wszystko przy wyjściu, ale program działający długo traci trochę pamięci w każdym obiegu i w końcu ją wyczerpuje. Zobacz wycieki pamięci.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ