background image

Wskaźniki

Ogromną  zaletą  C++  i  bardzo  potężnym  narzędziem  jest  możliwość 
bezpośredniego manipulowania zawartością pamięci za pomocą wskaźników. 
Należy  jednak  pamiętać,  że  wskaźniki  są  bardzo  często  przyczyną  dużego 
zamieszania w programach w C++.

Wskaźnik jest zmienną przechowującą adres w pamięci.

Pamięć  jest  miejscem  przechowywania  wartości.  Pamięć  dzieli  się  na 
sekwencyjnie ułożone komórki. Każda komórka ma swój adres.
Każda zmienna, każdego typu umieszczona jest pod odrębnym adresem. 

każda komórka = 1 bajt
zmienna nWiek typu unsigned long 
= 4 bajty = 32 bity 
nazwa zmiennej nWiek wskazuje 
na pierwszy bajt 
adres zmiennej nWiek to 102

nWiek

100 101 102 103104 105106 107108109110111

        
0101

1111010
1

0011000
1

1011000
0

0101

background image

Pamięć  jest  różnie  adresowana,  w  zależności  od  typu  komputera.  Zazwyczaj 
programista nie musi wiedzieć jaki jest szczegółowy adres danej zmiennej, od 
tego  jest  kompilator.  Jeśli  jednak  chcielibyście  się  bliżej  z  tym  zapoznać, 
trzeba wykorzystać operator adresu ( & ) . 

Przykład

shortVar

longVar

sVar

0101

0000

5

ff90

ff81

ff82

ff83

ff84

ff85

ff86

ff87

ff88

ff8a ff8c

ff8d ff8f

ff8e

ff8b

ff89

1111

1111

0000

1111

1111

0000

0000

1000

- 65535

 65535

shortVar

longVar

sVar

0101

0000

5

ff90

ff81

ff82

ff83

ff84

ff85

ff86

ff87

ff88

ff8a ff8c

ff8d ff8f

ff8e

ff8b

ff89

1111

1111

0000

1111

1111

0000

0000

1000

- 65535

 65535

background image

Przypisywanie adresu do wskaźnika

Każda  zmienna  ma  swój  adres.  Nawet  nie  znając  konkretnego  adresu 
zmiennej można go przypisać do wskaźnika.
Przykład
Załóżmy,  że  mamy  zmienną  całkowitą  typu  int  o  nazwie  nWiek.  Aby 
zadeklarować  wskaźnik  do  przechowywania  adresu  tej  zmiennej  trzeba 
napisać: 

int *pWiek = 0;

Kiedy  deklaruje  się  zmienną  wskaźnikową,  to  można  w  niej  umieścić  adres 
jakiegoś obiektu w pamięci. 
W  tym  przypadku  zmienna  wskaźnikowa  pWiek  przechowuje  adres  zmiennej 
całkowitej typu int.

Zauważmy,  że  zainicjalizowaliśmy  wskaźnik  pWiek  wartością  0.  Wskaźnik, 
którego wartość wynosi zero określany jest jako 

null 

(pusty, nie wskazujący 

na  żaden  obiekt).  Jeśli  nie  wiecie  jaki  adres  przypisać  do  wskaźnika,  to 
przypiszcie mu wartość zero. 
Wskaźniki  niezainicjalizowane  żadną  wartością  określane  są  jako 

dzikie 

wskaźniki

.  Stanowią  one  potencjalne  zagrożenie  dla  programu,  gdyż  mogą 

przechowywać  adres  dowolnej  komórki  pamięci  (nie  wiemy  jakiej). 
Modyfikacja pamięci pod tym adresem może doprowadzić np. do zawieszenia 
się komputera.

background image

Chcielibyśmy teraz przypisać mu adres zmiennej nWiek.  

int nWiek = 50;   //stwórz zmienna
int *pWiek = 0;   //stwórz wskaźnik
pWiek = &nWiek;   //wstaw adres do wskaźnika

Przypisaliśmy adres dzięki operatorowi adresu ( & ). Gdybyśmy zapomnieli o 
tym  operatorze,  to  do  wskaźnika  zostałaby  przypisana  wartość  zmiennej 
nWiek  (a  nie  jej  adres).  Oznacza  to,  że  wskazywałby  on  na  zupełnie  inną 
komórkę pamięci, niż oczekiwaliśmy.

Dostęp do zmiennej za pomocą jej adresu określany jest jako dostęp 
pośredni. 

Dostęp 

pośredni 

oznacza 

modyfikowanie 

lub 

odczytywanie  wartości  zmiennej  za  pośrednictwem  jej  adresu 
przechowywanego we wskaźniku. 

Wskaźniki,  tak  jak  wszystkie  inne  zmienne,  mogą  mieć  dowolne, 
poprawne  w  C++  nazwy.  Przyjmijmy  konwencję  nazywania 
wskaźników  rozpoczynając  nazwę  od  litery  p  (z  ang.  wskaźnik  

pointer), 
np.: pWiek, pLiczba itp.

background image

Operator dostępu pośredniego

Operator dostępu pośredniego ( * ) może służyć do odczytywania i zmieniania 
wartości  zmiennej,  przechowywanej  pod  adresem  zawartym  we  wskaźniku. 
Normalna zmienna pozwala na bezpośredni dostęp do swojej wartości. 

Przykład

Jeśli stworzy się zmienną typu int o nazwie nTwojWiek i chce się jej przypisać 
wartość zmiennej nWiek to można to zrealizować w następujący sposób:

int nTwojWiek; 
nTwojWiek = nWiek;

Wskaźnik  pozwala  na  pośredni  dostęp  do  wartości  zmiennej,  której  adres 
przechowuje. Żeby przypisać wartość zmiennej nWiek do zmiennej nTwojWiek 
posługując się wskaźnikiem pWiek, trzeba napisać w ten sposób:

int nTwojWiek; 
nTwojWiek = *pWiek;

Operator  dostępu  pośredniego  (*)  przed  zmienną  pWiek  oznacza  "wartość 
przechowywana pod adresem zawartym w
". To przypisanie można przeczytać 
następująco:  "Weź  wartość  przechowywaną  pod  adresem  zawartą  we 
wskaźniku pWiek i przypisz ją do zmiennej nTwojWiek
".

background image

Wskaźniki, adresy i zmienne

Bardzo  ważne  jest,  aby  odróżniać  wskaźnik,  adres  który  ten  wskaźnik 
przechowuje  i  wartość  przechowywaną  pod  adresem  zawartym  we 
wskaźniku.  Wiele  nieporozumień  wynika  z  nieprawidłowej  interpretacji  i 
błędnego rozumienia tych trzech różnych terminów.

Przykład 

int nZmienna = 5;
int *pWskaznik = &nZmienna;

pWskaznik  jest  zadeklarowany  jako  wskaźnik  na  zmienną  typu  int  i  jest 
inicjalizowany  adresem  zmiennej  nZmienna.  pWskaznik  (jak  sama  nazwa 
wskazuje)  jest  wskaźnikiem.  Adres  przechowywany  przez  pWskaznik  jest 
adresem  zmiennej  zmienna.  Wartość  pod  adresem  przechowywanym  przez 
pWskaznik jest równa 5. 

nZmienna

100 101 102 103104 105106 107108109110111

0000010
1

5

pWskaznik

0000  
0000

0000  
0101

101

background image

Manipulowanie danymi za pomocą 

wskaźników

Jeśli  przypisze  się  do  wskaźnika  adres  jakiejś  zmiennej,  to  można 
wykorzystywać ten wskaźnik do manipulowania wartością tej zmiennej. 

Przykład

odczytujemy  wartość  spod  adresu 
przechowywanego 

pWiek 

wypisujemy tę wartość. 

do 

zmiennej 

adresie 

przechowywanym 

we 

wskaźniku 

pWiek 

(nMojWiek), 

przypisujemy 

wartość 7. Na 7 zmienia się zawartość 
zmiennej nMojWiek. 

do  zmiennej  nMojWiek  przypisujemy 
wartość  9.  Wartość  tę,  bezpośrednio 
i  pośrednio  odczytujemy  w  tych 
fragmentach kodu

background image

Kontrolowanie adresu

Wskaźniki pozwalają na manipulowanie adresami bez wiedzy o ich faktycznej 
wartości.  Powiedzieliśmy,  że  kiedy  przypisujemy  adres  zmiennej  do 
wskaźnika,  to  on  na  prawdę  ma  wartość  równą  adresowi  tej  zmiennej. 
Dlaczego by jednak tego nie sprawdzić? 

Przykład

Kwintesencja 
wskaźników:

Co przechowuje 
wskaźnik?

Jak odczytać tę 
wartość?

background image

    Zawsze  do  odczytania  lub  modyfikacji  wartości  zmiennej 

przechowywanej  pod  danym  adresem,  wykorzystuj  operator 
adresowania pośredniego ( *).

    Zawsze  inicjalizuj  wskaźniki  albo  konkretnym  adresem  zmiennej 

wartością null (lub 0).  

    Zawsze  pamiętaj  o  różnicy  pomiędzy  adresem  przechowywanym 

we wskaźniku, a wartością przechowywaną pod adresem. 

Zazwyczaj wskaźniki są wykorzystywane w trzech sytuacjach:

 Zarządzanie danymi w pamięci operacyjnej.

 Dostęp do wnętrza klas - danych i funkcji.

 Przekazywanie wartości do zmiennych poprzez referencje.

Programiści na ogół wyróżniają pięć obszarów pamięci:

 Obszar zmiennych globalnych

 Wolna pamięć

 Rejestry

 Kod programu

 Stos

background image

Zmienne  lokalne  wraz  z  parametrami  funkcji  są  przechowywane  na  stosie. 
Kod  znajduje  się  w  obszarze  kodu  programu  (co  jest  chyba  oczywiste). 
Zmienne  globalne  również  znajdują    się  w  przeznaczonym  dla  siebie 
obszarze.  Rejestry  są  wykorzystywane  do  wewnętrznego  zarządzania 
funkcjami  (np.  do  przechowywania  adresu  szczytu  stosu  lub  wskaźnika 
instrukcji).  Cała  pozostała  pamięć  jest  dla  programu  wolna  (określa  się  ją 
czasem jako stertę - ang. heap).
Problem  ze  zmiennymi  lokalnymi  polega  na  tym,  że  wraz  z  zakończeniem 
funkcji są one przez program "zapominane". Zmienne globalne rozwiązują ten 
problem,  jednak  kosztem  nieograniczonego  dostępu  do  nich  z  dowolnego 
miejsca  w  programie,  co  niesie  ze  sobą  znaczną  komplikację  kodu. 
Umieszczenie danych w wolnej pamięci operacyjnej rozwiązuje oba problemy.

Można  z  powodzeniem  traktować  wolną  pamięć  jako  ogromy  zbiór 
sekwencyjne  ułożonych  komórek  pamięci  "czekających"  na  dane.  Jednak 
dostęp do tych komórek nie jest tak swobodny jak np. dostęp do stosu. Przed 
wykorzystaniem komórki trzeba „poprosić" system operacyjny o przydzielenie 
adresu i zarezerwowanie odpowiedniej liczby komórek. Dopiero wtedy można 
taki adres przypisać do wskaźnika i wykorzystywać.

Stos,  w  momencie  wyjścia  z  funkcji,  jest  automatycznie  czyszczony. 
Wszystkie  zmienne  lokalne  są  wyrzucane  z  pamięci.  Dane  na  stercie  trwają 
aż  do  zakończenia  programu.  Jest  możliwość  zwolnienia  zarezerwowanej 
pamięci, jeśli nie jest ona już  potrzebna.

background image

Zaletą sterty:

•    pamięć  w  niej  zarezerwowana  jest  dostępna  tak  długo,  aż  jej  się 
bezpośrednio  nie  zwolni.  Jeżeli  zarezerwuje  się  pamięć  na  stercie  wewnątrz 
funkcji to po zakończeniu funkcji, będzie ona nadal zarezerwowana.

•  możliwość dostępu tylko przez te funkcje, które mają dostęp do wskaźnika 
danego  obszaru.  Gwarantuje  to  spójność  dostępu  do  danych  i  eliminuje 
problem  niepożądanej  modyfikacji  danych  przez  niepowołane  do  tego 
funkcje.

Aby móc wykorzystywać pamięć na stercie trzeba mieć możliwość stworzenia 
wskaźnika do obszaru na stercie i przekazania tego wskaźnika do wybranych 
funkcji.

background image

new

Do  alokacji  (rezerwacji)  pamięci  służy  w  C++  słowo  kluczowe  new
Następuje  po  nim    nazwa  typu  obiektu  dla  którego  rezerwujemy  pamięć. 
Dzięki temu kompilator wie,  ile pamięci ma zarezerwować. 

Wartością  zwracaną  przez  new  jest  adres  w  pamięci.  Musi  on  być 
przypisany do wskaźnika. 

Przykład

unsigned short int * pWskaznik; 
pWskaznik = new unsigned short int;

unsigned short int * pWskaznik = new unsigned short int;

W obu przypadkach, pWskaznik wskazuje na stercie na wartość typu 
unsigned short int. 

background image

Można ten wskaźnik wykorzystywać dokładnie tak, jak wskaźnik na zmienną i 
dowolnie przypisywać wartości do pamięci:

*pWskaznik = 72;

Oznacza  to:  "Wstaw  72  pod  adres  wskazywany  przez  pWskaznik"  albo 
"Przypisz 72 do obszaru wskazywanego przez pWskaznik".

background image

delete

Kiedy  zakończy  się  operacje  na  zarezerwowanym  obszarze  pamięci  i  nie 
będzie  się  jej  już  więcej  wykorzystywać  to  należy  użyć  instrukcji  delete  na 
wskaźniku do danego obszaru. 
Wskaźnik  zadeklarowany  w  funkcji  jest  zmienną  lokalną  tej  funkcji,  w 
przeciwieństwie  do  pamięci  na  stercie,  na  którą  wskazuje.  Kiedy  funkcja  się 
skończy  to  wskaźnik  ten,  tak  jak  wszystkie  zmienne  lokalne  zostanie 
wyrzucony z pamięci (ze stosu). Oznacza to, że wskaźnika już nie będzie, ale 
obszar  na  stercie  będzie  nadal  zarezerwowany.  Taki  obszar  jest  już  dla 
programu  niedostępny.  Takie  zjawisko  określane  jest  jako  ulatnianie  się 
pamięci. 
Tak zarezerwowana pamięci pozostanie zajęta (i niedostępna) aż do 
zakończenia się programu.
Żeby zwolnić pamięć na stercie, musisz użyć słowa kluczowego delete
Przykład

delete pWskaznik;

background image

Przykład

Mimo że w tym konkretnym 
przypadku  jest  nadmiarowa 
ta  instrukcja  delete  (koniec 
programu 

automatycznie 

zwolni  całą  zarezerwowaną 
pamięć) 

to 

dobrym 

zwyczajem  jest  zadbanie  o 
to,  aby  samemu  zwolnić, 
przed 

zakończeniem 

programu, 

całą 

wykorzystywaną  pamięć  na 
stercie. 

background image

Utrata obszarów na stercie

Innym przypadkiem, w którym tracimy dostęp do zarezerwowanego na 
stercie obszaru, jest przypisanie nowego adresu do wskaźnika przed 
zwolnieniem pamięci wskazywanej przez ten wskaźnik. 

unsigned short int * pWskaznik = new unsigned short int; 
*pWskaznik = 72;
pWskaznik = new unsigned short int;
*pWskaznik = 84;

Do  wskaźnika  pWskaznik  ponownie  przypisujemy,  nowy  adres  obszaru  na 
stercie  i  w  wstawiamy  do  tego  obszaru  wartość  84.  Pierwszy  obszar,  ten  z 
wartością  72  jest  nadal  zarezerwowany  ale  już  niedostępny,  ponieważ 
wskaźnik,  który  na  niego  wskazywał  otrzymał  nową  wartość.  Nie  ma 
możliwości  odczytania  ani  zmiany  zawartości  tego  obszaru.  Będzie  on 
niepotrzebnie zajmował pamięć aż do zakończenia się programu. 

unsigned short int * pWskaznik = new unsigned short int; 
*pWskaznik = 72;
delete pWskaznik;
pWskaznik = new unsigned short int;
*pWskaznik = 84;

background image

Tworzenie obiektów na stercie

Tak  jak  tworzyliśmy  wskaźniki  do  zmiennych  typu  int,  tak  samo  możemy 
stworzyć wskaźnik do dowolnego innego obiektu. Jeżeli zadeklaruje się obiekt 
typu  Kot,  to  można  zadeklarować  wskaźnik  do  obiektów  tej  klasy  i  stworzyć 
obiekt  na  stercie,  podobnie  jak  na  stosie.  Składnia  jest  tu  taka  sama  jak  w 
przypadku liczb całkowitych:

Kot *pKot = new Kot;

Zostanie  wywołany  konstruktor  domyślny,  ten  bez  parametrów.  Konstruktor 
jest  wywoływany  zawsze  w  momencie  tworzenia  obiektu  danej  klasy, 
niezależnie od tego, czy operacja ma miejsce na stosie, czy na stercie.

Usuwanie obiektów

Kiedy wywoła się delete na wskaźniku do obiektu klasy znajdującego się na 
stercie,  to  przed  zwolnieniem  pamięci  zajmowanej  przez  obiekt  zostanie 
wywołany destruktor klasy, której jest dany obiekt. Dzięki temu klasa ma np. 
możliwość  zwolnienia  dodatkowo  zarezerwowanej  pamięci  tak,  jak  jest  to 
robione podczas usuwania obiektów ze stosu w momencie wyjścia z funkcji. 

background image

Przykład

na  stosie  tworzony  jest  obiekt 
klasy  ZwyklyKot,  wskazywany 
przez pRags. 

background image

Dostęp do danych wewnętrznych klasy

Dotychczas,  dostęp  do  wewnętrznych  danych  i  funkcji  obiektów  klasy 
zadeklarowanych lokalnie realizowany był poprzez użycie operatora kropka ( . 
).  Żeby  dostać  się  do  elementów  obiektu  stworzonego  na  stercie  trzeba 
pośrednio odwołać się do tego obiektu za pomocą wskaźnika. 

Przykład

(*pRags).PobierzWiek();

wywołuje funkcję wewnętrzną  
PobierzWiek() 

Nawiasy gwarantują, że odwołanie do obiektu nastąpi przed próbą dostępu 
do funkcji PobierzWiek ().

Ponieważ takie stosowanie wskaźników jest raczej nieporęczne, C++ 
oferuje  specjalny  operator  dla  pośredniego  dostępu  do  obiektów 
wskazywanych  przez  wskaźniki.  Operator  "wskazujący  na"  (  ->  ) 
składający się z myślnika ( - ) i znaku większości ( > ). C++ traktuje 
to jako pojedynczy symbol. 

background image

Przykład

na stercie, tworzony jest 
obiekt  klasy  ZwyklyKot. 
Konstruktor 

domyślny 

ustala jego wiek na 5. 

wywoływana jest metoda 
PobierzWiek  ().  Ponieważ 
odwołanie 

następuje 

poprzez  wskaźnik,  to 
wykorzystujemy operator 
"wskazujący na" ( -> ). 

background image

Dane wewnętrzne na stercie

Jedna (lub  więcej) zmienna wewnętrzna  może  być wskaźnikiem na obiekt 
na stercie. Pamięć może być zarezerwowana w konstruktorze klasy (albo w 
jednej z jej metod) i może być zwolniona w destruktorze. 

Przykład

Funkcja wywołująca, w tym 

przypadku main (), "nie wie", że 

nJegoWiek i nJegoWaga są 

wskaźnikami. main () wywołuje 

metody PobierzWiek () i UstawWiek 

() , a szczegóły operacji na pamięci 

zaszyte są wewnątrz implementacji 

klasy. 

Podczas 

kasowania 

obiektu 

oFilemon, 

wywoływany 

jest 

automatycznie 

destruktor 

klasy. 

Destruktor  kasuje  wskaźniki.  Jeśli 
wskaźniki  wskazywałyby  na  obiekty 
innej  klasy,  to  zostałyby  wywołane 
destruktory tych klas.

background image

Wskaźnik this

Każda wewnętrzna funkcja klasy ma ukryty parametr: wskaźnik this
this zawsze wskazuje na aktualny obiekt. 
Przy  każdym  wywołaniu  metod  PobierzWiek  ()  albo  UstawWiek  (),  wskaźnik 
this jest dołączany jako ukryty parametr.

Zadaniem  wskaźnika  this  jest  wskazywanie  na  obiekt,  którego  metoda 
została wywołana. Zazwyczaj nie będziemy go potrzebowali, będzie się tylko 
wywoływać  metody  i  zmieniać  zmienne  wewnętrzne.  Jednak  czasami  trzeba 
zagwarantować dostęp do obiektu (np. zwrócić adres aktualnego obiektu). W 
takiej sytuacji this będzie bardzo pomocny.
W normalnej sytuacji, aby dostać się do elementów klasy, nie potrzebuje się 
wskaźnika this. Można jednak bezpośrednio odwołać się do this

background image

Przykład 

Funkcje  dostępu  PobierzDlugosc  ()  i  UstawDlugosc  () 
bezpośrednio wykorzystują wskaźnik this do odczytania 
i  modyfikacji  zmiennych  wewnętrznych  obiektu 
Prostokąt.  PobierzSzerokosc()  i  UstawSzerokosc  () 
dokonują  odczytania  i  modyfikacji  klasyczną  metodą. 
Efekt  jest  taki  sam  w  obu  sytuacjach,  jednak  metoda 
klasyczna jest bardziej przejrzysta.

background image

Wskaźniki const

Umieszczenie go przed nazwą typu, na samym początku deklaracji wskaźnika 
powoduje  że  wskaźnik  jest  stały  i  nie  może  ulec  zmianie.  Słowo  const  po 
gwiazdce  i  przed  nazwą  typu  obiektu  powoduje,  że  obiekt  nie  może  być 
zmieniony z wykorzystaniem deklarowanego wskaźnika. 

Przykład

const int *pJeden;
int *const pDwa;
const int *const pTrzy;

pJeden jest wskaźnikiem na stałą typu int. Wartość ta nie może zostać 
zmieniona z wykorzystaniem tego wskaźnika. Oznacza to, że nie możesz 
napisać np. tak:

*pJeden = 5;

pDwa jest stałym wskaźnikiem na int. Wartość, na którą wskazuje może 
się zmienić, ale wskaźnik nie. Oznacza to, że nie wolno napisać tak:

pDwa = &x

pTrzy jest stałym wskaźnikiem na 
stałą 
int. Zarówno wartość jak i 
wskaźnik nie mogą się zmienić.


Document Outline