Jak malloc i free zarządzają pamięcią dynamiczną w C

13

Alokacja pamięci w locie polega na tym, jak programy C rosną i kurczą się podczas wykonywania. Nie znasz dokładnych potrzeb swojej aplikacji, dopóki nie zacznie ona działać. Rozwiązaniem jest użycie sterty. Żądasz blokady. System operacyjny rezerwuje to. Używasz tego. Następnie zwracasz go z opcją „za darmo”. Ta pętla umożliwia innym programom ponowne wykorzystanie tej samej pamięci RAM.

Rozważmy następujący prosty przykład. Jest to najbardziej podstawowa interakcja ze stertą, jaka jest możliwa.

Jak faktycznie działa malloc

Funkcja „malloc” nie jest magią. To jest mechanik sprawdzający zapasy. Kiedy to wywołasz, zostaną wykonane kolejno trzy akcje.

  1. Sprawdza aktualną pojemność sterty. Sprawdza przekazany parametr – w tym przypadku sizeof(int), który wynosi 4 bajty. Pyta, czy jest wystarczająco dużo ciągłej przestrzeni. Jeśli sterta jest pofragmentowana lub pełna, operacja kończy się niepowodzeniem.
  2. Jeśli jest miejsce, rezerwuje ten blok. System oznacza go jako „Twój”. Żadne inne wywołanie malloc nie może dotknąć tego fragmentu. Zapobiega to przypadkowym kolizjom między alokacjami pamięci.
  3. Zwraca adres tego bloku. Nie podaje samych danych. Daje zmienną wskaźnikową. Ta zmienna zawiera lokalizację. Teraz masz link do określonej lokalizacji w pamięci RAM.

W naszym przykładzie p otrzymuje ten adres. Zmienna wskaźnikowa to po prostu pojemnik na adres pamięci. Rzeczywista wartość całkowita znajduje się pod tym adresem.

Dlaczego konieczne jest sprawdzenie NULL

Zaraz po przydzieleniu pamięci zobaczysz sprawdzenie „if (p == 0)”. Niektórzy początkujący pomijają ten krok. To fatalny błąd.

Zawsze sprawdzaj wskaźnik po każdym wywołaniu malloc, aby upewnić się, że wskaźnik jest prawidłowy.

Rozmiar sterty stale się zmienia. To zależy od tego, jakie inne procesy są uruchomione. To zależy od tego, ile pamięci przejęli. Nie ma gwarancji, że połączenie zakończy się pomyślnie. Jeśli malloc nie powiedzie się, zwraca zero. Zero to „NULL”. Jeśli spróbujesz wyłuskać wskaźnik zerowy, program ulegnie awarii. Twardy.

Możesz napisać if (p == NULL) lub if (!p). Mają na myśli to samo. Ale musisz sprawdzić. Za każdym razem.

Co się stanie, jeśli zapomnisz zwolnić pamięć?

Jeśli program zakończy działanie bez wywołania opcji „wolny”, system operacyjny wszystko za Ciebie oczyści. Zwalnia miejsce w pliku wykonywalnym. Czyści stos. Zwraca pamięć globalną. Wykorzystuje również wszelkie oczekujące alokacje na stercie.

Czy to oznacza, że ​​wyciek pamięci po zakończeniu programu jest zjawiskiem normalnym? Technicznie tak. Trwa czyszczenie systemu operacyjnego. Nie ma długoterminowych konsekwencji dla samochodu.

Ale to jest zła forma. A co ważniejsze, wycieki pamięci podczas wykonywania są zabójcze. Jeśli będziesz nadal alokować pamięć bez jej zwalniania, dławisz swój własny proces. Sterta się zapełnia. Nowe przydziały nie powiodą się. Program umiera w wyniku uduszenia, a nie w wyniku awarii.

Wskaźniki a wartości, na które wskazują

Wskazówki mogą być mylące. Różnią się od wartości, do których się odnoszą. Oto jak to wygląda w praktyce.

Pierwszy scenariusz. Wybierasz jeden blok. Wskazujesz na to dwie zmienne.

Wniosek:
10
20

Dlaczego? p i q to różne wskaźniki. Ale „q = p” wskazuje na ten sam blok pamięci. Zmiana wartości p powoduje zmianę jej również na q. Zmiana go na q powoduje również zmianę na p. Są to dwie klamki jednych drzwi.

Drugi scenariusz. Wybierasz dwa oddzielne bloki.

Wniosek:
20

Tutaj „p” i „q” wskazują różne adresy. *q zawiera 20. *p = *q kopiuje wartość 20 do lokalizacji wskazanej przez p. Sam wskaźnik „p” nie porusza się. Wartość pod adresem p ulega zmianie.

To rozróżnienie ma znaczenie. Zmienia się adres zapisany w zmiennej. Inny zmienia dane pod tym adresem. Zamieszanie między nimi prowadzi do błędów logicznych, które są trudniejsze do debugowania niż błędy składniowe.

Wskaźniki, pamięć i swobodny spadek

Wynik programu: 20. Wiersz 6 jest oczywisty.

Możesz pomyśleć, że *p = *q i p = q robią to samo. To jest błędne. Jedna akcja przenosi dane, druga przenosi adresy. Kompilator pozwala na przypisanie *p = *q, ponieważ oba wyłuskane wskaźniki są typu int. Jest to bezpośrednia wymiana znaczeń. Ale p = q? To jest coś innego. Zmusza „p” do wskazania tego samego bloku pamięci co „q”. Adres został przeniesiony. Dane pozostają na swoim miejscu.

Jeśli typy nie są zgodne, kompilator blokuje operację. Wskaźnik do liczby całkowitej nie może wskazywać na ciąg znaków. Typy muszą się zgadzać.

Cztery sposoby inicjalizacji

Nowa zmienna wskaźnikowa jest niebezpieczna, dopóki nie zostanie zainicjowana.

int *p; tworzy wskaźnik, który na nic nie wskazuje. Wskazuje na śmieci. Dereferencja takiego wskaźnika jest błędem. Potrzebujesz znanej lokalizacji. Można się tam dostać na cztery sposoby.

  1. malloc : Ta funkcja przydziela blok na stercie. p zawiera teraz prawidłowy adres. Wskaźnik jest inicjowany, ponieważ wskazuje na konkretny, zarezerwowany fragment pamięci.
  2. Przypisanie : p = q; jeśli q jest ważne, p staje się ważne. Dziedziczy adres q. Jeśli q zawiera śmieci, p również je otrzymuje.
  3. Adres jawny : Możesz wskazać p bezpośrednio na znaną zmienną. p = &i; kieruje p do liczby całkowitej i. Tylko. Bezpośrednio.
  4. Zero/NULL : p = 0; lub p = NULL;. Spowoduje to ustawienie adresu na zero. Nie wskazuje na blok. Wskazuje pozycję zerową.

Po co używać zera? To jest flaga.

Możesz sprawdzić „if (p == 0)”, aby sprawdzić, czy wskaźnik jest prawidłowy. System to rozpoznaje. Jeśli spróbujesz wyłuskać wskaźnik zerowy, program ulegnie awarii. Twardy.

p = 0; *p = 5;

Druga linia zawodzi. „p” nic nie wskazuje. Nie można nigdzie zapisywać danych. To zachowanie ma kluczowe znaczenie w przypadku późniejszych list połączonych. To siatka bezpieczeństwa.

Zwalnianie sterty

Alokacja pamięci nie jest wystarczająca. Musisz ją zwrócić.

„malloc” zajmuje miejsce. „bezpłatny” zwraca to.

free(p) robi dwie rzeczy. Po pierwsze, anuluje rezerwację bloku. Sterta zwraca tę pamięć. Można go ponownie wykorzystać. Po drugie, „p” pozostaje niezainicjowane. To nie staje się magicznie bezpieczne. Musisz ponownie zainicjować p przed ponownym użyciem.

Blok znika z Twojego pola widzenia. Wskaźnik zwisa.

Oto standardowy wzorzec pracy z pojedynczą liczbą całkowitą na stercie:

Jest to w zasadzie demonstracja. Ale pokazuje cykl życia.

sizeof(int) zwraca rozmiar w bajtach. Na większości maszyn jest to 4. Można użyć zakodowanej na stałe wartości malloc(4). Nie rób tego. sizeof sprawia, że ​​kod jest przenośny. Czyta się lepiej. Dostosowuje się do zmian architektury.

malloc zwraca uniwersalny wskaźnik. Kompilatory często wyświetlają ostrzeżenie, jeśli nie rzutujesz typu. (int *) konwertuje ten ogólny blok na wskaźnik na liczbę całkowitą. Pasuje to do typu p.

free(p) zwraca blok na stertę.

Struktury i pierwszeństwo operatorów

Struktury zachowują się w ten sam sposób. Przydzielasz blok dla całej struktury, a nie tylko dla poszczególnych pól.

Spójrz na (*p).i = 10;.

Dlaczego nawiasy są potrzebne?

Ponieważ . ma wyższy priorytet niż *.

Bez nawiasów „p.i” oznacza „ (p.i)”. Próbuje uzyskać dostęp do „i” poprzez wskaźnik „p”, a następnie wyłuskać wynik. To nie jest to, czego potrzebujesz. Chcesz najpierw wyłuskać odwołanie do p, a następnie uzyskać dostęp do i.

Zasady pierwszeństwa są rygorystyczne. W tym kontekście * jest powiązane bliżej niż .. Nawiasy wymuszają wykonanie dereferencji przed uzyskaniem dostępu do elementu struktury.

Większość ludzi nie znosi wielokrotnego wpisywania (*p).i. To kłopotliwe.

Jest skrót w C.

p->i

To jest dokładnie to samo, co (*p).i. Ale pisanie jest szybsze. W całym kodzie C zobaczysz ->. Jest to standardowy sposób uzyskiwania dostępu do elementów struktury za pomocą wskaźnika.

Sterta jest zasobem ograniczonym. Przydzielaj pamięć ostrożnie. Zwolnij go sekwencyjnie. Albo patrz, jak zanika ci pamięć.