Menu

Wskaźniki w C: kompletny przewodnik z przykładami

Wskaźnik to zmienna, która przechowuje adres w pamięci. Ta strona buduje tę ideę od podstaw: operatory & i *, deklarowanie i wyłuskiwanie wskaźników, dlaczego typy wskaźników mają znaczenie i funkcja swap(), która pokazuje, po co w ogóle istnieją wskaźniki.

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

Każda zmienna w twoim programie mieszka gdzieś w pamięci, a każde "gdzieś" ma numer, czyli adres. Wskaźnik to po prostu zmienna, która przechowuje jeden z tych numerów. To cała idea. Wszystko inne na tej stronie to składnia i konsekwencje.

Wskaźniki mają w C tak duże znaczenie, bo C daje każdej funkcji kopię jej argumentów. Funkcja, która dostanie liczbę 7, może zmieniać swoją kopię do woli, a zmienna wywołującego ani drgnie. Daj funkcji adres, a będzie mogła sięgnąć wstecz i zmienić oryginał. Tablice, łańcuchy, pamięć dynamiczna i każda struktura danych, jaką kiedykolwiek zbudujesz w C, opierają się na tej jednej możliwości.

Pamięć, adresy i dlaczego wszystko ma adres

Gdy piszesz int score = 42;, kompilator rezerwuje kilka bajtów pamięci i nadaje im nazwę, której możesz używać. Te bajty mają też adres liczbowy. Wyobraź sobie pamięć jako bardzo długą ulicę z ponumerowanymi domami: score to rodzina mieszkająca pod numerem 0x7ffd4c2a. Nazwa jest dla ciebie, numer dla maszyny.

Operator & pyta o ten numer:

Uruchom to. Trzy adresy się różnią, bo trzy zmienne zajmują różne bajty, i będą się zmieniać przy każdym uruchomieniu. To normalne i dlatego nigdy nie wpisuje się adresu na sztywno. Zwróć uwagę na rzutowanie (void *): %p z definicji przyjmuje void *, a przekazanie wskaźnika innego typu to formalnie niezdefiniowane zachowanie. Niech to rzutowanie wejdzie ci w nawyk.

To, że adresy są nieprzewidywalne, nie jest problemem, bo prawie nigdy nie obchodzi cię, jaki adres jest. Obchodzi cię to, że go masz.

Deklarowanie wskaźnika

Deklaracja wskaźnika podaje typ, na który wskazuje:

int    *p;      // p może przechować adres inta
double *q;      // q może przechować adres double
char   *name;   // name może przechować adres chara

Czytaj int *p; jako "*p to int": wyłuskanie p daje int. To odczytanie przetrwa każdą skomplikowaną deklarację, jaką C później na ciebie rzuci.

Miejsce * to kwestia stylu, którą kompilator ignoruje: int* p, int *p i int * p deklarują to samo. Większość kodu w C pisze int *p i jest ku temu praktyczny powód:

int* a, b;   // a to wskaźnik na int; b to ZWYKŁY int, nie wskaźnik
int *a, *b;  // oba są wskaźnikami: * wiąże się z nazwą, a nie z typem

* przyczepia się do deklaratora, a nie do typu. Pisanie jej przy nazwie sprawia, że to widać.

& i *: dwa operatory

Całą pracę wykonują dwa operatory i są swoimi dokładnymi przeciwieństwami.

  • &x: pobranie adresu. Daje adres, pod którym leży x.
  • *p: wyłuskanie. Idzie pod adres zapisany w p i daje obiekt, który tam mieszka.

Ostatnie dwie linie to cały sens wskaźników. Nic nie przypisało niczego bezpośrednio do age, a jednak age się zmieniło, bo *p = 31 oznacza "zapisz 31 pod adresem, który przechowuje p", a tym adresem jest adres age.

Tak to wygląda w pamięci:

      p                        age
 +------------+           +----------+
 | 0x7ffd1c40 | --------> |    31    |
 +------------+           +----------+
 pod 0x7ffd1c38           pod 0x7ffd1c40

 p    -> adres 0x7ffd1c40
 *p   -> int zapisany pod nim, 31
 &age -> 0x7ffd1c40, ten sam adres, który ma p
 &p   -> 0x7ffd1c38, gdzie mieszka samo p

Zwróć uwagę na ostatnią linię: wskaźnik sam jest zmienną gdzieś w pamięci, więc też ma własny adres. To nie podchwytliwe pytanie, tylko konsekwencja, i tak właśnie działają wskaźniki na wskaźniki (int **).

Ponieważ & i * się znoszą, *&age to po prostu age, a &*p to po prostu p.

Dlaczego typy wskaźników mają znaczenie

Wskaźnik przechowuje adres, a adres to tylko liczba, więc dlaczego kompilator upiera się, żeby wiedzieć, czy wskazuje na int, czy na double? Z dwóch powodów:

  1. Ile bajtów odczytać. Wyłuskanie int * odczytuje (zwykle) 4 bajty, a wyłuskanie double * odczytuje 8. Bez typu *p nie miałoby sensu.
  2. Jak zinterpretować te bajty. Ten sam wzór bitów to jedna liczba jako int i zupełnie inna jako float.

Rozmiar samego wskaźnika natomiast nie zależy od tego, na co wskazuje: adres to adres:

Na maszynie 64-bitowej wszystkie trzy wskaźniki mają 8 bajtów, a rzeczy, na które wskazują, mają 4, 8 i 1. To typ wskazywany nadaje sens *p i arytmetyce wskaźników.

Przypisywanie między typami wskaźników to błąd, na który kompilator się poskarży, i słusznie: pi = &d; przygotowałoby grunt pod odczytanie 4 bajtów liczby double i nazwanie wyniku liczbą całkowitą.

Klasyczna motywacja: swap()

Oto funkcja, po którą sięga każdy kurs C, bo zawodzi dokładnie w taki sposób, który wyjaśnia wskaźniki.

Funkcja twierdzi, że je zamieniła, a main twierdzi, że nic się nie stało. broken_swap dostała kopie x i y. Idealnie zamieniła swoje kopie, a potem te kopie przestały istnieć, gdy funkcja się zakończyła.

Daj jej zamiast tego adresy:

Teraz a i b nadal są kopiami, ale kopiami dwóch adresów, a kopia adresu wskazuje dokładnie tę samą zmienną co oryginał. *a to x, skądkolwiek funkcja zostanie wywołana.

Na tym wzorcu opiera się każda funkcja C, która zmienia dane wywołującego, i parametry funkcji w ogóle: C nie ma przekazywania przez referencję, więc przekazujesz wskaźnik i go wyłuskujesz. Dlatego też scanf potrzebuje &:

int n;
scanf("%d", &n);   // scanf musi móc zapisać do n, więc potrzebuje adresu n

Zwracanie więcej niż jednej wartości

Funkcja w C zwraca jedną wartość. Wskaźniki pozwalają to obejść: przekaż adresy na dodatkowe wyniki.

Wartość zwracana niesie status, a wskaźniki niosą wyniki. Ten podział jest wszędzie: w bibliotece standardowej C i w API systemów operacyjnych.

Wskaźniki na wskaźniki

Skoro wskaźnik jest zmienną, możesz pobrać jego adres, a typem tego adresu jest "wskaźnik na wskaźnik na int":

  pp              p               value
+------+       +------+        +------+
| &p   | ----> | &val | -----> |   8  |
+------+       +------+        +------+

** spotkasz naprawdę wtedy, gdy funkcja musi zmienić wskaźnik trzymany przez wywołującego, na przykład alokator, który ustawia wskaźnik wywołującego na świeżą pamięć, oraz w char *argv[] przy argumentach wiersza poleceń.

Cztery błędy, które warto znać, zanim je popełnisz

Użycie wskaźnika, który nigdy na nic nie wskazywał. Niezainicjalizowany wskaźnik przechowuje bajty, które już tam były. Wyłuskanie go odczytuje albo zapisuje losowy adres.

int *p;      // p zawiera śmieci
*p = 10;     // niezdefiniowane zachowanie, prawdopodobnie awaria

Inicjalizuj każdy wskaźnik prawdziwym adresem albo wartością NULL. Związaną z tym dyscyplinę opisuje strona o wskaźnikach NULL.

Mylenie *p z p. p = 5; ustawia wskaźnik na adres 5 (bez sensu); *p = 5; zapisuje 5 tam, gdzie wskazuje. Przy pierwszym kompilator ostrzeże, a ostrzeżenia warto czytać.

Zapomniane & w scanf. scanf("%d", n) przekazuje wartość n jako adres. Kompiluje się z ostrzeżeniem, a potem zapisuje do pamięci, którą wskazuje ta liczba.

Zwracanie adresu zmiennej lokalnej. Zmienna lokalna znika w chwili, gdy funkcja się kończy; adres to pokwitowanie za zburzony dom.

int *bad(void) {
    int local = 42;
    return &local;   // wywołujący dostaje wiszący wskaźnik
}

Jeśli potrzebujesz pamięci, która przeżyje wywołanie, przydziel ją: do tego służy pamięć dynamiczna.

Co dalej

Wskaźniki nie kończą się na "przechowaj jeden adres". Dodawanie do wskaźnika przesuwa go po pamięci o jednostki jego typu, i to jest temat arytmetyki wskaźników; a nazwa tablicy w C degraduje się do wskaźnika na pierwszy element, dlatego wskaźniki i tablice to w praktyce jeden temat, gdy znasz oba. Przeczytaj te dwie strony w następnej kolejności, a reszta C przestanie wyglądać jak magia.

Najczęściej zadawane pytania

Czym jest wskaźnik w C?

Wskaźnik to zmienna, której wartością jest adres w pamięci, czyli położenie jakiegoś innego obiektu. int *p = &age; sprawia, że p przechowuje adres age, a *p odczytuje albo zapisuje zmienną age przez ten adres.

Czym różni się * od & w C?

&x to operator pobrania adresu: daje adres, pod którym leży x. *p to operator wyłuskania: idzie pod adres zapisany w p i daje obiekt, który tam jest. Znoszą się nawzajem, więc *&x to po prostu x.

Dlaczego * pojawia się dwa razy: w deklaracji i przy użyciu wskaźnika?

To dwa różne zastosowania tego samego symbolu. W int *p; znak * jest częścią typu i mówi, że "p to wskaźnik na int". W *p = 5; znak * to operator wyłuskania i mówi "zapisz 5 pod adresem, który przechowuje p". Jeśli czytasz deklarację jako "*p to int", oba użycia mają sens.

Po co mi wskaźniki w C?

C przekazuje każdy argument przez wartość, więc funkcja nigdy nie zmieni zmiennej wywołującego, jeśli nie dostanie jej adresu. Wskaźniki pozwalają też wydajnie przechodzić tablice, zwracać więcej niż jeden wynik, budować struktury powiązane i używać pamięci przydzielanej w czasie działania przez malloc.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ