Wskaźniki (typ wskaźnikowy) - część druga. Inicjalizacja, zasady, stałe wskaźniki

Wprowadzenie

W tej lekcji przedstawię Ci kolejne zagadnienia związane ze wskaźnikami. Jeśli nie znasz poprzedniej lekcji, lepiej się z nią zapoznaj, bowiem przedstawione tutaj problemy i metody, będą bazowały niemal w całości na wiedzy z poprzedniej lekcji.

Inicjalizacja wskaźnika

Jak dobrze wiesz, wskaźniki nie muszą być inicjalizowane. Można dokonać zwykłego przypisania, tak jak to robiliśmy w poprzedniej lekcji i wszystko działa jak trzeba. W rzeczywistości jednak, inicjalizację wskaźników wykorzystuje się co najmniej równie często, co zwykłe ustawianie wskaźnika, o ile nie częściej.

Jeśli dobrze znasz poprzednią lekcję, powinno Ci się przypomnieć moje stwierdzenie dotyczące inicjalizacji, że najlepiej jej nie stosować na początkowym etapie nauki. Zdania oczywiście nie zmieniam, problem jednak w tym, że od inicjalizacji wskaźników, nawet na początkowym etapie ich nauki, uciec się nie da.

Czemu zatem moim zdaniem, inicjalizacja wskaźników jest taka zła? Chodzi mianowicie o jej zapis - niemal wszyscy, którzy stosują inicjalizację, mają problem ze stosowaniem tego wszystkiego, co przedstawiłem Ci w poprzedniej lekcji lub szybko zapominają wpajane im wcześniej reguły. Apeluję do Ciebie - nie zapominaj o tym wszystkim, czego nauczyłem Cię w poprzedniej lekcji, bo wskaźniki naprawdę będą Ci wielokrotnie potrzebne.

Przejdźmy jednak do sprawcy całego zamieszania, czyli inicjalizacji. Schematycznie inicjalizacja wskaźnika wygląda następująco:

typ *nazwaWskaznika = &nazwaZmiennej;

Czy widzisz już problem albo coś dziwnego w powyższym zapisie? Jeśli nie, to przyjrzyj się jeszcze raz i chwilę się zastanów.

Gdy dobrze się przyjrzysz, zauważysz, że po lewej stronie mamy jakby wyłuskanie wartości ze wskaźnika, a po prawej stronie mamy adres zmiennej, z którą wskaźnik zostaje związany. Jeśli jednak dokładnie udało Ci się przeczytać poprzednią lekcję, wiesz dobrze, że gwiazdka nie zawsze oznacza wyłuskanie - w tym przypadku oznacza ona po prostu, że tworzona zmienna jest wskaźnikiem i nie oznacza wyłuskania.

Gdzie zatem konkretnie pojawia się błąd? Otóż, początkujące osoby, stosując często bezmyślnie inicjalizację, zapominają, że wskaźniki przechowują wyłącznie adresy zmiennych, na które wskazują, a żeby wydobyć faktyczną wartość zmiennej powiązanej ze wskaźnikiem, należy skorzystać z operatora wyłuskania.

Co gorsza, jeśli takie osoby mają w programie dwie zmienne i jeden wskaźnik i w którymś miejscu programu chcą przestawić wskaźnik z jednej zmiennej na drugą, stosują błędny zapis, wzorując się na inicjalizacji:

*nazwaWskaznika = &nazwaZmiennej; // UWAGA - BLAD!!!

Musisz bowiem przyznać, że nie wiedząc, w jaki sposób dokonuje się przypisania wartości wskaźnika, a widząc inicjalizację wskaźnika, również Tobie, jako pierwsza myśl, przyszedłby do głowy taki właśnie zapis. Na szczęście większość kompilatorów ostrzeże przed takim działaniem, jednak nie wszystkie.

Nawet jeśli kompilator zgłasza błąd, osoby, które stały się ofiarami mylnego zapisu inicjalizacji, nie wiedzą o co tak naprawdę chodzi i tracą niekiedy cenny czas na szukaniu błędów. Dlaczego Ci o tym wspominam? Bo naprawdę widziałem to już wiele razy. Dlatego też pamiętaj, że inicjalizacja mimo że przydatna, w przypadku wskaźników jest zwodnicza i żeby ją stosować, naprawdę warto wcześniej dobrze wiedzieć, jak działają wskaźniki.

Wskaźnik - ważne zasady i cechy

Mimo że to nie koniec ani tej lekcji ani tematyki wskaźników, najwyższa pora, aby zebrać informacje na ich temat w sposób podobny, jak to zrobiłem w przypadku referencji.

Oto 5 ważnych zasad dotyczących wskaźników:

Jak widzisz, w zasadzie prawie wszystkie właściwości wskaźników różnią się od właściwości dotyczących referencji mimo że momentami mogło Ci się wydawać, że referencja i wskaźnik to coś bardzo podobnego. Mimo że wszystkie te zasady powinny być dla Ciebie w miarę oczywiste, postaraj się je sobie przyswoić, bowiem wiedzy na temat wskaźników nigdy nie jest za wiele.

Wskaźniki a stałe

Jako, że nie zawsze chcemy pozwolić wskaźnikom, aby robiły to co im się podoba, warto rozważyć sposób ograniczenia wskaźnikom ich wszystkich możliwości. Oczywiście wskaźniki same nic nigdy nie robią, są tylko narzędziem programisty. Programista powinien jednak nakładać ograniczenia na siebie (lub ewentualnie na współprogramistów), aby zminimalizować szansę na niekontrolowane modyfikacje zmiennych, bo jak wiesz, za pomocą wskaźników można zmieniać wartości zmiennych, na które wskaźniki pokazują.

W przypadku wskaźników bardzo często stosuje się właśnie pewne ograniczenia, bowiem niekiedy za pomocą wskaźników można spowodować bardzo duże szkody w programie, a błędy tego typu to chyba jeden z najtrudniejszych do wykrycia rodzajów błędów. Dowiedzmy się zatem, jak takie ograniczenia można nałożyć.

Wskaźnik do stałego typu

Najczęściej stosowaną metodą jest użycie wskaźnika do stałego typu. Taki wskaźnik schematycznie deklarujemy następująco:

const typ *nazwaWskaznika;

Stosując przedstawiony już wcześniej sposób odczytu deklaracji od jej końca stwierdzamy, że nazwaWskaznika jest wskaźnikiem (*) do pewnego typu o symbolicznej nazwie typ (typ), który jest stały (const).

Zanim wyjaśnię, co zyskaliśmy w taki sposób, najlepiej będzie, jeśli zobaczysz jak to działa w praktyce. Oto krótki przykładowy program:

#include <iostream>

using namespace std;

int main()
{  
  float wzrost=183, waga=70; // zmienne typu float
  const float *wsk = & wzrost; // wskaznik do stalego typu float + inicjalizacja
  float *zwyklyWsk = &waga; // wskaznik do typu float
 
  cout <<"Wartosc zmiennej wzrost wynosi: "<<wzrost<<endl;  
  cout <<"Wyluskana wartosc ze wskaznika wynosi: "<< *wsk <<endl;
 
  wzrost+=5; // zmiana wzrostu (ktos sie pomylil przy mierzeniu)
  cout <<endl<<"Wartosc zmiennej wzrost wynosi: "<<wzrost<<endl;  
  cout <<"Wyluskana wartosc ze wskaznika wynosi: "<< *wsk <<endl;
 
  // usilujemy wrocic do poprzedniej wartosci wzrostu
  // *wsk-=5; // UWAGA - BLAD !!! wskaznik do stalego typu!
 
  wsk=&waga; // wskaznik pokazuje od teraz na wage
 
  cout <<endl<<"Wartosc zmiennej waga wynosi: "<<waga<<endl;  
  cout <<"Wyluskana wartosc ze wskaznika wynosi: "<< *wsk <<endl;
       
  waga-=3; // zmniejszamy wage o 3  
  cout <<endl<<"Wartosc zmiennej waga wynosi: "<<waga<<endl;  
  cout <<"Wyluskana wartosc ze wskaznika wynosi: "<< *wsk <<endl;      
 
  // usilujemy powrocic do wczesniejszej wagi
  // *wsk+=3; // UWAGA - BLAD !!! wskaznik do stalego typu!
 
  *zwyklyWsk+=3; // wszystko OK - ten wskaznik nie ma ograniczen
  cout <<endl<<"Wartosc zmiennej waga wynosi: "<<waga<<endl;  
  cout <<"Wyluskana wartosc normalnego wskaznika to: "<< *zwyklyWsk <<endl;  
  cout <<"Wyluskana wartosc ze wskaznika wynosi: "<< *wsk <<endl;    
   
  cout <<endl<<"Nacisnij ENTER aby zakonczyc..."<<endl;
  getchar();  
  return 0;
}
program nr 28.1

W programie mamy dwie zmienne i dwa wskaźniki - jeden do stałego typu, a drugi zwykły. Pokazując wskaźnikiem do stałego typu na daną zmienną, możemy wyłuskiwać wartość zmiennej, ale wszelkie próby zmiany wartości zmiennej poprzez wskaźnik, kończą się niepowodzeniem, bo wskaźnik jest do stałego typu.

Z drugiej jednak strony, wartość zmiennej zmienić możemy poprzez bezpośrednie odwołanie do wartości zmiennej i wszystkie te zmiany są odzwierciedlane w wartości wyłuskanej ze wskaźnika. Zwróć również uwagę, że wskaźnikiem do stałego typu możemy pokazywać na różne zmienne - raz wskaźnik ustawiamy na zmienną wzrost,a drugi raz na zmienną waga.

Jak widać dla wskaźnika, który nie jest do stałego typu, nie ma żadnych ograniczeń. Wskaźnik może robić co mu się podoba, w tym zmieniać wartość zmiennej.

Co zatem zyskujemy stosując wskaźnik do stałego typu? Używając takich wskaźników, mamy pewność, że zmiana wartości zmiennej może nastąpić tylko bezpośrednio przez zmienną i w ten sposób unikamy przypadkowych błędów spowodowanych zmianą wartości zmiennej za pomocą wskaźnika.

Stały wskaźnik

Mimo, że wskaźnik do stałego typu jest bardzo przydatny i chyba najczęściej wykorzystywany z pozostałych metod ograniczania możliwości wskaźników, w programach stosuje się również stałe wskaźniki.

Aby utworzyć w programie stały wskaźnik do wybranego typu, musimy schematycznie napisać:

typ * const nazwaWskaznika = &nazwaZmiennej;

Rozszyfrowując po raz kolejny deklarację (tylko deklarację a nie inicjalizację), otrzymujemy, że nazwaWskaznika jest stałym (const) wskaźnikiem (*) do typu o symbolicznej nazwie typ (typ).

Zwróć jednak uwagę na inicjalizację. Jak się już zapewne domyślasz, skoro przedstawiłem tutaj inicjalizację, to jest ona konieczna. Rzeczywiście tak właśnie jest. Dlaczego? Otóż dlatego, że nazwaWskaznika jest typu const,a dowolną zmienną typu const trzeba zawsze zainicjalizować. I nie ma tu żadnego znaczenia, że operujemy właśnie na wskaźnikach.

Myśląc inaczej, gdybyśmy zastąpili wyrażenie typ * jakimś innym typem, na przykład int, otrzymalibyśmy wówczas int const nazwaWskaznika. Oczywiście jest to dokładnie to samo, co const int nazwaWskaznika, a wracając do lekcji o modyfikatorach, wiesz doskonale, że taka zmienna musi zostać koniecznie zainicjalizowana. Dlatego właśnie również w przypadku stałego wskaźnika, inicjalizacja jest niezbędna.

Poniżej przedstawiam prosty program na wykorzystanie stałego wskaźnika. Program demonstruje, co osiągnęliśmy dzięki stałemu wskaźnikowi:

#include <iostream>

using namespace std;

int main()
{  
  float wzrost=183, waga=70; // zmienne typu float
  // float * const wsk; // UWAGA - BLAD !!! Brak inicjalizacji
  float * const wsk = &wzrost;
 
  cout <<"Wartosc zmiennej wzrost wynosi: "<<wzrost<<endl;  
  cout <<"Wyluskana wartosc ze wskaznika wynosi: "<< *wsk <<endl;
 
  wzrost+=5; // zmiana wzrostu (ktos sie pomylil przy mierzeniu)
  cout <<endl<<"Wartosc zmiennej wzrost wynosi: "<<wzrost<<endl;  
  cout <<"Wyluskana wartosc ze wskaznika wynosi: "<< *wsk <<endl;
 
  // usilujemy wrocic do poprzedniej wartosci wzrostu
  *wsk-=5;
  cout <<endl<<"Wartosc zmiennej wzrost wynosi: "<<wzrost<<endl;  
  cout <<"Wyluskana wartosc ze wskaznika wynosi: "<< *wsk <<endl;
 
  // chcemy aby wskaznik pokazywal od teraz na zmienna waga
  // wsk = &waga;   // UWAGA - BLAD !!! Staly wskaznik jest staly !!!
   
  cout <<endl<<"Nacisnij ENTER aby zakonczyc..."<<endl;
  getchar();  
  return 0;
}
program nr 28.2

Jak widzisz z powyższego programu, wskaźnik musi zostać koniecznie zainicjalizowany. Ponadto, zmiany wartości zmiennej są oczywiście odzwierciedlane w wartości wyłuskiwanej ze wskaźnika. W przeciwieństwie do wskaźnika do stałego typu, poprzez stały wskaźnik możemy zmienić wartość zmiennej, na którą wskaźnik pokazuje.

Jakie zatem stały wskaźnik wprowadza ograniczenia? Tak naprawdę tylko jedno - jest on na zawsze powiązany z jedną zmienną. Jak widzisz, gdy w końcowej części programu, próbujemy ustawić wskaźnik na inną zmienną, okazuje się to niemożliwe. Wynika to oczywiście z własności modyfikatora const i tak samo było w przypadku stałych zmiennych.

Jakie zatem są korzyści użycia stałego wskaźnika? Używając stałego wskaźnika co prawda pozwalamy mu na modyfikację zmiennej, na którą wskazuje, ale zabraniamy mu wskazywania na jakiekolwiek inne zmienne. Dzięki temu gwarantujemy, że wskaźnik nie zmieni przez przypadek wartości jakiejś innej zmiennej, zamiast tej zmiennej, którą mieliśmy na myśli.

Stały wskaźnik do stałego typu

Ten przypadek to już tak naprawdę niemal zupełne ograniczenie możliwości wskaźnika. Mimo to, w niektórych sytuacjach, takie zabezpieczenie może nam się przydać i ochronić nas przed niepotrzebnymi kłopotami.

Jeśli chcemy utworzyć stały wskaźnik do stałego typu, piszemy schematycznie:

const typ * const nazwaWskaznika = &nazwaZmiennej;

Czytając deklarację, stwierdzamy, że nazwaWskaznika jest stałym (const) wskaźnikiem (*) do typu o symbolicznej nazwie typ (typ), który jest stały (const).

Jako, że stały wskaźnik do stałego typu ma cechy zarówno stałego wskaźnika, jak i wskaźnika do stałego typu, to znaczy, że inicjalizacja jest oczywiście niezbędna.

Poniższy program pokazuje możliwości (a raczej ograniczenia) stałego wskaźnika do stałego typu:

#include <iostream>

using namespace std;

int main()
{  
  float wzrost=183, waga=70; // zmienne typu float
  // const float * const wsk; // UWAGA - BLAD !!! Brak inicjalizacji
  const float * const wsk = &wzrost;
 
  cout <<"Wartosc zmiennej wzrost wynosi: "<<wzrost<<endl;  
  cout <<"Wyluskana wartosc ze wskaznika wynosi: "<< *wsk <<endl;
 
  wzrost+=5; // zmiana wzrostu (ktos sie pomylil przy mierzeniu)
  cout <<endl<<"Wartosc zmiennej wzrost wynosi: "<<wzrost<<endl;  
  cout <<"Wyluskana wartosc ze wskaznika wynosi: "<< *wsk <<endl;
 
  // usilujemy wrocic do poprzedniej wartosci wzrostu
  // *wsk-=5; // UWAGA - BLAD !!! Wskaznik do stalego typu !!!
 
  // chcemy aby wskaznik pokazywal od teraz na zmienna waga
  // wsk = &waga;   // UWAGA - BLAD !!! Staly wskaznik jest staly !!!
   
  cout <<endl<<"Nacisnij ENTER aby zakonczyc..."<<endl;
  getchar();  
  return 0;
}
program nr 28.3

Jak udało Ci się zaobserwować, utworzony przez nas stały wskaźnik do stałego typu, ma bardzo małe możliwości. Nie może on ani zmienić wartości zmiennej, na którą wskazuje ani też nie może nagle ustawić się na inną zmienną.

Co nam daje użycie w programie takiego wskaźnika? Dzięki stałemu wskaźnikowi do stałego typu, otrzymujemy 100% bezpieczeństwa. Wiemy, że taki wskaźnik nie sprawi nam żadnych problemów w programie, bo nie dajemy mu nawet takiej możliwości.

Stałe zmienne a wskaźniki

Mimo że ograniczenia możemy wprowadzać na poziomie wskaźników, to ograniczenia mogą się również pojawiać na poziomie zmiennych. Jeśli utworzymy stałą zmienną, to musimy zadeklarować odpowiedni wskaźnik do operacji na zmiennej.

Nie można przecież dopuścić do tego, żeby co prawda zmienna była stała, ale za pomocą wskaźnika można będzie zmienić jej wartość, bowiem doprowadziłoby to do chyba najbardziej niezrozumiałych zachowań w programach.

Jeśli zatem mamy stałą zmienną, to musimy zadeklarować wskaźnik do stałego typu. Wskaźnik ten może oczywiście tylko czytać wartość zmiennej i nie może jej zmieniać, bo nawet sama zmienna nie może zmieniać swojej wartości. Wskaźnik może natomiast być ustawiany na różne stałe zmienne, chyba, że dodatkowo byśmy narzucili ograniczenie, że wskaźnik musi być stały.

Poniższy program demonstruje użycie stałej zmiennej w połączeniu ze wskaźnikiem do stałego typu. Zwróć uwagę, że została zastosowana inicjalizacja, chociaż oczywiście można było dokonać zwykłego przypisania w następnej linijce (bo wskaźnik nie jest stały). Oto program:

#include <iostream>

using namespace std;

int main()
{  
  const float wzrost=183, waga=70; // stale zmienne typu float
 
  // float * wsk = &wzrost; // Wskaznik nie moze byc bardziej uprzywilejowany!
  const float *wsk = &wzrost;
 
  cout <<"Wartosc zmiennej wzrost wynosi: "<<wzrost<<endl;  
  cout <<"Wyluskana wartosc ze wskaznika wynosi: "<< *wsk <<endl;
 
  // wzrost+=5; // OCZYWISCIE nie mozna - zmienna jest stala
  // *wsk+=5; // nie mozna - wskaznik do stalego typu
 
  wsk = &waga; // ustawiamy wskaznik na zmienna waga
 
  cout <<endl<<"Wartosc zmiennej waga wynosi: "<<waga<<endl;  
  cout <<"Wyluskana wartosc ze wskaznika wynosi: "<< *wsk <<endl;
 
  // waga-=3; // OCZYWISCIE nie mozna - zmienna jest stala
  // *wsk+=3; // nie mozna - wskaznik do stalego typu  
   
  cout <<endl<<"Nacisnij ENTER aby zakonczyc..."<<endl;
  getchar();  
  return 0;
}
program nr 28.4

Podsumowanie

W tej lekcji pokazałem Ci inicjalizację wskaźników i ostrzegłem przed bezmyślnym jej stosowaniem. Ponadto zostały podsumowane wiadomości o wskaźnikach oraz przedstawione sposoby ograniczania możliwości wskaźników, co prowadzi do zwiększenia bezpieczeństwa w całym programie.

Kolejne informacje na temat wskaźników zdobędziesz w następnej lekcji. Pamiętaj jednak, że nadal to wszystko o czym tutaj mówimy, to zupełne podstawy wskaźników.

powrót