Wskaźniki (typ wskaźnikowy) - część pierwsza. Adresowanie komórek, operator wyłuskania.

Wprowadzenie

Mam nadzieję, że dzięki poprzedniej lekcji, wiesz już doskonale czym jest referencja. Czas poznać kolejny typ - typ wskaźnikowy.

Wskaźnik podobnie jak referencja jest typem pochodnym. I tu właściwie całe podobieństwo do referencji się kończy. Tak jak referencja jest zagadnieniem w miarę łatwym do zrozumienia, tak typ wskaźnikowy jest do zrozumienia znacznie trudniejszy i sprawia problemy nawet zaawansowanym programistom.

W najbliższych lekcjach przedstawię Ci informacje o wskaźnikach. Chcę jednak, aby utkwiło w Twojej świadomości, że są to tak naprawdę tylko podstawowe informacje dotyczące wskaźników i że jak na razie Twoja wiedza odnośnie wskaźników będzie dość mocno ograniczona.

To wszystko wystarczy Ci przez jakiś czas na poznawanie nowych mechanizmów, a gdy zajdzie już taka potrzeba, wówczas informacje na temat wskaźników zostaną poszerzone.

Jednocześnie chcę Ci uświadomić, że wskaźniki są w języku C++ używane bardzo powszechnie i stosowane we wszystkich większych programach. Dlatego też uczulam Cię na zwracanie uwagi na wszystkie szczegóły i zapamiętanie w zasadzie wszystkiego, co postaram Ci się tu przedstawić.

Zmienne w pamięci operacyjnej

Musisz wiedzieć, że każda zmienna, która jest zadeklarowana w programie zostaje umieszczona w pamięci operacyjnej na czas działania programu.

Pamięć operacyjna to zbiór komórek pamięci. Każda komórka w pamięci ma określone położenie - adres. Adres jest liczbą, która jednoznacznie określa położenie komórki w pamięci.

Każda zmienna, którą deklarujemy w programie jest umieszczana w komórkach pamięci. W zależności, jakiego typu jest ta zmienna, może ona zostać umieszczona w jednej, dwóch lub większej liczbie komórek pamięci. Jednak adres zmiennej pokazuje zawsze na pierwszą komórkę pamięci - tam zaczyna się właśnie nasza zmienna.

Zwróć uwagę na dwa pojęcia - adres komórki i adres zmiennej. Zmienna może zajmować kilka komórek a każda z komórek ma własny adres. Jednak zmienna ma tylko jeden adres, równy adresowi pierwszej komórki pamięci, którą zajmuje.

Na szczęście my w naszych programach nie musimy nigdy podawać konkretnej wartości adresu zmiennej. Gdyby tak było, to okazałoby się, że nasz program działałby tylko w ściśle określonych warunkach, bowiem przecież nie możemy nigdy przewidzieć, czy dana komórka pamięci będzie zajęta czy nie.

Trzeba przecież pamiętać, że oprócz naszego programu, na komputerze mamy system operacyjny z wieloma procesami, a także najprawdopodobniej w tym czasie, na komputerze jest uruchomionych co najmniej kilka dodatkowych programów.

Na szczęście jednak, aż takimi szczegółami nie będziemy się musieli przejmować. Przejdźmy zatem do tego, czym musimy się martwić w naszych programach.

Operator pobrania adresu w C++

W dotychczasowych programach wiedza czym jest adres nie była Ci potrzebna. Teraz będzie już inaczej. Do pobierania adresu zmiennej dowolnego typu służy operator pobrania adresu - w języku C++ takim operatorem jest &. W rzeczywistości z operatora & - operatora pobrania adresu korzystaliśmy już co najmniej jeden raz, a mianowicie w przypadku referencji.

Kiedy deklarowaliśmy (i jednocześnie inicjowaliśmy) zmienną typu referencyjnego, mówiliśmy kompilatorowi tak naprawdę, że adres referencji ma być równy adresowi zmiennej, z którą została związana referencja. Zapis jednak w tym przypadku był nieco mylący, bowiem operator pobrania adresu znajdował się tylko i wyłącznie po stronie referencji, a po stronie zmiennej operatora tego już nie było.

Zostawmy jednak referencję i przejdźmy do adresów. W rzeczywistości jak już wspomniałem, w języku C++, każda zmienna ma swój adres. Można się o tym stosunkowo łatwo przekonać, dokonując wypisania wartości na ekran. Oto prosty przykład:

#include <iostream>

using namespace std;

int main()
{  
  int mojaLiczba=23;
  cout <<"Wartosc zmiennej to "<<mojaLiczba<<endl;
  cout <<"Adres zmiennej to "<< &mojaLiczba<<endl;
 
  cout <<endl<<"Nacisnij ENTER aby zakonczyc..."<<endl;
  getchar();  
  return 0;
}
program nr 27.1

Jak widzisz, w powyższym programie, wypisujemy wartość zmiennej oraz adres zmiennej. Do wypisania adresu posługujemy się operatorem pobrania adresu & - wypisanie odbywa się tak jak to miało miejsce do tej pory.

Zagadką może pozostawać, co znaczy wypisany adres, a konkretnie 0x na jego początku. Otóż domyślnie adres jest wypisywany w systemie szesnastkowym (a nie dziesiętnym) i żeby każdy o tym wiedział, że chodzi o system szesnastkowy, dopisywane jest na początek właśnie 0x.

Oczywiście moglibyśmy sobie zażyczyć wypisania adresu w inny sposób, ale nie o to nam teraz chodzi. Ważne jest, że coś takiego jak adres istnieje i że można jego wartość nawet wypisać na ekran (chociaż z wypisania adresu zmiennej na ekran jak się już możesz domyśleć, w praktyce się raczej nie korzysta).

Przedstawiłem Ci już trochę informacji, o tym jak są zorganizowane zmienne oraz w jaki sposób pobrać adres danej zmiennej. Czas poznać bohatera tej lekcji, czyli wskaźnik i dowiedzieć się, jak się nim posługujemy.

Wskaźnik - informacje podstawowe

Wskaźnik jak sama nazwa wskazuje służy do wskazywania (czyli do pokazywania) na zmienne dowolnego typu. Każdy wskaźnik jest określonego typu i na tym etapie kursu przyjmiemy, że wskaźnik może pokazywać na zmienne tylko takiego typu jakiego został zadeklarowany (czyli podobnie jak referencja).

Schematycznie, wskaźnik deklarujemy następująco:

typ *nazwaWskaznika;

Jak widzisz, wskaźnik oznaczamy za pomocą gwiazdki pojawiającej się przed nazwą zmiennej. Zwróć uwagę, że nie zastosowałem tutaj inicjalizacji. Inicjalizacja bowiem w przypadku wskaźników jest możliwa, ale nie jest wymagana (w przypadku referencji inicjalizacja była konieczna). Prawdę mówiąc, na samym początku nauki, najlepiej nie stosować inicjalizacji wskaźników, ale o tym później.

Czas przejść do prostego programu, w którym możemy zobaczyć wykorzystanie wskaźnika. Oto on:

#include <iostream>

using namespace std;

int main()
{  
  int liczba=6; // zmienna typu int
  int *wskaznik; // wskaznik do typu int
 
  wskaznik=&liczba; // powiazanie wskaznika ze zmienna liczba
 
  cout <<"Wartosc zmiennej liczba wynosi "<<liczba<<endl;
  cout <<"Wartosc wskaznika wynosi "<<wskaznik<<endl;
 
  cout <<endl<<"Nacisnij ENTER aby zakonczyc..."<<endl;
  getchar();  
  return 0;
}
program nr 27.2

Zanim zaczniesz zastanawiać się, skąd się wzięły takie "dziwne" wyniki po uruchomieniu programu, warto skupić się na kodzie programu.

Przede wszystkim na początku deklarujemy zmienną typu int, a następnie wskaźnik do typu int. Zauważ, że typy są ze sobą zgodne, bo jak już wcześniej wspomniałem, na tym etapie kursu będziemy przyjmować, że w przypadku powiązania wskaźnika ze zmienną, typy muszą być zgodne.

Następna linijka to chyba najważniejsza linijka w tym programie i w ogóle w przypadku wskaźników, więc jeśli tylko uda Ci się ją dobrze zrozumieć, to będzie to już połowa sukcesu.

Wskaźniki przechowują adresy! Jest to bardzo ważne stwierdzenie i możesz się nauczyć go na pamięć. Wartością każdego wskaźnika jest adres zmiennej, na którą ten wskaźnik wskazuje (o ile tylko wskaźnik został ustawiony na daną zmienną).

Teraz powinno Ci być nieco łatwiej zrozumieć krytyczną linijkę programu. Po lewej stronie mamy wartość wskaźnika. Dlaczego jest to wartość wskaźnika? Podobnie było przy referencji - mimo że w miejscu inicjalizacji używaliśmy operatora & to jednak w dalszej części programu używaliśmy już tylko nazwy zmiennej, bez operatora &. Tutaj mamy identycznie - w miejscu deklaracji pojawia się * na oznaczenie wskaźnika, jednak przypisanie, to już zwykła operacja i nie musimy ponownie użyć *.

Ponieważ jak już wyraźnie zaznaczyłem, wskaźniki przechowują adresy, zatem po prawej stronie musimy mieć adres zmiennej, na którą zostaje ustawiony wskaźnik. Nie możemy przecież do czegoś, co przechowuje adresy wpisać czegoś zupełnie innego.

W ten oto sposób, mam nadzieję, że udało Ci się zrozumieć najważniejszą linijkę w programie. Ostatnią kwestią pozostaje to, że po uruchomieniu programu widzisz, że wartość wskaźnika jest zupełnie inna niż wartość zmiennej. O tym już za chwilę.

Operator wyłuskania

Jak już wiesz, wypisując wartość wskaźnika nie otrzymujemy wartości zmiennej, na którą wskaźnik wskazuje. Co zatem zostaje wypisane? Nie trudno się domyśleć, że zostaje wypisany adres zmiennej, na którą wskaźnik wskazuje (stąd charakterystyczne 0x na początku).

W rzeczywistości w programie, rzadko kiedy interesuje nas adres zmiennej. Znacznie częściej interesuje nas wartość zmiennej pod tym adresem. Aby jednak wypisać wartość zmiennej, na którą wskazuje wskaźnik, musimy użyć specjalnego operatora - operatora wyłuskania.

Operator wyłuskania oznaczamy za pomocą * i poprzedzając nazwę wskaźnika tym operatorem, uzyskujemy wartość zmiennej, na którą dany wskaźnik wskazuje (w przypadku typów prostych).

Pragnę jednak zwrócić Twoją uwagę na to, że mimo że gwiazdka pojawia się w momencie deklaracji wskaźnika, jak i jako wydobycia, wyłuskania wartości zmiennej, na którą wskaźnik pokazuje, to z operatorem wyłuskania mamy do czynienia tylko w tym drugim przypadku. W języku C++ wykorzystano po prostu niektóre symbole do oznaczenia różnych, choć podobnych zjawisk, stąd na początku możesz mieć problem ze zrozumieniem, kiedy który symbol co oznacza.

Wykorzystajmy teraz operator wyłuskania w przykładowym programie. Dodatkowo, aby udowodnić Ci, że to wszystko co tutaj napisałem jest prawdą, dodamy kilka instrukcji do programu:

#include <iostream>

using namespace std;

int main()
{  
  int liczba=6; // zmienna typu int
  int *wskaznik; // wskaznik do typu int (1)
 
  wskaznik=&liczba; // powiazanie wskaznika ze zmienna liczba
 
  cout <<"Wartosc zmiennej liczba wynosi "<<liczba<<endl;
  cout <<"Adres zmiennej wynosi "<< &liczba <<endl;
  cout <<"Wartosc wskaznika wynosi "<<wskaznik<<endl;
  cout <<"Wyluskana wartosc ze wskaznika wynosi "<< *wskaznik <<endl; // (2)
 
  // porownajmy adres wskaznika i adres zmiennej
 
  if (&liczba==wskaznik)  // (3)
    cout <<"Obie zmienne wskazuja na ten sam adres"<<endl;
  else
    cout <<"Zmienne wskazuja na rozne adresy"<<endl;
 
  // porownajmy wyluskana wartosc i wartosc zmiennej
 
  if (liczba == *wskaznik) // (4)
    cout <<"Obie wartosci sa identyczne"<<endl;
  else
    cout <<"Wartosci sa inne"<<endl;  
 
  cout <<endl<<"Nacisnij ENTER aby zakonczyc..."<<endl;
  getchar();  
  return 0;
}
program nr 27.3

Część programu jest identyczna z poprzednim programem, więc nie będę jej już ponownie omawiał. Zauważ jednak, że wypisując adres zmiennej i wartość wskaźnika, który wskazuje na tę zmienną, można łatwo stwierdzić, że obie te wartości są identyczne. Stosując operator wyłuskania (linia opatrzona komentarzem (2)), wypisujemy wartość zmiennej, na którą pokazuje wskaźnik. Oczywiście wypisana wartość jest identyczna z wartością zmiennej.

Porównaj także linie opatrzone komentarzem (1) i (2). Jak już wspominałem, gwiazdka nie zawsze oznacza operator wyłuskania. W pierwszym przypadku gwiazdka oznacza po prostu deklarację wskaźnika. Dopiero za drugim razem mamy do czynienia z operatorem wyłuskania.

Mimo, że już wcześniej dokonaliśmy wypisania adresu zmiennej i wartości wskaźnika i stwierdziliśmy, że są one identyczne, w miejscu opatrzonym komentarzem (3) dokonujemy porównania adresu zmiennej i wskaźnika, chociaż rezultat jest oczywisty. Ma Ci to jeszcze raz uświadomić, że wartość wskaźnika to adres zmiennej. Jeśli porównasz tę linijkę z linijką, w której wskaźnik jest ustawiany na zmienną, to widać, że poza operatorami (= i ==), zapis jest identyczny.

Podobnie postępujemy w miejscu opatrzonym komentarzem (4), z tym tylko, że tym razem nie porównujemy adresów, a wartość zmiennej z wartością wyłuskaną ze wskaźnika. Oczywiście wartości są identyczne, bowiem wskaźnik cały czas pokazuje na zmienną.

Wskaźnik a zmienna

Podobnie, jak w przypadku referencji, wartość wyłuskana ze wskaźnika jest identyczna z wartością zmiennej, na którą wskaźnik pokazuje. Dzieje się tak dlatego, że jak już wiesz, wskaźnik przechowuje tylko adres zmiennej. Zatem nieważne, w jaki sposób zajdzie zmiana wartości zmiennej znajdującej się pod tym adresem, bowiem, gdy tylko będziemy usiłowali wyłuskać wskaźnik, zostanie odczytana wartość właśnie z tego obszaru pamięci.

Zasada działa również w drugą stronę (czyli znów podobieństwo do referencji). Gdy tylko dokonamy zmiany wartości wyłuskanej ze wskaźnika, wówczas wartość zmiennej będzie zmieniona, bowiem jest to nadal to samo miejsce w pamięci.

Dosyć jednak teorii - czas pokazać to "na żywo", a wtedy na pewno zrozumienie powyższych słów będzie łatwiejsze. Oto prosty przykład:

#include <iostream>

using namespace std;

int main()
{  
  float a=6.23; // zmienna typu float
  float *wsk; // wskaznik do typu float
 
  wsk=&a; // powiazanie wskaznika ze zmienna a
 
  cout <<"Wartosc zmiennej a wynosi "<<a<<endl;
  cout <<"Adres zmiennej a wynosi "<< &a <<endl;
  cout <<"Wartosc wskaznika wynosi "<<wsk<<endl;
  cout <<"Wyluskana wartosc ze wskaznika wynosi "<< *wsk <<endl;
 
  a=25.98;
  cout <<endl<<"Wartosc zmiennej a wynosi "<<a<<endl;
  cout <<"Adres zmiennej a wynosi "<< &a <<endl;
  cout <<"Wartosc wskaznika wynosi "<<wsk<<endl;
  cout <<"Wyluskana wartosc ze wskaznika wynosi "<< *wsk <<endl;
 
  // wsk=21; // UWAGA - BLAD !!!
  *wsk=21;
  cout <<endl<<"Wartosc zmiennej a wynosi "<<a<<endl;
  cout <<"Adres zmiennej a wynosi "<< &a <<endl;
  cout <<"Wartosc wskaznika wynosi "<<wsk<<endl;
  cout <<"Wyluskana wartosc ze wskaznika wynosi "<< *wsk <<endl;
 
  *wsk=*wsk+1; // lub ++(*wsk);
  cout <<endl<<"Wartosc zmiennej a wynosi "<<a<<endl;
  cout <<"Adres zmiennej a wynosi "<< &a <<endl;
  cout <<"Wartosc wskaznika wynosi "<<wsk<<endl;
  cout <<"Wyluskana wartosc ze wskaznika wynosi "<< *wsk <<endl;  
   
  cout <<endl<<"Nacisnij ENTER aby zakonczyc..."<<endl;
  getchar();  
  return 0;
}
program nr 27.4

Program, mimo że mogłoby się wydawać dość długi, ilustruje to o czym wspomniałem. Zmiany wartości zmiennej lub wartości wyłuskanej ze wskaźnika są odzwierciedlane w tej drugiej. Dodatkowo, jak widać, adres zarówno zmiennej, jak i wskaźnika jest taki sam, bo wskaźnik przez cały czas pokazuje na zmienną, a zmienna położenia oczywiście też nie zmienia.

Zwróć swoją uwagę na linijkę opatrzoną komentarzem UWAGA - BLAD!!!. Jak myślisz - co w tej linijce jest źle? Mam nadzieję, że udało Ci się odgadnąć - wartości wskaźnika (czyli adresowi) przypisaliśmy wartość 21. Oznaczałoby to tyle, że od teraz wskaźnik nie pokazuje na zmienną a, tylko na miejsce w pamięci o adresie 21. W wielu przypadkach kompilator ostrzeże Cię o takim zdarzeniu i w ogóle programu skompilować Ci się nie uda. Jednak już trochę starsze kompilatory program skompilują i wtedy katastrofa gwarantowana. Dlatego dobrze zapamiętaj, że zmieniamy zawsze wyłuskaną wartość ze wskaźnika, a nie samą wartość wskaźnika.

W ostatniej części programu, dokonujemy zwiększenia wyłuskanej wartości ze wskaźnika o 1. W podobny sposób możemy dokonywać wszystkich innych operacji na wyłuskanej ze wskaźnika wartości. Wszystkie te zmiany, tak jak widać, będą miały odzwierciedlenie w wartości zmiennej, na którą pokazuje wskaźnik.

Zmiana ustawienia wskaźnika

Jak do tej pory, korzystając ze wskaźnika, ustawialiśmy go zawsze na jedną zmienną. W poprzednim programie przekonaliśmy się, że wszystkie zmiany wartości zmiennej i wartości wyłuskanej ze wskaźnika, powodują automatycznie zmianę tej drugiej.

Okazuje się jednak, że wskaźniki w C++ charakteryzują się jeszcze jedną właściwością, a mianowicie, że raz mogą wskazywać na jedną zmienną a kiedy indziej na inną. Jest to cecha odróżniająca wskaźnik od referencji, bowiem jak pamiętasz, w przypadku referencji, referencja jest na stałe wiązana w momencie inicjalizacji ze zmienną i tego powiązania zmienić nie można. Takiego ograniczenia nie ma w przypadku wskaźników. Poniższy przykład obrazuje to zagadnienie:

#include <iostream>
#include <cstring>

using namespace std;

int main()
{  
  string a="Zmienna a", b="Zmienna b", c="Zmienna c"; // zmienne typu string
  string *wsk; // wskaznik do typu string
 
  wsk=&a; // powiazanie wskaznika ze zmienna a
 
  cout <<"Wartosc zmiennej a wynosi: "<<a<<endl;
  cout <<"Wyluskana wartosc ze wskaznika wynosi: "<< *wsk <<endl;
 
  *wsk="Tu byla zmienna a, ale teraz zmienilismy jej wartosc";
 
  cout <<endl<< *wsk << endl;
  cout << a <<endl;
 
  wsk = &b; // powiazanie wskaznika ze zmienna b
 
  cout <<endl<<"Wartosc zmiennej a wynosi: "<<a<<endl;
  cout <<"Wyluskana wartosc ze wskaznika wynosi: "<< *wsk <<endl;
  cout <<"Wartosc zmiennej b wynosi: "<<b<<endl;
 
  // nie wypisujemy a, bo na razie a sie nie zmieni
 
  *wsk=c; // przypisanie za pomoca wskaznika, rownowazne b=c
  cout <<endl<<"Wyluskana wartosc ze wskaznika wynosi: "<< *wsk <<endl;
  cout <<"Wartosc zmiennej b wynosi: "<<b<<endl;  
  cout <<"Wartosc zmiennej c wynosi: "<<c<<endl;        
 
  wsk = &c; // powiazanie wskaznika ze zmienna c
 
  *wsk="Tu kiedys byla zmienna c";
  cout <<endl<<"Wyluskana wartosc ze wskaznika wynosi: "<< *wsk <<endl;
  cout <<"Wartosc zmiennej b wynosi: "<<b<<endl;  
  cout <<"Wartosc zmiennej c wynosi: "<<c<<endl;  
 
  // nie wypisujemy juz b i c - ich wartosci sie nie zmienia
 
  wsk = &a; // powiazanie wskaznika ze zmienna a (PONOWNIE)
  *wsk="Przykladowy testowy tekst";
  cout <<endl<<"Wyluskana wartosc ze wskaznika wynosi: "<< *wsk <<endl;
  cout <<"Wartosc zmiennej a wynosi: "<<a<<endl;            
   
  cout <<endl<<"Nacisnij ENTER aby zakonczyc..."<<endl;
  getchar();  
  return 0;
}
program nr 27.5

W powyższym programie zademonstrowałem Ci, że rzeczywiście wskaźnik możemy ustawiać w trakcie działania programu, raz na jedną zmienną, a raz na inną. Tutaj wskaźnik pokazuje na zmienną a, później na zmienną b, następnie na zmienną c, a w końcu znowu na zmienną a. W międzyczasie dokonujemy kilku modyfikacji wartości wyłuskanej ze wskaźnika i jak widzisz wszystko działa zgodnie z oczekiwaniami.

Podsumowanie

W tej lekcji przedstawiłem Ci najbardziej podstawowe informacje o wskaźnikach. Wiem, że może nie było łatwo, ale jeśli czegoś nie rozumiesz, przeczytaj ponownie. Kolejne dwie lekcje będą bowiem kontynuacją informacji o wskaźnikach i wiedza przedstawiona w tej lekcji, będzie niezbędna do zrozumienia trudniejszych zagadnień.

powrót