Menu

Wskaźnik na strukturę w C: operator strzałki i struktury na stercie

Jak wskazywać na strukturę w C: operator strzałki, dlaczego (*p).x potrzebuje nawiasów, przekazywanie struktur przez wskaźnik, by je zmieniać lub uniknąć kopii, alokacja struktury przez malloc i budowa węzła listy wiązanej.

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

Struktura przekazana do funkcji jest kopiowana. Dla punktu z dwoma liczbami int to nie problem, ale dla 200-bajtowego rekordu to marnotrawstwo, a do tego uniemożliwia zmiany, bo funkcja widzi tylko własną kopię. Wskaźnik na strukturę rozwiązuje oba problemy naraz i jest też jedynym sposobem na budowę rosnących struktur danych: list, drzew, grafów.

Pobranie wskaźnika na strukturę

Nie trzeba niczego specjalnego. & daje adres, a typ to „wskaźnik na strukturę”:

Wszystkie trzy wypisują 3. Dwa ostatnie to ta sama operacja zapisana na dwa sposoby.

Dlaczego (*ptr).x potrzebuje nawiasów

Może cię kusić, by je pominąć i napisać *ptr.x. To skompiluje się do czegoś zupełnie innego albo, częściej, w ogóle się nie skompiluje, bo operator pola . ma wyższy priorytet niż dereferencja *:

*ptr.x        // parsowane jako *(ptr.x): zle
(*ptr).x      // najpierw dereferencja, potem pole: poprawnie
ptr->x        // to samo, krocej

ptr.x prosi o pole wskaźnika, a wskaźnik nie ma pól. Komunikat kompilatora („request for member 'x' in something not a structure or union”) to przebrany błąd priorytetu.

Ponieważ forma z nawiasami jest brzydka i łatwo o pomyłkę, C daje ->. W praktyce prawie nigdy nie zobaczysz (*p).x w prawdziwym kodzie: sięgaj po p->x i zapomnij o długiej formie, poza tym, że wyjaśnia, co znaczy strzałka.

Przekazywanie struktury przez wskaźnik

Parametr wskaźnikowy daje funkcji prawdziwą strukturę wywołującego, więc zmiany zostają:

Dwa nawyki, które warto tu wyrobić:

  • Oznaczaj wskaźniki tylko do odczytu jako const. const struct Point *p obiecuje, że funkcja nie zmieni tego, na co wskazuje p. Jeśli ktoś później doda w środku p->x = 0, kompilacja się nie uda, zamiast błąd trafić na produkcję. To także dokumentuje intencję w miejscu wywołania.
  • Przekazuj wskaźnik przy dużych strukturach, nawet gdy tylko czytasz. Struktura z 64-bajtowym buforem na nazwę kosztuje 64 bajty kopiowania przy każdym wywołaniu przez wartość, a wskaźnik 8.

Małe struktury nadal można spokojnie przekazywać przez wartość: 8-bajtowe struct Point kopiuje się równie tanio jak wskaźnik, a wersja przez wartość nie może być NULL, co usuwa cały rodzaj awarii.

Wskaźniki do tablic struktur

Arytmetyka wskaźników działa na tablicach struktur dokładnie tak jak na tablicach int: p + 1 przesuwa się o całą strukturę, łącznie z wyrównaniem.

Nazwa tablicy zamienia się we wskaźnik na jej pierwszy element, więc w miejscu wywołania staff już jest struct Employee *. Dlatego funkcja potrzebuje też osobno przekazanego n: sizeof wewnątrz funkcji zmierzyłby wskaźnik, a nie tablicę.

Alokacja struktury na stercie

Struktury na stosie umierają na końcu swojego zasięgu. Aby utworzyć taką, która przeżyje funkcję, w której powstała, albo utworzyć ich tyle, ile wymaga wejście, zaalokuj ją przez malloc:

W tej małej funkcji każdy z czterech szczegółów jest nośny:

  • sizeof *e, a nie sizeof(struct Employee). Czyta się to jako „rozmiar tego, na co wskazuje e”, więc jeśli typ kiedyś się zmieni, alokacja automatycznie za nim pójdzie. Te dwie rzeczy nie mogą się rozjechać.
  • Sprawdzaj NULL. malloc zwraca NULL, gdy nie może spełnić żądania. Zapis e->id przez wskaźnik null to segmentation fault.
  • Inicjalizuj każde pole. malloc nie zeruje pamięci, struktura przychodzi pełna śmieci. (calloc(1, sizeof *e) wyzeruje ją za ciebie.)
  • Ktoś musi wywołać free. Zwrócenie zaalokowanego wskaźnika przenosi ten obowiązek na wywołującego. Napisz to w komentarzu: niejasna zasada własności to początek wycieków pamięci.

Nagroda: węzeł listy wiązanej

Oto coś, czego struktury nie potrafią bez wskaźników. Struktura nie może zawierać samej siebie, bo byłaby nieskończenie duża, ale może zawierać wskaźnik na strukturę tego samego rodzaju, a ta jedna sztuczka buduje każdą wiązaną strukturę danych w C.

Zauważ, że struct Node *next; odwołuje się do struct Node, zanim deklaracja struct Node się skończy. Jest to dozwolone właśnie dlatego, że wskaźnik ma znany rozmiar niezależnie od tego, na co wskazuje, więc kompilator nie potrzebuje jeszcze pełnej definicji. To także jedyne miejsce, gdzie musisz użyć etykiety struct Node, nawet jeśli istnieje też typedef: nazwa z typedef nie istnieje jeszcze wewnątrz własnej definicji.

Pętla free_list zapamiętuje head->next przed wywołaniem free(head). Odczyt head->next po zwolnieniu to użycie po zwolnieniu (use-after-free): pamięć już nie należy do ciebie, a odczytana wartość to cokolwiek zostawił tam alokator.

Częste błędy

  • p->x na niezainicjalizowanym wskaźniku lub NULL. Najczęstsza przyczyna awarii w kodzie pełnym struktur. Inicjalizuj wskaźniki wartością NULL i sprawdzaj je przed dereferencją.
  • Zwracanie wskaźnika na lokalną strukturę. struct Point *bad(void) { struct Point p = {1,2}; return &p; } zwraca adres pamięci, która przestaje istnieć w chwili powrotu z funkcji. Zwróć strukturę przez wartość albo ją zaalokuj.
  • Podwójne zwolnienie albo brak zwolnienia. Każde malloc łączy się z dokładnie jednym free. Po zwolnieniu ustaw wskaźnik na NULL, żeby późniejsze przypadkowe użycie wysypało program głośno, zamiast po cichu psuć pamięć.
  • Kopiowanie struktury, która zawiera wskaźnik. Przypisanie struktury kopiuje wartość wskaźnika, a nie to, na co on wskazuje. Obie struktury odwołują się wtedy do tego samego bufora, a ta, która zwolni go pierwsza, zostawia drugą z wiszącym wskaźnikiem.

Najczęściej zadawane pytania

Co robi operator -> w C?

p->x odwołuje się do pola x przez wskaźnik p. Jest to dokładny odpowiednik (*p).x: najpierw dereferencja wskaźnika, potem pobranie pola. Strzałka istnieje, bo wskaźniki na struktury są tak powszechne, że dłuższa forma zaśmiecałaby kod.

Dlaczego *p.x nie działa dla wskaźnika na strukturę?

Bo . wiąże mocniej niż *. *p.x jest parsowane jako *(p.x): weź pole x z p (który jest wskaźnikiem, a nie strukturą) i zrób na nim dereferencję. Potrzebujesz (*p).x albo po prostu p->x.

Przekazywać strukturę przez wartość czy przez wskaźnik w C?

Przez wskaźnik, gdy funkcja musi zmienić strukturę wywołującego albo gdy struktura jest duża i kopiowanie byłoby marnotrawstwem. Przez wartość w przypadku małych struktur tylko do odczytu, gdzie kopia jest tania, a kod prostszy. Parametry wskaźnikowe tylko do odczytu oznaczaj jako const struct T *, aby kompilator pilnował tej obietnicy.

Jak zaalokować strukturę przez malloc w C?

struct Node *n = malloc(sizeof *n);. Forma sizeof *n pyta o rozmiar tego, na co wskazuje n, więc pozostaje poprawna, nawet jeśli później zmienisz typ. Sprawdź wynik względem NULL, zainicjalizuj każde pole, a na koniec wywołaj free(n).

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ