Podrozdziały


4.6 Wskaźniki

Zmienne wskaźnikowe (ang. pointer) są fundamentalnym pojęciem C/C++. W programach napisanych w tych językach są one wszechobecne i bez ich zrozumienia nie da się zrozumieć nawet najprostszego kodu napisanego w C/C++.

Wskaźniki umożliwiają bowiem, między innymi:

Szczególnym rodzajem wskaźników są wskaźniki funkcyjne, którymi zajmiemy się w rozdziale o funkcjach .


4.6.1 Wskaźniki do zmiennych

Jak pamiętamy, typ zmiennej określa rodzaj informacji zapisanej w tej zmiennej. Dla wskaźników informacją tą jest adres w pamięci komputera, pod którym zapisana jest inna zmienna. Ta inna zmienna może przy tym być dowolnego typu — również wskaźnikowego. Zazwyczaj, choć, jak się przekonamy, są od tego wyjątki, określając typ zmiennej wskaźnikowej musimy określić też typ tych zmiennych, których adresy dana zmienna wskaźnikowa może przechowywać. To, że wskaźnik musi „wiedzieć”, na zmienne jakiego typu może wskazywać, związane jest między innymi z tym, że musi być znana długość (w bajtach) tego typu zmiennych: dzięki temu możliwe są pewne operacje na wskaźnikach, które wkrótce poznamy.

Wartością zmiennej wskaźnikowej jest adres innej zmiennej.

Załóżmy, że chcemy wprowadzić zmienną wskaźnikową przystosowaną do przechowywania adresów zmiennych typu double. Po utworzeniu chcemy też przypisać tym zmiennym wartości, a więc wpisać do nich adresy istniejących zmiennych typu double.

Zobaczmy, jak to wszystko można zrobić na przykładzie poniższego programu:


P15: pointers.cpp     Wskaźniki

      1.  #include <iostream>
      2.  using namespace std;
      3.  
      4.  int main() {
      5.      double x, y = 1.5, u;               
      6.  
      7.      double *px;                         
      8.      px = &x;
      9.  
     10.      double* py = &y;                    
     11.  
     12.      double *pz, *pu = &u, v;            
     13.  
     14.      cout << "1. *py = "  << *py
     15.           << " y = " << y << endl;       
     16.  
     17.      x = 0.5;                            
     18.      cout << "2. *px = "  << *px
     19.           << " x = " << x << endl;
     20.  
     21.      *px = 5*x;                          
     22.      cout << "3. *px = "  << *px
     23.           << " x = " << x << endl;
     24.  
     25.      pz = px;                            
     26.      cout << "4. *pz = "  << *pz << endl;
     27.  
     28.      *pu = v = *pz = 10;                 
     29.      cout << "5. *pu = "  << *pu
     30.           << " u = " << u << " v = "
     31.           << v << " x = " << x << endl;
     32.  
     33.      cout << "6.  py = " <<  py << endl; 
     34.      cout << "7. &py = " << &py << endl;
     35.  }

którego uruchomienie daje następujący rezultat:

    1. *py = 1.5 y = 1.5
    2. *px = 0.5 x = 0.5
    3. *px = 2.5 x = 2.5
    4. *pz = 2.5
    5. *pu = 10 u = 10 v = 10 x = 10
    6.  py = 0xbffffa30
    7. &py = 0xbffffa18

W wierszu programu deklarujemy i definiujemy (przydzielamy pamięć na) trzy zmienne typu double. Jedna z nich, y, jest zainicjowana wartością 1.5; pozostałe nie mają określonej wartości, ale, co ważne, istnieją, a zatem mają konkretny i ustalony adres w pamięci komputera.

Linia to deklaracja/definicja zmiennej wskaźnikowej (krócej: wskaźnika) do zmiennej typu double. Po wykonaniu tej instrukcji zmienna px istnieje i jest przystosowana do przechowywania adresów zmiennych typu double. Mówimy, że px jest wskaźnikiem do zmiennej typu double. Natomiast zmienną, której adres jest wartością zmiennej wskaźnikowej, nazywamy zmienną wskazywaną przez ten wskaźnik.

W naszym przykładzie, na razie, wartość utworzonego wskaźnika px jest nieokreślona, to znaczy nie został tam wpisany adres żadnej istniejącej zmiennej typu double. A zatem wskaźnik już istnieje, ale na razie nie wskazuje na żadną zmienną.

Jak widzimy, deklaracja wskaźnika ma postać

       Typ *nazwa;
lub
       Typ* nazwa;
lub nawet
       Typ * nazwa;
czyli nie ma znaczenia, czy „przylepimy” gwiazdkę do nazwy typu czy do nazwy zmiennej — możemy również tę gwiazdkę otoczyć białymi znakami, na przykład znakami odstępu.

Nazwą typu wskaźnika px zadeklarowanego jako ' Typ* px;' jest Typ*.

Zapis taki oznacza właśnie, że zmienna o nazwie px będzie zmienną wskaźnikową przystosowaną do przechowywania adresów innych zmiennych, ale koniecznie typu Typ. Jak teraz przypisać tej zmiennej jakąś wartość? Widzimy to w następnej linii. Do px wpisujemy tu adres zmiennej x, która jest typu double, gdyż w naszym przypadku zmienna px była zadeklarowana jako zmienna typu double*. Zmienna x już istnieje, bo została utworzona w linii ; niewątpliwie więc ma adres, mimo że jej wartość jest wciąż nieokreślona.

A zatem, jak widzimy, znak ' &' pełni rolę operatora wyłuskania adresu i działa na wyrażenie po swojej prawej stronie. Aby takie wyłuskanie było możliwe, wyrażenie po prawej stronie operatora musi być l-wartością (patrz rosdział o l-wartościach ).

Jeśli var jest identyfikatorem zmiennej, to wartością wyrażenia &var jest adres tej zmiennej.

Możemy też użyć konstrukcji występującej w linii : za jednym zamachem wskaźnik py deklarujemy i inicjujemy adresem istniejącej y, a więc istniejącej zmiennej typu double. Natomiast wartością zmiennej wskazywanej przez py, czyli zmiennej y, jest 1.5, wpisane do niej w linii .

Zmienną, której adres jest wartością zmiennej wskaźnikowej, nazywamy zmienną wskazywaną przez ten wskaźnik.

Sytuację można więc zilustrować jak na rysunku. Zmienna py istnieje w pamięci pod pewnym adresem — w przykładzie na rysunku adres ten to 0xbffffa18 (adresy zapisuje się zwyczajowo w układzie szesnastkowym). Zmienna ta zajmuje 4 bajty (choć może to zależeć od platformy).

Image memcell

Wartością zapisaną pod tym adresem, a więc wartością zmiennej py jest adres 0xbffffa30 zmiennej y typu double. Pod tym z kolei adresem zapisana jest liczba typu double (zwykle więc w ośmiu bajtach): jest to wartość zmiennej y wskazywanej przez wskaźnik py. W linii  programu pointers.cpp,

       double *pz, *pu = &u, v;
widzimy następną deklarację/definicję trzech zmiennych: pz, pu, i  v. Zilustrowany jest tu ważny fakt:

Deklarując w jednej instrukcji dwa lub więcej wskaźników, należy identyfikator każdego z tych wskaźników poprzedzić gwiazdką.

Na przykład, zadeklarowana w linii  zmienna v jest typu double, a nie wskaźnikowego.

Jak poprzez wskaźnik do zmiennej można „dostać” się do samej zmiennej? Robi się to poprzez operator dereferencji, zwany też operatorem wyłuskania wartości, oznaczany przez gwiazdkę.

Jeśli pvar jest identyfikatorem wskaźnika, to wyrażenie *pvar jest nazwą (aliasem nazwy) zmiennej wskazywanej przez pvar.

A zatem, py wskazuje na y (patrz linia ), a wartością zmiennej y jest 1.5 (linia ). Zatem, ponieważ *py jest nazwą zmiennej wskazywanej przez py, więc jest w tej chwili inną nazwą zmiennej y. Drukując wartość *py drukujemy zatem 1.5: aktualną wartość y (patrz linia 1 wydruku z programu).

Podobnie, w linii  nadajemy wartość 0.5 zmiennej x. Drukując teraz wartość *px drukujemy wartość zmiennej wskazywanej przez px, czyli w tej chwili zmiennej x, czyli właśnie 0.5 (linia 2 wydruku).

W linii  widzimy, że *px, będąc nazwą zmiennej typu double wskazywanej przez px, może być użyte po lewej stronie przypisania. Ponieważ w tej chwili px wskazuje na x, jest to równoważne przypisaniu ' x = 5*x'. Drukując zatem wartości x*px otrzymujemy to samo, a mianowicie, zgodnie z przewidywaniem, 2.5.

W linii  utworzyliśmy wskaźnik pz. W linii  wpisujemy do tej zmiennej wartość px; jest nią ten sam adres który jest zapisany w  px. Jest to adres zmiennej x. Zatem w tej chwili x, *px*pz oznaczają to samo: drukując wartość *pz otrzymujemy również 2.5.

W linii  przypisujemy wartość 10 do *pz. Ponieważ jednak pz wskazuje (po przypisaniu z linii ) na zmienną x, więc przypisując do *pz przypisujemy tak naprawdę do x. Drukując teraz wartość x otrzymujemy, zgodnie z oczekiwaniem, 10, jak widać to w linii 5 wydruku.

Co będzie, jeśli będziemy chcieli wydrukować py? Zmienna ta jest zmienną wskaźnikową, więc jej wartością jest adres, w naszym przykładzie jest to adres zmiennej y. Zatem wstawienie py do strumienia wyjściowego (linia ) spowoduje wypisanie adresu zmiennej y; adres ten to, jak widzimy na wydruku, 0xbffffa30.

A jaki adres ma sama zmienna py? Żeby to sprawdzić, możemy wydrukować (patrz ostatnia linia) wartość wyrażenia &py: wartością tą jest właśnie adres zmiennej py (a nie adres będący wartością py). Jak widzimy z wydruku, adresem zmiennej py było 0xbffffa18. Wyjaśnia to do końca dane z rysunku.

Zauważmy, że operator ' *' występuje w podwójnej roli (nie licząc jego znaczenia jako operatora mnożenia). Nie powinno to jednak stanowić problemu: jeśli gwiazdka występuje w instrukcji deklaracyjnej i na lewo od niej jest nazwa typu, to znaczy, że chodzi o deklarację/definicję zmiennej wskaźnikowej. W innych przypadkach, jeśli gwiazdka występuje jako operator jednoargumentowy, chodzi o dereferencję. Na przykład w linii

       int k = 7, *pk = &k, m = *pk;
gwiazdka przy pierwszym wystąpieniu pk oznacza deklarację. Co prawda na lewo od niej jest przecinek, a nie nazwa typu, ale pamiętamy, że zapis powyższy jest skrótowym zapisem ciągu deklaracji/definicji
       int k   = 7;
       int *pk = &k;
       int m   = *pk;
więc wsystko jest tak jak trzeba. Przy drugim wystąpieniu pk, na lewo od gwiazdki jest znak ' =', a zatem tutaj chodzi o jednoargumentowy operator — musi to zatem oznaczać dereferencję.


Tak jak w Javie, istnieje wyróżnione odniesienie puste null odpowiadające wartości odnośnika nie zawierającego odniesienia do żadnego istniejącego obiektu, tak w C/C++ istnieje wskaźnik „pusty”.

W C/C++ rolę wskaźnika pustego, a więc nie zawierającego adresu żadnej zmiennej, może spełniać wskaźnik o wartości 0 (zero).

Wartość pustego wskaźnika oznaczana była przez NULL — była to zdefiniowana nazwa (makro) preprocesora rozwijana, w zależności od platformy, do '0', '0L' lub ' (void*)0'. W nowym standardzie powinniśmy zamiast tego używać słowa kluczowego nullptr.

Wartość 0 można przypisać każdemu wskaźnikowi, niezależnie od jego typu (czyli od typu zmiennej, jaką wskazuje). Zwróćmy tu uwagę na ważny fakt: przypisanie ' pk = 0;', gdzie pk jest wskaźnikiem, nie oznacza wcale, że istnieje jakaś ogólna konwersja od typu całkowitego do typu wskaźnikowego (wartość literału '0' jest typu int). Choć adresy, które są wartościami zmiennych wskaźnikowych, są oczywiście całkowite, to typ wskaźnikowy nie jest typem całościowym. Wartość zerowa jest wyjątkowa i jest jedyną wartością całkowitą, którą można nadać wskaźnikowi poprzez przypisanie: kompilator skonwertuje ją do odpowiedniego typu i otrzymana wartość wskaźnika odpowiadać będzie wskazaniu pustemu. Przypisanie na przykład ' pk = 7;' spowodowałoby błąd kompilacji.

Każdy wskaźnik przed użyciem należy na coś sensownego „nakierować”. Częsty błąd to konstrukcja

       int *q;
       ...
       ...
       *q = 7;
Zauważmy, że wskaźnik q istnieje, ale nie zawiera na razie adresu żadnej istniejącej zmiennej typu int. Zawartość zmiennej q jest w tej chwili całkowicie przypadkowa i nieprzewidywalna. W ostatniej linii tego fragmentu usiłujemy wpisać wartość 7 do zmiennej typu int wskazywanej przez q. Ale q niczego nie wskazuje! Wpiszemy więc 7 w całkowicie przypadkowym miejscu pamięci zamazując jego dotychczasową zawartość (jeśli nam system na to pozwoli —  jeśli akurat będzie to adres poza obszarem pamięci przydzielonym przez system operacyjny naszemu procesowi, to program załamie się podczas wykonania; na przykład pod Uniksem/Linuksem dostaniemy komunikat 'segmentation fault, core dumped').

Spójrzmy jeszcze na poniższy prosty program


P16: pminmax.cpp     Wskaźniki

      1.  #include <iostream>
      2.  using namespace std;
      3.  
      4.  int main() {
      5.      double x, y, *pmin, *pmax;      
      6.      cout << "Podaj dwie liczby: ";
      7.      cin  >> x >> y;
      8.      if (x < y) {
      9.          pmin = &x; pmax = &y;
     10.      } else {
     11.          pmin = &y; pmax = &x;
     12.      }
     13.      cout << "Min = " << *pmin << endl;
     14.      cout << "Max = " << *pmax << endl;
     15.  }

którego uruchomienie daje

    Podaj dwie liczby: 9.76 7.34
    Min = 7.34
    Max = 9.76
W linii  deklarujemy dwie zmienne typu double i dwa wskaźniki typu double*. Następnie wczytujemy dwie liczby do zmiennych xy, po czym ustawiamy wskaźniki pminpmax w ten sposób, aby pmin wskazywało ma mniejszą z liczb xy, a  pmax na większą z nich. Używając następnie operatora dereferencji, w ostatnich dwóch liniach drukujemy najpierw mniejszą, potem większą z liczb.


4.6.2 Wskaźniki generyczne

Czasem potrzebne są wskaźniki wskazujące po prostu pewien obszar pamięci, bez specyfikowania typu zmiennej tam się znajdującej. Służą do tego wskaźniki generyczne, które deklarujemy jako typu void*; w poniższym fragmencie pk, pq są wskaźnikami typu int*, natomiast pv jest wskaźnikiem generycznym.

      1.      int k = 8, *pk = &k, *p, *q;
      2.      void *pv = pk;
      3.      p = static_cast<int*>(pv);
      4.      q = (int*)pv;
Widzimy w linii drugiej, że wartość typu int* można przypisać do zmiennej typu void*. Po takim przypisaniu pv zawiera ten sam adres co pk, czyli adres zmiennej k. Jest jednak między nimi ważna różnica: zmienna pv nie zawiera żadnej informacji o typie — jest to „surowy” adres w pamięci. Zatem nie miałoby sensu wyłuskanie wartości *pv — adres jest znany, ale nie wiadomo byłoby ile bajtów należy uwzględnić i jak je zinterpretować.

Do wskaźników generycznych nie można stosować operatora wyłuskania wartości (dereferencji).

Mając dany w zmiennej typu void* surowy adres można, jeśli się wie, co się robi, wymusić zinterpretowanie go jako adresu zmiennej określonego typu. Widzimy to w liniach 3 i 4. Dokonujemy tu jawnej konwersji od typu void* do typu int*. Takie rzutowanie typu oznacza się (podobnie jak w Javie) nazwą typu w nawiasach (patrz linia 4). Jest to jedyny sposób dozwolony w czystym C. W języku C++ istnieje też inna forma, przedstawiona w linii trzeciej (patrz rozdział o konwersjach jawnych ).

Z tego, co powiedzieliśmy, nie wynika jeszcze jakaś szczególnie fundamentalna rola wskaźników w C/C++. W dalszych rozdziałach jednak, przy okazji omawiania tablic, funkcji, dynamicznego zarządzania pamięcią, polimorfizmu itd., zobaczymy, że właśnie wskaźniki są w tym języku pojęciem centralnym.

T.R. Werner, 25 lutego 2017; 22:31