Jedną z najbardziej przydatnych dla programisty C++ rzeczy jest możliwość bezpośredniego manipulowania pamięcią za pomocą wskaźników.
Z tego rozdziału dowiesz się:
· czym są wskaźniki,
· jak deklarować wskaźniki i używać ich,
· czym jest sterta i w jaki sposób można manipulować pamięcią.
Wskaźniki 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 wskaźników. Aby w pełni zrozumieć potrzebę ich używania, musisz zapoznać się z zawartością kolejnych rozdziałów.
Wskaźnik (ang. pointer) jest zmienną, przechowującą adres pamięci. To wszystko. Jeśli rozumiesz to proste stwierdzenie, wiesz już wszystko o wskaźnikach. Jeszcze raz: wskaźnik jest zmienną przechowującą adres pamięci.
Aby zrozumieć, do czego służą wskaźniki, 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
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 <iostream>
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!)
Każda zmienna posiada adres. Możesz umieścić go we wskaźniku nawet bez znajomości adresu danej zmiennej.
Przypuśćmy na przykład, że zmienna howOld jest całkowita. Aby zadeklarować wskaźnik o nazwie pAge, mogący zawierać adres tej zmiennej, możesz napisać:
int *pAge = 0;
Spowoduje to zadeklarowanie zmiennej pAge jako wskaźnika 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ą wskaźnikową 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.
W tym przykładzie wskaźnik pAge jest inicjalizowany wartością zero. Wskaźnik, którego wartością jest zero, jest nazywany wskaźnikiem pustym (ang. null pointer). Podczas tworzenia wskaźników, powinny być one zainicjalizowane jakąś wartością. Jeśli nie wiesz, jaką wartość przypisać wskaźnikowi, przypisz mu wartość 0. Wskaźnik, który nie jest zainicjalizowany, jest nazywany wskaźnikiem błędnym (ang. wild pointer). Błędne wskaźniki są bardzo niebezpieczne.
UWAGA Pamiętaj o zasadzie bezpiecznego programowania: inicjalizuj swoje wskaźniki!
Musisz jawnie przypisać wskaźnikowi adres zmiennej howOld. Poniższy przykład pokazuje, jak to zrobić:
unsigned short int howOld = 50; // tworzymy zmienną
unsigned short int * pAge = 0; // tworzymy wskaźnik
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 wskaźnik do typu unsigned short int i ustawia ją na zero. To, że zmienna pAge jest wskaźnikiem, można poznać po gwiazdce (*) umieszczonej pomiędzy typem zmiennej a jej nazwą.
Trzecia, ostatnia linia, przypisuje wskaźnikowi pAge adres zmiennej howOld. Przypisywanie adresu można poznać po użyciu operatora adresu (&). Gdyby operator adresu został pominięty, wskaźnikowi pAge zostałaby przypisana wartość zmiennej howOld. Oczywiście, wartość ta mogłaby być poprawnym adresem.
Teraz wskaźnik 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 * pAge = &howOld; // tworzymy wskaźnik do howOld
pAge jest wskaźnikiem, zawierającym teraz adres zmiennej howOld. Używając wskaźnika pAge, możesz sprawdzić wartość zmiennej howOld, która w tym przypadku wynosi 50. Dostęp do zmiennej howOld poprzez wskaźnik pAge jest nazywany dostępem pośrednim (dostęp do niej rzeczywiście odbywa się poprzez ten wskaźnik). 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 wskaźniku. Użycie wskaźnikó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 wskaźników sytuacja wygląda inaczej: każdy wskaźnik zajmuje cztery bajty. Typ wskaźnika informuje kompilator, ile potrzeba miejsca w pamięci do przechowania obiektu, którego adres zawiera wskaźnik!
W deklaracji
unsigned shot int * pAge = 0; // tworzymy wskaźnik
zmienna pAge jest zadeklarowana jako wskaźnik do typu unsigned short int. Mówi ona kompilatorowi, że wskaźnik ten (który do przechowania adresu wymaga czterech bajtów) będzie przechowywał adres obiektu typu unsigned short int, który zajmuje dwa bajty.
Podobnie jak inne zmienne, wskaźniki mogą mieć dowolne nazwy. Wielu programistów przestrzega konwencji, w której nazwy wszystkiech wskaźników poprzedza się literką p (pointer), np. pAge czy pNumber.
Operator wyłuskania (*) jest zwany także operatorem dostępu pośredniego albo dereferencją. Podczas wyłuskiwania wskaźnika otrzymywana jest wartość wskazywana przez adres zawarty w tym wskaźniku.
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;
Wskaźnik umożliwia pośredni dostęp do wartości zmiennej, której adres zawiera. Aby przypisać wartość zmiennej howOld do zmiennej yourAge za pomocą wskaźnika pAge, powinieneś napisać:
yourAge = *pAge;
Operator wyłuskania (*) znajdujący się przed zmienną pAge oznacza „wartość przechowywana pod adresem.” To przypisanie można potraktować jako: „Weź wartość przechowywaną pod adresem zawartym w pAge i przypisz ją do zmiennej yourAge”.
UWAGA W przypadku wskaźników gwiazdka (*) może posiadać dwa znaczenia (może symbolizować część deklaracji wskaźnika albo operator wyłuskania).
Gdy deklarujesz wskaźnik, * jest częścią deklaracji i następuje po typie wskazywanego obiektu. Na przykład:
// tworzymy wskaźnik do typu unsigned short
unsigned short * page = 0;
Gdy wskaźnik 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 wskaźniku, 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.
Należy dokonać rozróżnienia pomiędzy wskaźnikiem, adresem zawartym w tym wskaźniku, a zmienną o adresie zawartym w tym wskaźniku. Nieumiejętność rozróżnienia ich jest najczęstszym powodem nieporozumień ze wskaźnikami.
Weźmy 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 wskaźnik do typu int i jest inicjalizowana adresem zmiennej theVariable. pPointer jest wskaźnikiem. 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 wskaźnikowa 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 wskaźników zawierających adresy pamięci.
Gdy przypiszesz wskaźnikowi adres zmiennej, możesz użyć tego wskaźnika w celu uzyskania dostępu do danych zawartych w tej zmiennej. Listing 8.2 pokazuje, w jaki sposób adres lokalnej zmiennej jest przypisywany wskaźnikowi i w jaki sposób ten wskaźnik może operować wartością w tej zmiennej.
Listing 8.2. Operowanie danymi poprzez wskaźnik
0: // Listing 8.2 Użycie wskaźnika
1:
2: #include <iostream>
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; // wskaźnik
13:
14: myAge = 5;
16: cout << "myAge: " << myAge << "\n";
17: pAge = &myAge; // wskaźnikowi pAge przypisuje adres zmiennej myAge
18: cout << "*pAge: " << *pAge << "\n\n";
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: }
myAge: 5
*pAge: 5
Ustawiam *pAge = 7...
*pAge: 7
myAge: 7
Ustawiam myAge = 9...
myAge: 9
*pAge: 9
Program deklaruje dwie zmienne: myAge typu un...
Infesto