00-wskazniki-by-zuchel.pdf

(128 KB) Pobierz
Wskaźniki
opracował: inż. Marcin Żuchelkowski
Wskaźnik
(ang.
pointer)
- typ zmiennej odpowiedzialnej za przechowywanie adresu do innej
zmiennej (innego miejsca w pamięci) w obrębie naszej aplikacji.
Wskaźnik
może wskazywać na jakąś zmienną, strukturę, tablicę a nawet funkcję. Jest niczym
innym jak zmienną, która nie przechowuje wartości, tylko wskazuje na inną zmienną istniejącą w
aplikacji. Oto podstawowe operatory niezbędne do operowania wskaźnikami:
*
-
operator wyłuskania wartości
zmiennej, na którą wskazuje wskaźnik (wyciąga wartość
ze wskaźnika)
&
-
operator pobrania adresu
określonej zmiennej
Jako, że C++ jest językiem, to możemy przetłumaczyć niektóre instrukcje na mniej formalny i
bardziej ludzki język polski.
I tak:
foo = &myvar;
możemy przetłumaczyć jako: „foo staje się adresem zmiennej myvar.”
Operator wyłuskania również można również przetłumaczyć na polski. I tak:
baz = *foo;
możemy przetłumaczyć jako: „baz staje się równe wartości wskazywanej przez foo.”
Jest jedna konwencja leksykalna, która psuje oznaczenia, ponieważ C++ dopuszcza:
int *p=&c;
a jest to „skrót myślowy” od:
int *p;
p=&c;
Na szczęście to występuje tylko przy deklaracji wskaźnika...
Przećwiczny jeszcze często występujące konwencje:
int c, *pc;
Błędne będą:
pc = c; //
Error –
pc
to adres, ale
c
jest wartością.
*pc = &c;
// Error -
&c
jest adresem, ale
*pc
jest wartością.
Poprawne zaś będą:
pc = &c;
// Obydwa
pc
i
&c
są adresami.
*pc = c; //Obydwa
*pc
i
c
są wartościami.
#include <stdio.h>
int main()
{
int *pc, c; //rys.1
c = 22; //rys.2
printf("Adres c: %p\n", &c);
printf("Wartosc c: %d\n\n", c);
// 22
pc = &c; //rys.3 – dowiązanie wskaźnika
printf("Adres pointer pc: %p\n", pc);
printf("Wartosc wskazywana pc: %d\n\n", *pc); // 22
c = 11; //rys.4
printf("Adres pointer pc: %p\n", pc);
printf("Wartosc wskazywana pc: %d\n\n", *pc); // 11
*pc = 2; //rys.5
printf("Adres c: %p\n", &c);
printf("Wartosc c: %d\n\n", c); // 2
return 0;
}
Wskaźniki i tablice
Pojęcie tablic jest powiązane z pojęciem wskaźników. W rzeczywistości tablice działają bardzo
podobnie do wskaźników do ich pierwszych elementów, a właściwie tablicę zawsze można
niejawnie przekonwertować na wskaźnik odpowiedniego typu. Rozważmy na przykład te dwie
deklaracje:
int myarray [20];
int * mypointer;
Poprawna byłaby następująca operacja przypisania:
mypointer = myarray;
Następnie
mypointer
i
myarray
byłyby równoważne i miałyby bardzo podobne właściwości.
Główną różnicą jest to, że
mypointer
może mieć przypisany inny adres, podczas gdy
myarray
nigdy
nie można przypisać do niczego i zawsze będzie reprezentować ten sam blok 20 elementów typu
int. Dlatego następujące przypisanie byłoby nieprawidłowe:
myarray = mypointer;
Zobaczmy przykład, który łączy tablice i wskaźniki:
#include <iostream>
using namespace std;
int main ()
{
int numbers[5];
int * p;
p = numbers; *p = 10;
p++; *p = 20;
p = &numbers[2]; *p = 30;
p = numbers + 3; *p = 40;
p = numbers; *(p+4) = 50;
for (int n=0; n<5; n++)
cout << numbers[n] << ", ";
return 0;
}
Wskaźniki i tablice obsługują ten sam zestaw operacji, z tym samym znaczeniem dla obu. Główną
różnicą jest to, że wskaźnikom można przypisywać nowe adresy, podczas gdy tablicy nie.
Gdy poznawaliśmy tablice nawias kwadratowy ([]) zostały wyjaśniony jako indeks elementu
tablicy. Cóż, w rzeczywistości te nawiasy są operatorem wyłuskiwania znanym jako operator
przesunięcia. Odwołują się do zmiennej, za którą podążają, tak jak robi to *, ale dodają również
liczbę w nawiasach do wyłuskiwanego adresu. Na przykład:
a[5] = 0;
*(a+5) = 0;
// a [offset 5] = 0
// wartość wskazywana przez (a+5) = 0
Te dwa wyrażenia są równoważne i poprawne, nie tylko jeśli a jest wskaźnikiem, ale także jeśli a
jest tablicą. Pamiętaj, że jeśli jest to tablica, to jej nazwa może być używana tak samo jak wskaźnik
do jej pierwszego elementu.
Inicjalizacja wskaźników.
Wskaźniki można zainicjować tak, aby wskazywały określone lokalizacje w momencie ich
zdefiniowania:
int myvar;
int * myptr = &myvar;
//tutaj ten „skrót myślowy”
Wynikowy stan zmiennych po tym kodzie (wyżej) jest taki sam jak po:
int myvar;
int * myptr;
myptr = &myvar;
Kiedy wskaźniki są inicjowane, inicjowany jest adres, na który wskazują (tj.
myptr),
nigdy
wskazywana wartość (tj.
* myptr).
Dlatego powyższego kodu nie należy mylić z:
int myvar;
int * myptr;
*myptr = &myvar;
Co zresztą nie miałoby większego sensu (i nie jest prawidłowym kodem).
Gwiazdka (*) w deklaracji wskaźnika (wiersz 2) wskazuje tylko, że jest to wskaźnik, a nie operator
wyłuskiwania (jak w wierszu 3). Obie rzeczy po prostu używają tego samego znaku: *. Jak zawsze,
spacje nie są istotne i nigdy nie zmieniają znaczenia wyrażenia.
Wskaźniki mogą być zainicjowane albo adresem zmiennej (tak jak w powyższym przypadku) lub
wartością innego wskaźnika (lub tablicy):
int myvar;
int
*foo = &myvar;
int
*bar = foo;
Arytmetyka wskaźników:
Przeprowadzanie operacji arytmetycznych na wskaźnikach jest trochę inne niż przeprowadzanie ich
na zwykłych typach liczb całkowitych. Na początek dozwolone są tylko operacje dodawania i
odejmowania; inne nie mają sensu w świecie wskaźników. Ale zarówno dodawanie, jak i
odejmowanie mają nieco inne zachowanie ze wskaźnikami, w zależności od rozmiaru typu danych,
na który wskazują.
Kiedy wprowadzono podstawowe typy danych, zobaczyliśmy, że typy mają różne rozmiary. Na
przykład:
char
ma zawsze rozmiar 1 bajta,
short
jest zazwyczaj większy, a
int
i
long
są jeszcze
większe; ich dokładny rozmiar zależy od systemu. Na przykład wyobraźmy sobie, że w danym
systemie
char
zajmuje 1 bajt,
short
zajmuje 2 bajty, a
long
zajmuje 4.
Przypuśćmy, że zdefiniujemy 3 wskaźniki w kompilatorze:
char *mychar;
short *myshort;
long *mylong;
i wiemy że wskazują na punkty 1000, 2000 oraz 3000.
Wtedy jeśli napiszemy:
++mychar;
++myshort;
++mylong;
mychar,
jak można by się spodziewać, zawierałby wartość 1001. Ale co, nie jest to oczywiste,
myshort
zawierałby wartość 2002, a
mylong
zawierałby 3004, mimo że każdy z nich został
zwiększony tylko raz. Powodem jest to, że podczas dodawania jedynki do wskaźnika, wskaźnik
wskazuje na następny element tego samego typu, a zatem rozmiar w bajtach typu, na który
wskazuje, jest dodawany do wskaźnika.
Dotyczy to zarówno dodawania,
jak i odejmowania dowolnej
liczby od wskaźnika. Stałoby się
dokładnie tak samo, gdybyśmy
napisali:
mychar = mychar + 1;
myshort = myshort + 1;
mylong = mylong + 1;
Jeśli chodzi o operatory
inkrementacji (++) i
dekrementacji (--), oba mogą być
używane jako prefiks lub sufiks
wyrażenia, z niewielką różnicą w
zachowaniu: jako prefiks inkrementacja następuje przed oceną wyrażenia i jako przyrostek przyrost
następuje po ocenie wyrażenia. Dotyczy to również wyrażeń inkrementujących i dekrementujących
wskaźniki, które mogą stać się częścią bardziej skomplikowanych wyrażeń zawierających również
operatory wyłuskiwania (*). Pamiętając zasady pierwszeństwa operatorów, możemy przypomnieć,
że operatory przyrostkowe, takie jak inkrementacja i dekrementacja, mają wyższy priorytet niż
operatory przedrostkowe, takie jak operator dereferencji (*). Dlatego następujące wyrażenie:
*p++
jest równoważne do
*(p++).
A to, co robi, to zwiększenie wartości
p
(więc wskazuje teraz na następny element), ale ponieważ
++ jest używane jako przyrostek, całe wyrażenie jest oceniane jako wartość wskazywana pierwotnie
przez wskaźnik (adres, na który wskazywał przed zwiększeniem).
Zasadniczo są to cztery możliwe kombinacje operatora dereferencji z wersją prefiksową i sufiksową
operatora inkrementacji (to samo dotyczy również operatora dekrementacji):
*p++
// to samo co
*(p++):
inkrementacja wskaźnika i dereferencja nieinkrementowanego adresu
*++p
// to samo co
*(++p):
inkrementacja wskaźnika i dereferencja inkrementowanego adresu
++*p
// to samo co
++(*p):
wyłuskaj wskaźnik i zwiększ wartość, na którą wskazuje
(*p)++
// dereferencja wskaźnika i post-inkrementacja wartości, na którą wskazuje
Typowe - ale nie takie proste - stwierdzenie zawierające te operatory to:
*p++ = *q++;
Ponieważ ++ ma wyższy priorytet niż *, zarówno p, jak i q są inkrementowane, ale ponieważ oba
operatory inkrementacji (++) są używane jako przyrostek, a nie prefiks, wartość przypisana do *p to
*q przed zwiększeniem zarówno p, jak i q . A potem obie są zwiększane. Byłoby to mniej więcej
równoważne:
*p = *q;
++p;
++q;
Jak zawsze, nawiasy zmniejszają zamieszanie, zwiększając czytelność wyrażeń.
Zgłoś jeśli naruszono regulamin