Rozdział 8 Wskaźniki


Rozdział 8.
Wskazniki
Jedną z najbardziej przydatnych dla programisty C++ rzeczy jest możliwość bezpośredniego
manipulowania pamięcią za pomocą wskazników.
Z tego rozdziału dowiesz się:
" czym są wskazniki,
" jak deklarować wskazniki i używać ich,
" czym jest sterta i w jaki sposób można manipulować pamięcią.
Wskazniki stanowią podwójne wyzwanie dla osoby uczącej się języka C++: po pierwsze, mogą
być niezrozumiałe, a po drugie, na początku może nie być jasne, do czego mogą się przydać. W
tym rozdziale krok po kroku wyjaśnimy działanie wskazników. Aby w pełni zrozumieć potrzebę
ich używania, musisz zapoznać się z zawartością kolejnych rozdziałów.
Czym jest wskaznik?
Wskaznik (ang. pointer) jest zmienną, przechowującą adres pamięci. To wszystko. Jeśli rozumiesz
to proste stwierdzenie, wiesz już wszystko o wskaznikach. Jeszcze raz: wskaznik jest zmienną
przechowującą adres pamięci.
Kilka słów na temat pamięci
Aby zrozumieć, do czego służą wskazniki, musisz wiedzieć kilka rzeczy o pamięci komputera.
Pamięć jest podzielona na kolejno numerowane lokalizacje. Każda zmienna umieszczona jest w
danym miejscu pamięci, jednoznacznie określonym przez tzw. adres pamięci. Rysunek 8.1
przedstawia schemat miejsca przechowywania zmiennej typu unsigned long o nazwie theAge
(wiek).
Rys. 8.1. Schemat przechowywania zmiennej theAge
Użycie operatora adresu (&)
W każdym komputerze pamięć jest adresowana w inny sposób, za pomocą różnych, złożonych
schematów. Zwykle programista nie musi znać konkretnego adresu danej zmiennej, tymi
szczegółami zajmuje się kompilator. Jeśli jednak chcesz uzyskać tę informację, możesz użyć
operatora adresu (&), który zwraca adres obiektu znajdującego się w pamięci. Jego wykorzystanie
przedstawiono na listingu 8.1.
Listing 8.1. Przykład użycia operatora adresu
0: // Listing 8.1 Demonstruje operator adresu
1: // oraz adresy zmiennych lokalnych
2:
3: #include
4:
5: int main()
6: {
7: using namespace std;
8: unsigned short shortVar=5;
9: unsigned long longVar=65535;
10: long sVar = -65535;
11:
12: cout << "shortVar:\t" << shortVar;
13: cout << "\tAdres zmiennej shortVar:\t";
14: cout << &shortVar << "\n";
15:
16: cout << "longVar:\t" << longVar;
17: cout << "\tAdres zmiennej longVar:\t" ;
18: cout << &longVar << "\n";
19:
20: cout << "sVar:\t\t" << sVar;
21: cout << "\tAdres zmiennej sVar:\t" ;
22: cout << &sVar << "\n";
23:
24: return 0;
25: }
Wynik
shortVar: 5 Adres zmiennej shortVar:
0012FF7C
longVar: 65535 Adres zmiennej longVar:
0012FF78
sVar: -65535 Adres zmiennej sVar:
0012FF74
Analiza
Tworzone i inicjalizowane są trzy zmienne: typu unsigned short w linii 8., typu unsigned
long w linii 9. oraz long w linii 10. Ich wartości i adresy są wypisywane w liniach od 12 do 16.
Adresy zmiennych uzyskiwane są za pomocą operatora adresu (&).
Wartością zmiennej shortVar (krótka zmienna) jest 5 (tak jak można było oczekiwać). W moim
komputerze Pentium (32-bitowym) ta zmienna ma adres 0012FF7C. Adres zależy od komputera i
może być nieco inny przy każdym uruchomieniu programu. W twoim komputerze adresy tych
zmiennych także mogą się różnić.
Deklarując typ zmiennej, informujesz kompilator, ile miejsca w pamięci powinien dla niej
zarezerwować, jednak adres jest przydzielany zmiennej automatycznie. Na przykład długie (long)
zmienne całkowite zajmują zwykle cztery bajty, co oznacza, że zmienna posiada adres dla czterech
bajtów pamięci.
Zwróć uwagę, że twój kompilator, podobnie jak mój, może nalegać na to, by zmienne
otrzymywały adresy będące wielokrotnością 4 (tj. zmienna longVar otrzymuje adres położony
cztery bajty za zmienną shortVar, mimo iż zmienna shortVar potrzebuje tylko dwóch bajtów!)
Przechowywanie adresu we wskazniku
Każda zmienna posiada adres. Możesz umieścić go we wskazniku nawet bez znajomości adresu
danej zmiennej.
Przypuśćmy na przykład, że zmienna howOld jest całkowita. Aby zadeklarować wskaznik o
nazwie pAge, mogący zawierać adres tej zmiennej, możesz napisać:
int *pAge = 0;
Spowoduje to zadeklarowanie zmiennej pAge jako wskaznika do typu int. Innymi słowy,
zmienna pAge jest zadeklarowana jako przechowująca adresy wartości całkowitych.
Zwróć uwagę, że pAge jest zmienną. Gdy deklarujesz zmienną całkowitą (typu int), kompilator
rezerwuje tyle pamięci, ile jest potrzebne do przechowania wartości całkowitej. Gdy deklarujesz
zmienną wskaznikową taką jak pAge, kompilator rezerwuje ilość pamięci wystarczającą do
przechowania adresu (w większości komputerów zajmuje on cztery bajty). pAge jest po prostu
kolejnym typem zmiennej.
Puste i błędne wskazniki
W tym przykładzie wskaznik pAge jest inicjalizowany wartością zero. Wskaznik, którego
wartością jest zero, jest nazywany wskaznikiem pustym (ang. null pointer). Podczas tworzenia
wskazników, powinny być one zainicjalizowane jakąś wartością. Jeśli nie wiesz, jaką wartość
przypisać wskaznikowi, przypisz mu wartość 0. Wskaznik, który nie jest zainicjalizowany, jest
nazywany wskaznikiem błędnym (ang. wild pointer). Błędne wskazniki są bardzo niebezpieczne.
UWAGA Pamiętaj o zasadzie bezpiecznego programowania: inicjalizuj swoje wskazniki!
Musisz jawnie przypisać wskaznikowi adres zmiennej howOld. Poniższy przykład pokazuje, jak to
zrobić:
unsigned short int howOld = 50; // tworzymy zmienną
unsigned short int * pAge = 0; // tworzymy wskaznik
pAge = &howOld; // umieszczamy adres zmiennej hOld w zmiennej pAge
Pierwsza linia tworzy zmienną  howOld typu unsigned short int  oraz inicjalizuje ją
wartością 50. Druga linia deklaruje zmienną pAge jako wskaznik do typu unsigned short int
i ustawia ją na zero. To, że zmienna pAge jest wskaznikiem, można poznać po gwiazdce (*)
umieszczonej pomiędzy typem zmiennej a jej nazwą.
Trzecia, ostatnia linia, przypisuje wskaznikowi pAge adres zmiennej howOld. Przypisywanie
adresu można poznać po użyciu operatora adresu (&). Gdyby operator adresu został pominięty,
wskaznikowi pAge zostałaby przypisana wartość zmiennej howOld. Oczywiście, wartość ta
mogłaby być poprawnym adresem.
Teraz wskaznik pAge zawiera adres zmiennej howOld. Ta zmienna ma wartość 50. Można
uzyskać ten rezultat wykonując o jeden krok mniej, na przykład:
unsigned short int howOld = 50; // tworzymy zmienną
unsigned short int * pAge = &howOld; // tworzymy wskaznik do howOld
pAge jest wskaznikiem, zawierającym teraz adres zmiennej howOld. Używając wskaznika pAge,
możesz sprawdzić wartość zmiennej howOld, która w tym przypadku wynosi 50. Dostęp do
zmiennej howOld poprzez wskaznik pAge jest nazywany dostępem pośrednim (dostęp do niej
rzeczywiście odbywa się poprzez ten wskaznik). Z dalszej części rozdziału dowiesz się, jak w ten
sposób odwoływać się do wartości zmiennej.
Dostęp pośredni oznacza dostęp do zmiennej o adresie przechowywanym we wskazniku. Użycie
wskazników stanowi pośredni sposób uzyskania wartości przechowywanej pod danym adresem.
UWAGA W przypadku zwykłej zmiennej, jej typ informuje kompilator, ile potrzebuje pamięci do
przechowania jej wartości. W przypadku wskazników sytuacja wygląda inaczej: każdy wskaznik
zajmuje cztery bajty. Typ wskaznika informuje kompilator, ile potrzeba miejsca w pamięci do
przechowania obiektu, którego adres zawiera wskaznik!
W deklaracji
unsigned shot int * pAge = 0; // tworzymy wskaznik
zmienna pAge jest zadeklarowana jako wskaznik do typu unsigned short int. Mówi ona
kompilatorowi, że wskaznik ten (który do przechowania adresu wymaga czterech bajtów) będzie
przechowywał adres obiektu typu unsigned short int, który zajmuje dwa bajty.
Nazwy wskazników
Podobnie jak inne zmienne, wskazniki mogą mieć dowolne nazwy. Wielu programistów
przestrzega konwencji, w której nazwy wszystkich wskazników poprzedza się literką p
(pointer), np. pAge czy pNumber.
Operator wyłuskania
Operator wyłuskania (*) jest zwany także operatorem dostępu pośredniego albo dereferencją.
Podczas wyłuskiwania wskaznika otrzymywana jest wartość wskazywana przez adres zawarty w
tym wskazniku.
Zwykłe zmienne zapewniają bezpośredni dostęp do swoich wartości. Gdy tworzysz nową zmienną
typu unsigned short int o nazwie yourAge i chcesz jej przypisać wartość zmiennej howOld,
możesz napisać:
unsigned short int yourAge;
yourAge = howOld;
Wskaznik umożliwia pośredni dostęp do wartości zmiennej, której adres zawiera. Aby przypisać
wartość zmiennej howOld do zmiennej yourAge za pomocą wskaznika pAge, powinieneś
napisać:
unsigned short int yourAge;
yourAge = *pAge;
Operator wyłuskania (*) znajdujący się przed zmienną pAge oznacza  wartość przechowywana
pod adresem. To przypisanie można potraktować jako:  Wez wartość przechowywaną pod
adresem zawartym w pAge i przypisz ją do zmiennej yourAge .
UWAGA W przypadku wskazników gwiazdka (*) może posiadać dwa znaczenia (może
symbolizować część deklaracji wskaznika albo operator wyłuskania).
Gdy deklarujesz wskaznik, * jest częścią deklaracji i następuje po typie wskazywanego obiektu.
Na przykład:
// tworzymy wskaznik do typu unsigned short
unsigned short * page = 0;
Gdy wskaznik jest wyłuskiwany, operator wyłuskiwania wskazuje, że odwołujemy się do
wartości, znajdującej się w miejscu pamięci określonym przez adres zawarty we wskazniku, a
nie do tego adresu.
// wartości wskazywanej przez pAge przypisujemy wartość 5
*pAge = 5;
Zwróć także uwagę, że ten sam znak (*) jest używany jako operator mnożenia. Kompilator wie
z kontekstu, o który operator chodzi w danym miejscu programu.
Wskazniki, adresy i zmienne
Należy dokonać rozróżnienia pomiędzy wskaznikiem, adresem zawartym w tym wskazniku, a
zmienną o adresie zawartym w tym wskazniku. Nieumiejętność rozróżnienia ich jest najczęstszym
powodem nieporozumień ze wskaznikami.
Wezmy następujący fragment kodu:
int theVariable = 5;
int * pPointer = &theVariable;
Zmienna theVariable jest zadeklarowana jako zmienna typu int i jest inicjalizowana wartością
5. Zmienna pPointer jest zadeklarowana jako wskaznik do typu int i jest inicjalizowana
adresem zmiennej theVariable. pPointer jest wskaznikiem. Adres zawarty w pPointer jest
adresem zmiennej theVariable. Wartością znajdującą się pod adresem zawartym w pPointer
jest 5. Schemat zmiennych theVariable i pPointer przedstawia rysunek 8.2.
Rys. 8.2. Schematyczna reprezentacja pamięci
Na tym rysunku wartość 5 została umieszczona pod adresem 101. Jest on podany jako liczba
dwójkowa
0000 0000 0000 0101
Jest to dwubajtowa (16-bitowa) wartość, której wartością dziesiętną jest 5.
Zmienna wskaznikowa ma adres 106. Jej wartość to
0000 0000 0000 0000 0000 0000 0110 0101
Jest to binarna reprezentacja wartości 101 (dziesiętnie), stanowiącej adres zmiennej
theVariable, która zawiera wartość 5.
Przedstawiony powyżej układ pamięci jest uproszczony, ale ilustruje przeznaczenie wskazników
zawierających adresy pamięci.
Operowanie danymi poprzez wskazniki
Gdy przypiszesz wskaznikowi adres zmiennej, możesz użyć tego wskaznika w celu uzyskania
dostępu do danych zawartych w tej zmiennej. Listing 8.2 pokazuje, w jaki sposób adres lokalnej
zmiennej jest przypisywany wskaznikowi i w jaki sposób ten wskaznik może operować wartością
w tej zmiennej.
Listing 8.2. Operowanie danymi poprzez wskaznik
0: // Listing 8.2 Użycie wskaznika
1:
2: #include
3:
4: typedef unsigned short int USHORT;
5:
6: int main()
7: {
8:
9: using std::cout;
10:
11: USHORT myAge; // zmienna
12: USHORT * pAge = 0; // wskaznik
13:
14: myAge = 5;
15:
16: cout << "myAge: " << myAge << "\n";
17: pAge = &myAge; // wskaznikowi pAge przypisuje adres
zmiennej myAge
18: cout << "*pAge: " << *pAge << "\n\n";
19:
20: cout << "Ustawiam *pAge = 7...\n";
21: *pAge = 7; // ustawia myAge na 7
22:
23: cout << "*pAge: " << *pAge << "\n";
24: cout << "myAge: " << myAge << "\n\n";
25:
26: cout << "Ustawiam myAge = 9...\n";
27: myAge = 9;
28:
29: cout << "myAge: " << myAge << "\n";
30: cout << "*pAge: " << *pAge << "\n";
31:
32: return 0;
33: }
Wynik
myAge: 5
*pAge: 5
Ustawiam *pAge = 7...
*pAge: 7
myAge: 7
Ustawiam myAge = 9...
myAge: 9
*pAge: 9
Analiza
Program deklaruje dwie zmienne: myAge typu unsigned short oraz wskaznik do typu
unsigned short, zmienną pAge. W linii 14. zmiennej myAge jest przypisywana wartość 5;
potwierdza to komunikat wypisywany w linii 16.
W linii 17. wskaznikowi pAge jest przypisywany adres zmiennej myAge. W linii 18 następuje
wyłuskanie wskaznika pAge i wypisanie otrzymanej wartości (to pokazuje, że wartość o adresie
zawartym w pAge jest wartością 5, czyli wartością zmiennej myAge). W linii 21. zmiennej o
adresie zawartym w pAge jest przypisywana wartość 7. Powoduje to przypisanie tej wartości
zmiennej myAge, co potwierdzają komunikaty wypisywane w liniach 23. i 24.
W linii 29. zmiennej myAge jest przypisywana wartość 9. Ta wartość jest pobierana bezpośrednio
w linii 29., zaś w linii 30. pośrednio (poprzez wyłuskanie wskaznika pAge).
Sprawdzanie adresu
Wskazniki umożliwiają operowanie adresami nawet bez znajomości ich faktycznych wartości. Do
tej pory musiałeś przyjmować jako oczywiste, że gdy przypisujesz wskaznikowi adres zmiennej,
to wartością wskaznika staje się rzeczywiście adres tej zmiennej. Dlaczego nie miałbyś się teraz co
do tego upewnić? Przedstawia to listing 8.3.
Listing 8.3. Sprawdzanie zawartości wskaznika
0: // Listing 8.3 Co zawiera wskaznik?.
1:
2: #include
3:
4:
5: int main()
6: {
7: using std::cout;
8:
9: unsigned short int myAge = 5, yourAge = 10;
10:
11: // wskaznik
12: unsigned short int * pAge = &myAge;
13:
14: cout << "myAge:\t" << myAge
15: << "\t\tyourAge:\t" << yourAge << "\n";
16:
17: cout << "&myAge:\t" << &myAge
18: << "\t&yourAge:\t" << &yourAge <<"\n";
19:
20: cout << "pAge:\t" << pAge << "\n";
21: cout << "*pAge:\t" << *pAge << "\n";
22:
23:
24: cout << "\nPonowne przypisanie: pAge = &yourAge...\n\n";
25: pAge = &yourAge; // ponowne przypisanie do wskaznika
26:
27: cout << "myAge:\t" << myAge <<
28: "\t\tyourAge:\t" << yourAge << "\n";
29:
30: cout << "&myAge:\t" << &myAge
31: << "\t&yourAge:\t" << &yourAge <<"\n";
32:
33: cout << "pAge:\t" << pAge << "\n";
34: cout << "*pAge:\t" << *pAge << "\n";
35:
36: cout << "\n&pAge:\t" << &pAge << "\n";
37:
38: return 0;
39: }
Wynik
myAge: 5 yourAge: 10
&myAge: 0012FF7C &yourAge: 0012FF78
pAge: 0012FF7C
*pAge: 5
Ponowne przypisanie: pAge = &yourAge...
myAge: 5 yourAge: 10
&myAge: 0012FF7C &yourAge: 0012FF78
pAge: 0012FF78
*pAge: 10
&pAge: 0012FF74
(Twoje wyniki mogą być inne.)
Analiza
W linii 9. deklarowane są dwie zmienne, myAge oraz yourAge, obie typu unsigned short. W
linii 12. deklarowana jest zmienna pAge, będąca wskaznikiem do typu unsigned short; ten
wskaznik jest inicjalizowany adresem zmiennej myAge.
W liniach od 14. do 18. następuje wypisanie wartości i adresów zmiennych myAge i yourAge.
Linia 20. wypisuje zawartość wskaznika pAge, którą jest adres zmiennej myAge. Linia 21.
wypisuje rezultat wyłuskania wskaznika pAge, czyli wypisuje wartość zmiennej wskazywanej
przez ten wskaznik (wartość zmiennej myAge, wynoszącą 5).
W taki właśnie sposób działają wskazniki. Linia 20. pokazuje, że wskaznik pAge zawiera adres
zmiennej myAge, zaś linia 21. pokazuje, w jaki sposób wartość przechowywana w zmiennej
myAge może zostać uzyskana w wyniku wyłuskania wskaznika pAge. Zanim przejdziesz dalej,
upewnij się, czy to rozumiesz. Przestudiuj kod i porównaj z wynikiem.
W linii 25. zmiennej pAge jest przypisywany nowy adres, tym razem adres zmiennej yourAge.
Ponownie wypisywane są wartości i adresy. Wyniki pokazują, że wskaznik pAge zawiera teraz
adres zmiennej yourAge i że jako efekt wyłuskania uzyskujemy wartość przechowywaną w tej
zmiennej.
Linia 36. wypisuje adres zmiennej pAge. Tak jak wszystkie inne zmienne, swój adres posiada
także wskaznik. Adres ten także może być umieszczony we wskazniku. (Przypisywanie adresu
wskaznika do innego wskaznika zostanie omówione wkrótce.)
TAK
W celu uzyskania dostępu do danych przechowywanych pod adresem zawartym we wskazniku
używaj operatora wyłuskania (*).
Inicjalizuj wszystkie wskazniki albo adresem poprawnym, albo adresem pustym (0).
Pamiętaj o różnicy pomiędzy adresem we wskazniku, a wartością pod tym adresem.
Użycie wskazników
Aby zadeklarować wskaznik, napisz typ zmiennej lub obiektu, którego adres będzie
przechowywany w tym wskazniku, gwiazdkę (*) oraz nazwę wskaznika. Na przykład:
unsigned short int * pPointer = 0;
Aby zainicjalizować wskaznik (przypisać mu adres), poprzedz nazwę zmiennej, której adres
chcesz przypisać, operatorem adresu (&). Na przykład;
unsigned short int theVariable = 5;
unsigned short int * pPointer = & theVariable;
Aby wyłuskać wskaznik, poprzedz nazwę wskaznika operatorem wyłuskania (*). Na przykład:
unsigned short int theValue = *pPointer;
Do czego służą wskazniki?
Jak dotąd, poznałeś krok po kroku proces przypisywania wskaznikowi adresu zmiennej. W
praktyce jednak nie będziesz tego robił nigdy. Po co miałbyś utrudniać sobie życie używaniem
wskazników, skoro masz do dyspozycji zmienną, do której masz pełny dostęp? Jedynym
powodem, dla którego operujemy wskaznikami na zmiennych automatycznych (tj. lokalnych), jest
zademonstrowanie sposobu działania wskazników. Teraz, gdy poznałeś już składnię wskazników,
możesz poznać ich praktyczne zastosowania. Wskazniki są najczęściej używane do wykonywania
trzech zadań:
" zarządzania danymi na stercie,
" uzyskiwania dostępu do danych i funkcji składowych klasy.
" przekazywania zmiennych do funkcji poprzez referencję.
W pozostałej części rozdziału skupimy się na zarządzaniu danymi na stercie oraz dostępie do
danych i funkcji składowych klasy. Przekazywanie zmiennych przez referencję omówimy w
następnym rozdziale.
Stos i sterta
W rozdziale 5., w podrozdziale  Jak działają funkcje  rzut oka <> wspomniano o
pięciu obszarach pamięci:
" globalnej przestrzeni nazw,
" stercie,
" rejestrach,
" przestrzeni kodu,
" stosie.
Zmienne lokalne znajdują się na stosie (podobnie jak parametry funkcji). Kod występuje,
oczywiście, w przestrzeni kodu, zaś zmienne globalne w globalnej przestrzeni nazw. Rejestry są
używane do kontrolowania wewnętrznych zadań procesora, takich jak śledzenie szczytu stosu czy
miejsca wykonania programu. Cała pozostała pamięć jest prawie w całości przeznaczona na tak
zwaną stertę (ang. heap).
Problem ze zmiennymi lokalnymi polega na tym, że nie są one trwałe: gdy funkcja kończy
działanie, zmienne te są niszczone. Rozwiązują ten problem zmienne globalne, nie są one jednak
dostępne bez ograniczeń w całym programie; powoduje to, kod jest trudny do zrozumienia i
zmodyfikowania. Umieszczanie danych na stercie uwalnia od obu tych niedogodności.
Możesz uznawać stertę za obszerny blok pamięci, zawierający dziesiątki tysięcy kolejno
ponumerowanych pojemników, oczekujących na twoje dane. Jednak w odróżnieniu od stosu, nie
możesz nadawać tym pojemnikom etykietek. Musisz poprosić o adres pojemnika, który
rezerwujesz, a następnie przechować ten adres we wskazniku.
Można także znalezć inną analogię: wyobraz sobie, że przyjaciel dał ci numer telefonu do firmy
kurierskiej. Wracasz do domu, programujesz ten numer w swoim aparacie telefonicznym pod
określonym przyciskiem, po czym wyrzucasz kartkę z numerem. Gdy naciśniesz przycisk, telefon
wybierze jakiś numer i połączy cię z firmą kurierską. Nie pamiętasz numeru i nie wiesz, gdzie
znajduje się firma, ale przycisk umożliwia ci dostęp do niej. Firma to twoje dane na stercie. Nie
wiesz gdzie jest, ale wiesz jak się z nią skontaktować. Służy do tego jej adres  w tym przypadku
jest nim numer telefonu. Nie musisz znać tego numeru; wystarczy, że masz go we wskazniku
(przycisku w telefonie). Wskaznik umożliwia ci dostęp do danych, bez konieczności
przeprowadzania szczegółowych, dodatkowych działań.
Gdy funkcja kończy działanie, stos jest czyszczony automatycznie. Wszystkie zmienne lokalne
wychodzą poza zakres i są usuwane ze stosu. Sterta nie jest czyszczona aż do chwili zakończenia
działania programu, dlatego to ty jesteś odpowiedzialny za zwolnienie wszelkiej zaalokowanej
przez siebie pamięci.
Zaletą sterty jest to, że zaalokowana (zarezerwowana) na niej pamięć pozostaje dostępna aż do
momentu, w którym ją zwolnisz. Jeśli pamięć na stercie zaalokujesz w funkcji, po wyjściu z tej
funkcji pamięć pozostanie nadal dostępna.
Zaletą tej metody korzystania z pamięci (w przeciwieństwie do zmiennych globalnych) jest to, że
dostęp do tej pamięci mają tylko te funkcje, które posiadają do niej wskaznik. Dzięki temu można
ściśle kontrolować interfejs do danych  eliminuje to potencjalny problem nieoczekiwanej i
niezauważalnej zmiany danych przez inną funkcję.
Aby ten mechanizm działał, musisz mieć możliwość tworzenia wskaznika do obszaru pamięci na
stercie oraz przekazywania tego wskaznika pomiędzy funkcjami. Proces ten opisują następne
podrozdziały.
Operator new
W języku C++, do alokowania pamięci na stercie służy słowo kluczowe new (nowy). Po tym
słowie kluczowym następuje typ obiektu, jaki chcesz zaalokować  dzięki temu kompilator wie,
ile miejsca powinien zarezerwować. Instrukcja new unsigned short int alokuje na stercie
dwa bajty, a instrukcja new long alokuje cztery bajty.
Zwracaną wartością jest adres pamięci. Musi on zostać przypisany do wskaznika. Aby stworzyć na
stercie obiekt typu unsigned short, możesz napisać:
unsigned short int * pPointer;
pPointer = new unsigned short int;
Można oczywiście zainicjalizować wskaznik w trakcie jego tworzenia:
unsigned short int * pPointer = new unsigned short int;
W obu przypadkach, wskaznik pPointer wskazuje teraz położony na stercie obiekt typu
unsigned short int. Możesz użyć tego wskaznika tak, jak każdego innego wskaznika do
zmiennej i przypisać obiektowi na stercie dowolną wartość:
*pPointer = 72;
Oznacza to:  Umieść 72 jako wartość obiektu wskazywanego przez pPointer lub  Przypisz
obszarowi sterty wskazywanemu przez wskaznik pPointer wartość 72 .
UWAGA Gdy operator new nie jest w stanie zarezerwować pamięci na stercie (w końcu pamięć
ma ograniczoną objętość), zgłasza wyjątek (patrz rozdział 20.,  Wyjątki i obsługa błędów ).
delete
Gdy skończysz korzystać z obszaru pamięci na stercie, musisz użyć słowa kluczowego delete
(usuń) z właściwym wskaznikiem. Instrukcja delete zwalnia pamięć zaalokowaną na stercie, tj.
zwraca ją stercie. Pamiętaj, że sam wskaznik  w przeciwieństwie do pamięci, na którą wskazuje
 jest zmienną lokalną. Gdy funkcja, w której został zadeklarowany, kończy działanie, wskaznik
wychodzi poza zakres i jest niszczony. Pamięć zaalokowana operatorem new nie jest zwalniana
automatycznie; staje się niedostępna  taka sytuacja jest nazywana wyciekiem pamięci (ang.
memory leak). Nazwa wzięła się stąd, że pamięć nie może być odzyskana, aż do momentu
zakończenia działania programu (z punktu widzenia programu, pamięć  wycieka z komputera).
Aby zwrócić stercie pamięć, użyj słowa kluczowego delete. Na przykład:
delete pPointer;
Gdy zwalniasz wskaznik, w rzeczywistości zwalniasz jedynie pamięć, której adres jest zawarty w
tym wskazniku. Mówisz:  Zwróć stercie pamięć, na którą wskazuje ten wskaznik . Wskaznik
nadal pozostaje wskaznikiem i można mu ponownie przypisać adres. Listing 8.4 przedstawia
alokowanie zmiennej na stercie, użycie tej zmiennej, a następnie zwolnienie jej.
OSTRZEŻENIE Gdy używasz słowa kluczowego delete dla wskaznika, zwalniana jest
pamięć, na którą on wskazuje. Ponowne wywołanie delete dla tego wskaznika spowoduje
załamanie programu! Gdy zwalniasz wskaznik, ustaw go na zero (null, wskaznik pusty).
Kompilator gwarantuje, że wywołanie delete z pustym wskaznikiem jest bezpieczne. Na
przykład:
Animal *pDog = new Animal;
delete pDog; // zwalnia pamięć
pDog = 0; // ustawia wskaznik na null
// ...
delete pDog; // nieszkodliwe
Listing 8.4. Alokowanie, użycie i zwolnienie wskaznika
0: // Listing 8.4
1: // Alokowanie i zwalnianie wskaznika
2:
3: #include
4: int main()
5: {
6: using std::cout;
7: int localVariable = 5;
8: int * pLocal= &localVariable;
9: int * pHeap = new int;
10: *pHeap = 7;
11: cout << "localVariable: " << localVariable << "\n";
12: cout << "*pLocal: " << *pLocal << "\n";
13: cout << "*pHeap: " << *pHeap << "\n";
14: delete pHeap;
15: pHeap = new int;
16: *pHeap = 9;
17: cout << "*pHeap: " << *pHeap << "\n";
18: delete pHeap;
19: return 0;
20: }
Wynik
localVariable: 5
*pLocal: 5
*pHeap: 7
*pHeap: 9
Analiza
W linii 7. program deklaruje i inicjalizuje lokalną zmienną. W linii 8. deklaruje i inicjalizuje
wskaznik, przypisując mu adres tej zmiennej. W linii 9. deklaruje wskaznik, lecz inicjalizuje go
wartością uzyskaną w wyniku wywołania operatora new int. Powoduje to zaalokowanie na
stercie miejsca dla wartości typu int.
Linia 10. przypisuje wartość 7 do nowo zaalokowanej pamięci. Linia 11. wypisuje wartość
zmiennej lokalnej, a linia 12. wypisuje wartość wskazywaną przez pLocal (lokalna) Jak należało
oczekiwać, są one takie same. Linia 13. wypisuje wartość wskazywaną przez pHeap (sterta) i
pokazuje, że rzeczywiście mamy dostęp do wartości zaalokowanej w linii 10.
W linii 14. pamięć zaalokowana w linii 9. jest zwracana na stertę (w wyniku wywołania delete).
Czynność ta zwalnia pamięć i odłącza od niej wskaznik. Teraz pHeap może wskazywać inne
miejsce w pamięci. Nowy adres i wartość przypisujemy mu w liniach 15. i 16., zaś w linii 17.
wypisujemy wynik. Linia 18. zwalnia pamięć i zwraca ją stercie.
Choć linia 18. jest nadmiarowa (zakończenie programu automatycznie powoduje zwolnienie
pamięci), do dobrych obyczajów należy jawne zwalnianie wskazników. Gdy program będzie
modyfikowany lub rozbudowywany, zapamiętanie tego kroku może okazać się bardzo przydatne.
Wycieki pamięci
Inną sytuacją, która może doprowadzić do wycieku pamięci, jest ponowne przypisanie
wskaznikowi adresu, bez wcześniejszego zwolnienia pamięci, na którą w danym momencie
wskazuje. Spójrzmy na poniższy fragment kodu:
0: unsigned short int * pPointer = new unsigned short int;
1: *pPointer = 72;
2: pPointer = new unsigned short int;
3: pPointer = 84;
Linia 0 tworzy pPointer i przypisuje mu adres rezerwowanego na stercie obszaru. Linia 1.
umieszcza w tym obszarze wartość 72. Linia 2. ponownie przypisuje wskaznikowi pPointer
adres innego obszaru pamięci. Linia 3. umieszcza w tym obszarze wartość 84. Pierwotny obszar
 w którym jest zawarta wartość 72  jest niedostępny, gdyż wskaznik do tej pamięci został
wypełniony innym adresem. Nie ma sposobu na odzyskanie pierwotnego obszaru, nie ma też
sposobu na zwolnienie go przed zakończeniem działania programu.
Ten kod powinien zostać napisany następująco:
0: unsigned short int * pPointer = new unsigned short int;
1: *pPointer = 72;
2: delete pPointer;
3: pPointer = new unsigned short int;
4: pPointer = 84;
Teraz pamięć, wskazywana pierwotnie przez pPointer, jest zwalniana w linii 2.
UWAGA Za każdym razem, gdy użyjesz w programie słowa kluczowego new, powinieneś użyć
także odpowiadającego mu słowa kluczowego delete. Należy pamiętać, na co wskazuje dany
wskaznik (aby mieć pewność, że zostanie to zwolnione, gdy przestanie potrzebne).
Tworzenie obiektów na stercie
Możesz stworzyć nie tylko wskaznik do zmiennej całkowitej, ale i wskaznik do dowolnego
obiektu. Jeśli zadeklarowałeś obiekt typu Cat (kot), możesz zadeklarować wskaznik do tej klasy i
stworzyć na stercie egzemplarz obiektu tej klasy (tak jak mogłeś stworzyć go na stosie). Składnia
jest taka sama, jak w przypadku innych zmiennych:
Cat *pCat = new Cat;
Powoduje to wywołanie domyślnego konstruktora klasy  czyli konstruktora, który nie ma
parametrów. Konstruktor jest wywoływany za każdym razem, gdy tworzony jest obiekt klasy (na
stosie lub na stercie).
Usuwanie obiektów
Gdy wywołujesz delete ze wskaznikiem do obiektu na stercie, przed zwolnieniem pamięci
obiektu wywoływany jest jego destruktor. Dzięki temu klasa ma szansę  posprzątania po sobie,
tak jak w przypadku obiektów niszczonych na stosie. Tworzenie i usuwanie obiektów na stercie
przedstawia listing 8.5.
Listing 8.5. Tworzenie i usuwanie obiektów na stercie
0: // Listing 8.5
1: // Tworzenie obiektów na stercie
2: // z użyciem new oraz delete
3:
4: #include
5:
6: class SimpleCat
7: {
8: public:
9: SimpleCat();
10: ~SimpleCat();
11: private:
12: int itsAge;
13: };
14:
15: SimpleCat::SimpleCat()
16: {
17: std::cout << "Wywolano konstruktor.\n";
18: itsAge = 1;
19: }
20:
21: SimpleCat::~SimpleCat()
22: {
23: std::cout << "Wywolano destruktor.\n";
24: }
25:
26: int main()
27: {
28: std::cout << "SimpleCat Mruczek...\n";
29: SimpleCat Mruczek;
30: std::cout << "SimpleCat *pFilemon = new SimpleCat...\n";
31: SimpleCat * pFilemon = new SimpleCat;
32: std::cout << "delete pFilemon...\n";
33: delete pFilemon;
34: std::cout << "Wyjscie, czekaj na Mruczka...\n";
35: return 0;
36: }
Wynik
SimpleCat Mruczek...
Wywolano konstruktor.
SimpleCat *pFilemon = new SimpleCat...
Wywolano konstruktor.
delete pFilemon...
Wywolano destruktor.
Wyjscie, czekaj na Mruczka...
Wywolano destruktor.
Analiza
Linie od 6. do 13. deklarują okrojoną klasę SimpleCat (prosty kot). Linia 9. deklaruje
konstruktor tej klasy, zaś linie od 15. do 19. zawierają jego definicję. Linia 10 deklaruje destruktor
klasy, a linie od 21. do 24. zawierają jego definicję.
W linii 29. na stosie tworzony jest obiekt Mruczek, powoduje to wywołanie konstruktora klasy.
W linii 31. na stercie tworzony jest egzemplarz obiektu SimpleCat, wskazywany przez zmienną
pFilemon; w wyniku tego działania następuje ponowne wywołanie konstruktora. W linii 33.
znajduje się słowo kluczowe delete ze wskaznikiem pFilemon, dlatego wywoływany jest
destruktor. Gdy funkcja main() kończy działanie, obiekt Mruczek wychodzi z zakresu i
ponownie wywoływany jest destruktor.
Dostęp do składowych klasy
W przypadku obiektów Cat stworzonych lokalnie, dostęp do składowych funkcji i danych odbywa
się za pomocą operatora kropki (.). Aby odwołać się do składowych utworzonego na stercie
obiektu Cat, musisz wyłuskać wskaznik i wywołać operator kropki dla obiektu wskazywanego
przez ten wskaznik. Aby odwołać się do funkcji składowej GetAge(), możesz napisać:
(*pFilemon).GetAge();
Aby zapewnić, wyłuskanie wskaznika pFilemon przed odwołaniem się do metody GetAge(),
użyte zostały nawiasy.
Ponieważ taki zapis jest dość skomplikowany, C++ oferuje skrótowy operator dostępu
pośredniego: operator wskazywania (->). Składa się on z ze znaku minus (-) i znaku większości
(>), zapisanych razem. Kompilator traktuje je jako pojedynczy symbol. Dostęp do składowych
funkcji i danych utworzonego na stercie obiektu przedstawia listing 8.6.
Listing 8.6. Dostęp do składowych funkcji i danych utworzonego na stercie obiektu.
0: // Listing 8.6
1: // Dostęp do składowych funkcji i danych obiektu
2: // utworzonego na stercie, z użyciem operatora ->
3:
4: #include
5:
6: class SimpleCat
7: {
8: public:
9: SimpleCat() {itsAge = 5; }
10: ~SimpleCat() {}
11: int GetAge() const { return itsAge; }
12: void SetAge(int age) { itsAge = age; }
13: private:
14: int itsAge;
15: };
16:
17: int main()
18: {
19: SimpleCat * Mruczek = new SimpleCat;
20: std::cout << "Mruczek ma " << Mruczek->GetAge() << " lat\n";
21: Mruczek->SetAge(7);
22: std::cout << "Mruczek ma " << Mruczek->GetAge() << " lat\n";
23: delete Mruczek;
24: return 0;
25: }
Wynik
Mruczek ma 5 lat
Mruczek ma 7 lat
Analiza
W linii 19. na stercie tworzony jest egzemplarz obiektu klasy SimpleCat. Domyślny konstruktor
ustawia jego zmienną składową itsAge (jego wiek) na 5, zaś w linii 20. wywoływana jest metoda
GetAge(). Ponieważ zmienna Mruczek jest wskaznikiem, w celu uzyskania dostępu do danych i
funkcji składowych został użyty operator wskazania (->). W linii 21. zostaje wywołana metoda
SetAge(), po czym w linii 22. ponownie wywoływana jest metoda GetAge().
Dane składowe na stercie
Wskaznikami do obiektów znajdujących się na stercie może być jedna lub więcej danych
składowych klasy. Pamięć może być alokowana w konstruktorze klasy lub w którejś z jej metod,
zaś do jej zwolnienia można wykorzystać destruktor. Przedstawia to listing 8.7.
Listing 8.7. Wskazniki jako dane składowe
0: // Listing 8.7
1: // Wskazniki jako dane składowe
2: // dostępne poprzez operator ->
3:
4: #include
5:
6: class SimpleCat
7: {
8: public:
9: SimpleCat();
10: ~SimpleCat();
11: int GetAge() const { return *itsAge; }
12: void SetAge(int age) { *itsAge = age; }
13:
14: int GetWeight() const { return *itsWeight; }
15: void setWeight (int weight) { *itsWeight = weight; }
16:
17: private:
18: int * itsAge;
19: int * itsWeight;
20: };
21:
22: SimpleCat::SimpleCat()
23: {
24: itsAge = new int(5);
25: itsWeight = new int(2);
26: }
27:
28: SimpleCat::~SimpleCat()
29: {
30: delete itsAge;
31: delete itsWeight;
32: }
33:
34: int main()
35: {
36: SimpleCat *Mruczek = new SimpleCat;
37: std::cout << "Mruczek ma " << Mruczek->GetAge() << " lat\n";
38: Mruczek->SetAge(7);
39: std::cout << "Mruczek ma " << Mruczek->GetAge() << " lat\n";
40: delete Mruczek;
41: return 0;
42: }
Wynik
Mruczek ma 5 lat
Mruczek ma 7 lat
Analiza
Klasa SimpleCat (prosty kot) deklaruje (w liniach 18. i 19.) dwie zmienne składowe; obie te
zmienne są wskaznikami do wartości całkowitych. Konstruktor (linie od 22. do 26.) inicjalizuje na
stercie pamięć dla tych wskazników i przypisuje obiektom wartości domyślne.
Zwróć uwagę, że dla tworzonych obiektów int możemy wywołać pseudo-konstruktor,
przekazując mu domyślną wartość obiektu. Umożliwia to stworzenie obiektu na stercie i
zainicjalizowanie jego wartości (w linii 24. jest to wartość 5, zaś w linii 25., wartość 2).
Destruktor (linie od 28. do 32.) zwalnia zaalokowaną pamięć. Nie ma sensu przypisywać
wskaznikom wartości pustej (null), gdyż po opuszczeniu destruktora i tak nie będą już dostępne.
Jest to jedna z okazji, przy których można bezpiecznie złamać regułę mówiącą, że usuwanym
wskaznikom należy przypisać wartość pustą (choć oczywiście nie zaszkodzi postępować zgodnie z
tą regułą).
Funkcja wywołująca (w tym przypadku funkcja main()) nie jest świadoma, że zmienne składowe
itsAge i itsWeight (jego waga) są wskaznikami do pamięci na stercie. Funkcja main()tak
samo odwołuje się do akcesorów GetAge() i SetAge(), zaś szczegóły zarządzania pamięcią są
ukryte w implementacji klasy  i tak właśnie powinno być.
Gdy w linii 40. zwalniany jest obiekt Mruczek, następuje wywołanie jego destruktora. Destruktor
zwalnia wszystkie wskazniki składowe. Gdyby wskazniki te wskazywały na obiekty innych klas
(lub być może tej samej klasy), zostałyby wywołane także destruktory tych klas.
Przechowywanie własnych zmiennych składowych jako referencji jest całkiem nierozsądne, chyba
że istnieje ku temu ważny powód. W tym przypadku nie ma takiego powodu, ale być może w innej
sytuacji przechowywanie zmiennych okaże się bardzo przydatne.
Nasuwa się oczywiste pytanie: co próbujesz uzyskać? Musisz zacząć od projektu. Jeśli
zaprojektujesz obiekt, który odwołuje się do innego obiektu, a ten drugi obiekt może zaistnieć
przed zaistnieniem pierwszego obiektu i trwać jeszcze po jego zniszczeniu, wtedy pierwszy obiekt
musi odwoływać się do drugiego obiektu poprzez referencję.
Na przykład: pierwszy obiekt może być oknem, a drugi dokumentem. Okno potrzebuje dostępu do
dokumentu, ale nie kontroluje czasu jego życia. Dlatego okno musi odwoływać się do obiektu
poprzez referencję.
W C++ można to osiągnąć poprzez użycie wskazników lub referencji. Referencje zostaną opisane
w rozdziale 9.
Często zadawane pytanie
Gdy zadeklaruję na stosie obiekt, którego dane składowe tworzone są na stercie, co
znajdzie się na stosie, a co na stercie? Na przykład:
#include
class SimpleCat
{
public:
SimpleCat();
~SimpleCat();
int GetAge() const { return *itsAge; }
// inne metody
private:
int * itsAge;
int * itsWeight;
};
SimpleCat::SimpleCat()
{
itsAge = new int(5);
itsWeight = new int(2);
}
SimpleCat::~SimpleCat()
{
delete itsAge;
delete itsWeight;
}
int main()
{
SimpleCat Mruczek;
std::cout << "Mruczek ma " << Mruczek.GetAge() << " lat\n";
Mruczek.SetAge(7);
std::cout << "Mruczek ma " << Mruczek.GetAge() << " lat\n";
return 0;
}
Odpowiedz: Na stosie znajdzie się lokalny obiekt Mruczek. Ten obiekt zawiera dwa wskazniki,
z których każdy zajmuje cztery bajty stosu i zawiera adres zmiennej całkowitej zaalokowanej na
stercie. W tym przykładzie, na stosie i na stercie zajętych zostanie po osiem bajtów.
Wskaznik this
Każda funkcja składowa klasy posiada ukryty parametr, jest nim wskaznik this (to). Wskaznik
this wskazuje na ten egzemplarz obiektu klasy, dla którego wywołana została dana funkcja
składowa. W każdym wywołaniu funkcji GetAge() lub SetAge() występuje ukryty parametr w
postaci wskaznika this.
Wskaznika this można użyć jawnie; pokazuje to listing 8.8.
Listing 8.8. Użycie wskaznika this
0: // Listing 8.8
1: // Użycie wskaznika this
2:
3:
4: #include
5:
6: class Rectangle
7: {
8: public:
9: Rectangle();
10: ~Rectangle();
11: void SetLength(int length)
12: { this->itsLength = length; }
13: int GetLength() const
14: { return this->itsLength; }
15:
16: void SetWidth(int width)
17: { itsWidth = width; }
18: int GetWidth() const
19: { return itsWidth; }
20:
21: private:
22: int itsLength;
23: int itsWidth;
24: };
25:
26: Rectangle::Rectangle()
27: {
28: itsWidth = 5;
29: itsLength = 10;
30: }
31: Rectangle::~Rectangle()
32: {}
33:
34: int main()
35: {
36: Rectangle theRect;
37: cout << "theRect ma dlugosc " << theRect.GetLength()
38: << " metrow.\n";
39: cout << "theRect ma szerokosc " << theRect.GetWidth()
40: << " metrow.\n";
41: theRect.SetLength(20);
42: theRect.SetWidth(10);
43: cout << "theRect ma dlugosc " << theRect.GetLength()
44: << " metrow.\n";
45: cout << "theRect ma szerokosc " << theRect.GetWidth()
46: << " metrow.\n";
47: return 0;
48: }
Wynik
theRect ma dlugosc 10 metrow.
theRect ma szerokosc 5 metrow.
theRect ma dlugosc 20 metrow.
theRect ma szerokosc 10 metrow.
Analiza
Akcesory SetLength() (ustaw długość) oraz GetLength() (pobierz długość) w jawny sposób
korzystają ze wskaznika this przy dostępie do zmiennych składowych obiektu Rectangle
(prostokąt). Akcesory SetWidth() (ustaw szerokość) oraz GetWidth() (pobierz szerokość) nie
korzystają z tego wskaznika jawnie. Nie ma żadnej różnicy pomiędzy działaniami tych akcesorów,
choć składnia z użyciem wskaznika this jest łatwiejsza do zrozumienia.
Gdyby tutaj kończyły się wiadomości na temat wskaznika this, wspominanie o nim nie miałoby
sensu. Należy pamiętać że wskaznik this jest wskaznikiem; oznacza to, że zawiera adres obiektu.
Może zatem okazać się bardzo przydatny.
Praktyczne zastosowanie wskaznika this poznasz w rozdziale 10.,  Funkcje zaawansowane ;
zostanie w nim omówione zagadnienie przeciążania operatorów. Na razie pamiętaj tylko o
istnieniu wskaznika this oraz jego przeznaczeniu: wskazywaniu na swój obiekt klasy.
Nie musisz martwić się tworzeniem i usuwaniem wskaznika this  wszystkim zajmuje się
kompilator.
Utracone wskazniki
Utracone wskazniki są jednym ze zródeł trudnych do zlokalizowania błędów. Wskaznik zostaje
utracony, gdy wywołasz dla niego delete  a więc zwolnisz pamięć, na którą wskazuje  a
następnie nie przypiszesz mu wartości pustej. Jeśli spróbujesz pózniej użyć wskaznika bez
ponownego przypisania mu adresu obiektu, wynik będzie nieprzewidywalny i, o ile masz
szczęście, program się załamie.
Przypomina to sytuację, w której firma kurierska zmieniła adres, a ty użyłeś zaprogramowanego
przycisku w telefonie. Może nie zdarzyłoby się nic strasznego  telefon zadzwoniłby gdzieś w
magazynie na którejś z pustyń. Może się jednak zdarzyć, że numer tego telefonu został
przydzielony fabryce amunicji, a twój telefon doprowadziłby do wybuchu, który wysadziłby w
powietrze całe miasto!
Innymi słowy, nie używaj wskazników po ich zwolnieniu. Wskaznik nadal wskazuje to samo
miejsce w pamięci, ale kompilator może umieścić w nim zupełnie nowe dane; użycie wskaznika
może spowodować załamanie programu. Co gorsza, program może działać pozornie normalnie i
załamać się kilka minut pózniej. Można to nazwać bombą z opóznionym zapłonem, co wcale nie
jest zabawne. W celu zachowania bezpieczeństwa, po zwolnieniu wskaznika przypisz mu wartość
pustą (0). To spowoduje rozbrojenie wskaznika.
Listing 8.9 przedstawia tworzenie utraconego wskaznika.
OSTRZEŻENIE Przedstawiony poniżej program celowo tworzy utracony wskaznik. NIE
uruchamiaj go. Jeśli będziesz miał szczęście, program załamie się sam.
Listing 8.9. Tworzenie utraconego wskaznika
0: // Listing 8.9
1: // Demonstruje utracony wskaznik
2:
3: typedef unsigned short int USHORT;
4: #include
5:
6: int main()
7: {
8: USHORT * pInt = new USHORT;
9: *pInt = 10;
10: std::cout << "*pInt: " << *pInt << std::endl;
11: delete pInt;
12:
13: long * pLong = new long;
14: *pLong = 90000;
15: std::cout << "*pLong: " << *pLong << std::endl;
16:
17: *pInt = 20; // o, nie! on był usunięty!
18:
19: std::cout << "*pInt: " << *pInt << std::endl;
20: std::cout << "*pLong: " << *pLong << std::endl;
21: delete pLong;
22: return 0;
23: }
Wynik
*pInt: 10
*pLong: 90000
*pInt: 20
*pLong: 65556
(Nie próbuj odtworzyć tego wyniku; jeśli masz szczęście, w twoim komputerze uzyskasz inny
wynik, jeśli nie masz szczęścia, komputer ci się zawiesi.)
Analiza
Linia 8. deklaruje zmienną pInt jako wskaznik do typu USHORT; zmienna ta wskazuje nowo
zaalokowaną pamięć. Linia 9. umieszcza w tej pamięci wartość 10, zaś linia 10. wypisuje jej
wartość. Po wypisaniu wartości wskaznik jest zwalniany za pomocą instrukcji delete. W tym
momencie utracony został wskaznik pInt.
Linia 13. deklaruje nowy wskaznik, pLong, wskazujący pamięć zaalokowaną operatorem new. W
linii 14. obiektowi wskazywanemu przez pLong jest przypisywana wartość 9000, zaś w linii 15.
wypisywana jest wartość tego obiektu.
Linia 17. przypisuje wartość 20 do miejsca w pamięci, na które wskazuje pInt, ale wskaznik ten
nie wskazuje na poprawny wynik. Pamięć wskazywana przez pInt została zwolniona w wyniku
wywołania delete, więc przypisanie jej wartości może okazać się katastrofą.
Linia 19. wypisuje wartość wskazywaną przez pInt. Oczywiście, jest nią 20. Linia 20. powinna
wypisać wartość wskazywaną przez pLong, powinna ona wynosić 90000, jednak w tajemniczy
sposób ta wartość zmieniła się na 65556. Nasuwają się dwa pytania:
1. Jak mogła zmienić się wartość wskazywana przez pLong, skoro nie był wykorzystywany
wskaznik pLong?
2. Gdzie została umieszczona wartość 20, która w linii 17. została przypisana obiektowi
wskazywanemu przez pInt?
Jak można się domyślić, pytania te są ze sobą powiązane. Gdy w linii 17., w pamięci wskazywanej
przez pInt umieszczana była wartość, w miejscu wskazywanym dotąd przez pInt kompilator
ufnie umieścił 20. Jednak ponieważ w linii 11. ta pamięć została zwolniona, kompilator mógł ją
przydzielić czemuś innemu. Gdy w linii 13. został stworzony wskaznik pLong, otrzymał on
poprzedni adres pamięci wskazywanej przez pInt. (Proces ten różni się w poszczególnych
komputerach, w zależności od pamięci, w której przechowywane są wartości.) Gdy do miejsca
wskazywanego uprzednio przez pInt zostało przypisane 20, zastąpiło ono wartość wskazywaną
przez pLong. Proces ten nazywa się nadpisaniem wartości i często występuje w przypadku użycia
utraconego wskaznika.
Jest to szczególnie uciążliwy błąd, gdyż zmieniona wartość nie była związana z utraconym
wskaznikiem. Zmiana wartości wskazywanej przez pLong była jedynie efektem ubocznym użycia
utraconego wskaznika pInt. W obszernym programie taki błąd jest bardzo trudny do wykrycia.
Dla zabawy, zastanówmy się, jak mogła znalezć się w pamięci wartość 65 556:
1. Wskaznik pInt wskazywał określone miejsce w pamięci, w którym została umieszczona
wartość 10.
2. Instrukcja delete zwolniła wskaznik pInt, dzięki czemu zwolniło się miejsce do
przechowania innej wartości. Następnie to samo miejsce w pamięci zostało przydzielone
wskaznikowi pLong.
3. W miejscu wskazywanym przez pLong została umieszczona wartość 90000. W komputerze,
w którym uruchomiono ten przykładowy program, do przechowania tej wartości użyto
czterech bajtów (00 01 5F 90), przechowywanych w odwróconej kolejności. Ta wartość
była przechowywana jako 5F 90 00 01.
4. W pamięci wskazywanej przez pInt została umieszczona wartość 20 (czyli w zapisie
szesnastkowym: 00 14). Ponieważ pInt przez cały czas wskazywało ten sam adres,
zastąpione zostały pierwsze dwa bajty pamięci wskazywanej przez pLong, co dało wartość 00
14 00 01.
5. Gdy wartość wskazywana przez pLong została wypisana, odwrócenie bajtów dało wynik 00
01 00 14, co odpowiada wartości dziesiętnej 65556.
Często zadawane pytanie
Jaka jest różnica pomiędzy wskaznikiem pustym a wskaznikiem utraconym?
Odpowiedz: Gdy zwalniasz wskaznik, informujesz kompilator, by zwolnił pamięć. Wskaznik
istnieje nadal i zawiera ten sam adres. Jest jedynie wskaznikiem utraconym.
Gdy napiszesz myPtr = 0; zmieniasz go ze wskaznika utraconego na wskaznik pusty.
Normalnie, gdy usuniesz (zwolnisz) wskaznik, po czym usuniesz go ponownie, wynik będzie
nieprzewidywalny. Oznacza to, że może zdarzyć się dosłownie wszystko  jeśli masz
szczęście, program się załamie. Natomiast przy zwalnianiu pustego wskaznika nie dzieje się
nic; jest to bezpieczne.
Użycie utraconego lub pustego wskaznika (na przykład napisanie myPtr = 5;) jest
niedozwolone i może spowodować załamanie programu. Jeśli wskaznik jest pusty, program
musi się załamać  stanowi to kolejną przewagę wskaznika pustego nad wskaznikiem
utraconym. Programiści zdecydowanie wolą, gdy program załamuje się w sposób
przewidywalny, znacznie ułatwia to usuwanie błędów.
Wskazniki const
W przypadku wskazników, słowo kluczowe const możesz umieścić przed typem, po nim lub w
obu tych miejscach. Wszystkie poniższe deklaracje są poprawne:
const int *pOne;
int * const pTwo;
const int * const pThree;
pOne jest wskaznikiem do stałej wartości całkowitej. Wskazywana przez niego wartość nie może
być zmieniana.
pTwo jest stałym wskaznikiem do wartości całkowitej. Wartość może być zmieniana, ale pTwo nie
może wskazywać na nic innego.
pThree jest stałym wskaznikiem do stałej wartości całkowitej. Wskazywana przez niego wartość
nie może być zmieniana, pThree nie może również wskazywać na nic innego.
Aby dowiedzieć się, która wartość jest stała, wystarczy spojrzeć na prawo od słowa kluczowego
const. Jeśli znajduje się tam typ, stała jest wartość. Jeśli znajduje się zmienna, wtedy stały jest
wskaznik.
const int * p1; // wskazywana wartość typu int jest stała
int * const p2; // p2 jest stałe i nie może wskazywać na nic innego
Wskazniki const i funkcje składowe const
Z rozdziału 6.,  Programowanie zorientowane obiektowo , dowiedziałeś się, że funkcja składowa
może być zadeklarowana za pomocą słowa kluczowego const. Gdy funkcja zostanie
zadeklarowana w taki właśnie sposób, przy każdej próbie zmiany danych obiektu wewnątrz tej
funkcji kompilator zgłosi błąd.
Jeśli zadeklarujesz wskaznik do obiektu const, za pomocą pomocy tego wskaznika możesz
wywoływać tylko metody const. Ilustruje to listing 8.10.
Listing 8.10. Użycie wskaznika do obiektu const
0: // Listing 8.10
1: // Użycie wskazników z metodami const
2:
3: #include
4: using namespace std;
5:
6: class Rectangle
7: {
8: public:
9: Rectangle();
10: ~Rectangle();
11: void SetLength(int length) { itsLength = length; }
12: int GetLength() const { return itsLength; }
13: void SetWidth(int width) { itsWidth = width; }
14: int GetWidth() const { return itsWidth; }
15:
16: private:
17: int itsLength;
18: int itsWidth;
19: };
20:
21: Rectangle::Rectangle()
22: {
23: itsWidth = 5;
24: itsLength = 10;
25: }
26:
27: Rectangle::~Rectangle()
28: {}
29:
30: int main()
31: {
32: Rectangle* pRect = new Rectangle;
33: const Rectangle * pConstRect = new Rectangle;
34: Rectangle * const pConstPtr = new Rectangle;
35:
36: cout << "szerokosc pRect: " << pRect->GetWidth()
37: << " metrow\n";
38: cout << "szerokosc pConstRect: " << pConstRect->GetWidth()
39: << " metrow\n";
40: cout << "szerokosc pConstPtr: " << pConstPtr->GetWidth()
41: << " metrow\n";
42:
43: pRect->SetWidth(10);
44: // pConstRect->SetWidth(10);
45: pConstPtr->SetWidth(10);
46:
47: cout << "szerokosc pRect: " << pRect->GetWidth()
48: << " metrow\n";
49: cout << "szerokosc pConstRect: " << pConstRect->GetWidth()
50: << " metrow\n";
51: cout << "szerokosc pConstPtr: " << pConstPtr->GetWidth()
52: << " metrow\n";
53: return 0;
54: }
Wynik
szerokosc pRect: 5 metrow
szerokosc pConstRect: 5 metrow
szerokosc pConstPtr: 5 metrow
szerokosc pRect: 10 metrow
szerokosc pConstRect: 5 metrow
szerokosc pConstPtr: 10 metrow
Analiza
Linie od 6. do 19. deklarują klasę Rectangle (prostokąt). Linia 14. deklaruje metodę
GetWidth() (pobierz szerokość) jako funkcję składową const. Linia 32. deklaruje wskaznik do
obiektu typu Rectangle. Linia 33. deklaruje pConstRect, który jest wskaznikiem do stałego
obiektu typu Rectangle. Linia 34. deklaruje pConstPtr, który jest stałym wskaznikiem do
obiektu typu Rectangle.
Linie od 36. do 41. wypisują ich wartości.
W linii 43. wskaznik pRect jest używany do ustawienia szerokości prostokąta na 10. W linii 44.
zostałby użyty wskaznik pConstRect, ale został on zadeklarowany jako wskazujący na stały
obiekt typu Rectangle. W związku z tym nie może legalnie wywoływać funkcji składowej, nie
będącej funkcją const, dlatego został wykomentowany. W linii 45. wskaznik pConstPtr
wywołuje metodę SetWidth() (ustaw szerokość). Wskaznik pConstPtr został zadeklarowany
jako stały wskaznik do obiektu typu Rectangle. Innymi słowy, zawartość wskaznika jest stała i
nie może wskazywać na nic innego, natomiast wskazywany prostokąt nie jest stały.
Wskazniki const this
Gdy deklarujesz obiekt jako const, deklarujesz jednocześnie, że wskaznik this jest wskaznikiem
do obiektu const. Wskaznik const this może być używany tylko z funkcjami składowymi
const.
Stałe obiekty i stałe wskazniki omówimy szerzej w następnym rozdziale, przy okazji omawiania
referencji do stałych obiektów.
TAK NIE
Jeśli nie chcesz, by obiekty przekazywane przez Nie usuwaj wskaznika więcej niż jeden raz.
referencję były zmieniane, chroń je słowem
kluczowym const.
Jeśli obiekt może być zmieniany, przekazuj go
przez referencję.
Jeśli nie chcesz, by zmieniany był mały obiekt,
przekazuj go przez wartość.
Działania arytmetyczne na wskaznikach 
temat dla zaawansowanych
Wskazniki można od siebie odejmować. Jedną z użytecznych technik jest przypisanie dwóm
wskaznikom różnych elementów tablicy, a następnie odjęcie wskazników od siebie (w celu
obliczenia ilości elementów rozdzielających dwa wskazywane elementy). Może to być bardzo
użyteczne w przypadku przetwarzania tablic znaków. Pokazuje to listing 8.11.
Listing 8.11. Wydzielanie słów z łańcucha znaków
0: #include
1: #include
2: #include
3:
4: bool GetWord(char* theString,
5: char* word, int& wordOffset);
6:
7: // program sterujący
8: int main()
9: {
10: const int bufferSize = 255;
11: char buffer[bufferSize+1]; // zawiera cały łańcuch
12: char word[bufferSize+1]; // zawiera słowo
13: int wordOffset = 0; // zaczynamy od początku
14:
15: std::cout << "Wpisz lancuch znakow:\n";
16: std::cin.getline(buffer,bufferSize);
17:
18: while (GetWord(buffer,word,wordOffset))
19: {
20: std::cout << "Wydzielono slowo: " << word << std::endl;
21: }
22:
23: return 0;
24:
25: }
26:
27:
28: // funkcja wydzielająca słowa z łańcucha.
29: bool GetWord(char* theString, char* word, int& wordOffset)
30: {
31:
32: if (!theString[wordOffset]) // koniec łańcucha?
33: return false;
34:
35: char *p1, *p2;
36: p1 = p2 = theString+wordOffset; // wskazuje następne słow
37:
38: // pomijamy wiodące spacje
39: for (int i = 0; i<(int)strlen(p1) && !isalnum(p1[0]); i++)
40: p1++;
41:
42: // sprawdzamy, czy mamy słowo
43: if (!isalnum(p1[0]))
44: return false;
45:
46: // p1 wskazuje teraz na początek następnego słowa;
47: // niech na nie wskazuje także p2
48: p2 = p1;
49:
50: // przesuwamy p2 na koniec słowa
51: while (isalnum(p2[0]))
52: p2++;
53:
54: // teraz p2 wskazuje na koniec słowa
55: // p1 wskazuje na początek słowa
56: // różnicą jest długość słowa
57: int len = int (p2 - p1);
58:
59: // kopiujemy słowo do bufora
60: strncpy (word,p1,len);
61:
62: // kończymy je bajtem zerowym
63: word[len]='\0';
64:
65: // szukamy początku następnego słowa
66: for (int j = int(p2-theString); j<(int)strlen(theString)
67: && !isalnum(p2[0]); j++)
68: {
69: p2++;
70: }
71:
72: wordOffset = int(p2-theString);
73:
74: return true;
75: }
Wynik
Wpisz lancuch znakow:
Ten kod po raz pierwszy pojawil sie w raporcie jezyka C++
Wydzielono slowo: Ten
Wydzielono slowo: kod
Wydzielono slowo: po
Wydzielono slowo: raz
Wydzielono slowo: pierwszy
Wydzielono slowo: pojawil
Wydzielono slowo: sie
Wydzielono slowo: w
Wydzielono slowo: raporcie
Wydzielono slowo: jezyka
Wydzielono slowo: C
Analiza
W linii 15. użytkownik jest proszony o wpisanie łańcucha znaków. Otrzymany łańcuch jest
przekazywany funkcji GetWord() (pobierz słowo) w linii 18., wraz z buforem do przechowania
pierwszego słowa i zmienną wordOffset (przesunięcie słowa), inicjalizowaną w linii 13.
wartością zero. Słowa zwracane przez funkcję GetWord() są wypisywane do chwili, w której
funkcja ta zwróci wartość false.
Każde wywołanie funkcji GetWord() powoduje skok do linii 29. W linii 32. sprawdzamy, czy
wartością theString[wordOffset] jest zero  to będzie oznaczać, że doszliśmy do końca
łańcucha; w takim przypadku funkcja GetWord() zwróci wartość false.
Zwróć uwagę na fakt, że C++ uważa zero za wartość false. Moglibyśmy przepisać tę linię
następująco:
32: if (theString[wordOffset] == 0) // koniec łańcucha?
W linii 35. są deklarowane dwa wskazniki do znaków, p1 i p2, które w linii 36. są inicjalizowane
tak, aby wskazywały na miejsce łańcucha o przesunięciu wordOffset względem jego początku.
Początkowo zmienna wordOffset ma wartość zero, więc oba wskazniki wskazują początek
łańcucha.
Linie 39. i 40. przechodzą poprzez łańcuch, do chwili, w której wskaznik p1 wskaże pierwszy
znak alfanumeryczny. Linie 43. i 44. zapewniają, że faktycznie znalezliśmy znak alfanumeryczny;
jeśli tak nie jest, zwracamy wartość false.
Wskaznik p1 wskazuje teraz na następne słowo, a linia 48. ustawia wskaznik p2 tak, aby
wskazywał na to samo miejsce.
Następnie linie 51. i 52. powodują, że wskaznik p2 przechodzi poprzez słowo, zatrzymując się na
pierwszym znaku nie będącym znakiem alfanumerycznym. W tym momencie wskaznik p2
wskazuje na koniec słowa, na którego początek wskazuje wskaznik p1. Odejmując wskaznik p1
od wskaznika p2 w linii 55. i rzutując rezultat do wartości całkowitej, możemy obliczyć długość
słowa. Następnie kopiujemy słowo do bufora word (słowo), przekazując wskaznik (wskazujący
początkowy znak  p1) oraz obliczoną długość słowa.
W linii 63. na końcu słowa w buforze dopisujemy wartość null (zero). Wskaznik p2 jest
inkrementowany tak, by wskazywał na początek następnego słowa, następnie przesunięcie tego
słowa (względem początku łańcucha) jest umieszczane w zmiennej referencyjnej wordOffset.
Na koniec, funkcja zwraca wartość true, aby wskazać, że znaleziono następne słowo.
Jest to klasyczny przykład kodu, który najlepiej analizować uruchamiając w debuggerze, śledząc
jego działanie krok po kroku.


Wyszukiwarka

Podobne podstrony:
Cinquecento demontaż deski rozdzielczej tablicy wskaźników poradnik
Alchemia II Rozdział 8
Drzwi do przeznaczenia, rozdział 2
czesc rozdzial
Rozdział 51
rozdzial
rozdzial (140)
rozdzial
rozdział 25 Prześwięty Asziata Szyjemasz, z Góry posłany na Ziemię
czesc rozdzial
rozdzial1
Rozdzial5
Rozdział V
Rescued Rozdział 9

więcej podobnych podstron