r08-06.doc

(221 KB) Pobierz
Szablon dla tlumaczy

Rozdział 8.
Wskaźniki

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.

Czym jest wskaźnik?

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.

Kilka słów na temat 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

 

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 <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!)

Przechowywanie adresu we wskaźniku

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.

Puste i błędne wskaźniki

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 howOld = 50;      // tworzymy zmienną

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.

Nazwy wskaźników

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

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ć:

 

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: „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.

Wskaźniki, adresy i zmienne

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.

Operowanie danymi poprzez wskaźniki

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;

15: 

16:      cout << "myAge: " << myAge << "\n";

17:      pAge = &myAge;  // wskaźnikowi 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 un...

Zgłoś jeśli naruszono regulamin