Wskaźnik (typ danych)W językach programowania pozwalających na bezpośredni dostęp do pamięci (jak np. Język asemblera, C, C++, Cyclone) pamięć jest reprezentowana jako jednowymiarowa tablica bajtów – wszystkie zmienne (statyczne i dynamiczne) są umieszczane w tej „tablicy”. Wskaźnik jest indeksem do tej tablicy – najczęściej ów indeks jest jednocześnie logicznym adresem. Zwykle istnieje też specjalny symbol, który określa wskazanie jako puste. W językach C, Cyclone jest to NULL, w C++ nullptr, w Pascalu nil. Wskaźnik taki nie wskazuje na nic (konkretnie: jego wartość liczbowa jako adresu jest równa 0), gdyż w nowoczesnych systemach operacyjnych żaden proces nie ma dostępu do komórki pamięci o adresie 0, stąd też jest ona wykorzystywana do oznaczenia wskazania do niczego. Wartość ta służy np. do oznaczania końca listy jednokierunkowej, liści drzewa binarnego itp. W nowszych językach takich jak Java czy C# zamiast wskaźników używa się ulepszonej formy referencji, które nigdy nie mogą wskazywać na przypadkowy adres pamięci, mogą jedynie wskazywać na rzeczywisty obiekt lub mieć wartość null. Eliminuje to całą kategorię błędów wynikających z próby interpretacji przypadkowego fragmentu pamięci jako obszaru zawierającego konkretne, użyteczne dane. Nie dają one jednak pełnej kontroli nad pamięcią i uniemożliwiają wykorzystanie wskaźników do szybkiego poruszania się po tablicy. Definicja wskaźnika na obiekt lub (prostą) zmiennąW językach C/C++ definicja wskaźnika jest instrukcją składającą się z nazwy typu zmiennej, znaku * (używanego też w innym kontekście jako operator wyłuskania) i identyfikatora nowo tworzonego wskaźnika: TYP * zmienna;
Przykłady: int* p; // 'p' jest zmienną wskaźnikową przechowującą adres liczby typu 'int'
double* q; // 'q' jest zmienną wskaźnikową przechowującą adres liczby typu 'double'
struct X; // deklaracja struktury
X* pp; // definicja wskaźnika na obiekt/strukturę typu X
W języku C tradycyjnie znak * umieszcza się tuż przy nazwie zmiennej (co uwypukla nazwę typu, na który wskazuje wskaźnik); w C++ tuż przy nazwie typu (co uwypukla nazwę zmiennej i jej typ). Obydwie możliwości są jednak poprawne dla każdego z tych języków. int *p; // zapis w stylu języka C
int* p; // ta sama instrukcja w stylu języka C++
W przypadku definiowania kilku zmiennych w jednej linii, wskaźnikami są tylko te, przy których postawiony został znak *: int *a, *b, c; //a, b - wskaźniki na typ int, c - zmienna typu int
Operator pobrania adresuKażda zmienna/obiekt posiada jednoznaczny adres. W językach C i C++ adres ten można pobrać przy pomocy operatora & int *w; /* 'w' jest wskaźnikiem na zmienną typu int */
int a = 5;
int b = 5;
w = &a; /* wskaźnik 'w' wskazuje teraz na obszar
pamięci zajmowany przez zmienną 'a' */
w = &b; /* a teraz 'w' wskazuje na zmienną 'b' */
Własnoręczne wpisywanie adresu oraz operacje na wskaźnikach (C/C++)Skoro można pobrać adres zmiennych, to można też wpisać go własnoręcznie: typ_danych *wskaźnik = (typ_danych*) 0x556677; // Dowolny adres w pamięci
Można robić także operacje arytmetyczne: wskaźnik = 0x556677 + 0x4;
Uwaga: operacje na adresach typu + - += -= ++ -- zmieniają adres o: liczba * sizeof(typ_danych) // Dla + - += -=
sizeof(typ_danych) // Dla ++ --
, np. Wyłuskanie wskaźnikaObszar wskazywany przez wskaźnik na int *w; /* wskaźnik na zmienną typu int */
int a = 100;
int b;
w = &a; /* wskaźnik 'w' wskazuje na zmienną typu int 'a' */
b = *w; /* przypisz zmiennej 'b' wartość spod adresu wskazywanego
przez 'w'; teraz 'b' równe jest 100 */
Rzutowanie wskaźnikaPonieważ formalnie wskaźnik pokazuje na obszar pamięci, można dowolnie interpretować zawartość tej pamięci. Jest to pewne uogólnienie, które może powodować błędy. Rzutowaniem nazywa się operację wymuszającą interpretację danego wskazania jako określonego typu danych, np. char* wiki = "Wikipedia";
printf("Adres %d interpreterowany jako\n", (unsigned int)wiki);
printf("* łańcuch znaków: %s\n", wiki);
printf("* znak: %c\n", *(char*)wiki);
printf("* wartość całkowita ze znakiem: %d\n", *(int*)wiki);
printf("* wartość całkowita bez znaku: %d\n", *(unsigned int*)wiki);
I przykładowy wynik działania programu: Adres 134514016 interpretowany jako * łańcuch znaków: Wikipedia * znak: W * wartość całkowita ze znakiem: 1768646999 * wartość całkowita bez znaku: 1768646999 Problemy podczas rzutowaniaO ile takie rzutowanie np.: z typu long double c = 0.0;
char d = 'x';
long double* wsk1LongDouble = &c;
d = *(char*)wsk1LongDouble; // bezpiecznie
char* wskChar = &d;
c = *(long double*)wskChar; // niebezpieczne
Jest to spowodowane tym, że rozmiar zmiennej typu char wynosi 1 bajt (jest to pewne uproszczenie, ale dla potrzeb przykładu wystarczy), podczas gdy zmiennej typu long double wynosi 8 bajtów. W momencie dereferencji zrzutowanego wskaźnika, wskazującego na wartość jedno bajtową, nastąpi próba odczytania wartości long double, czyli 8 bajtów, które znajdują się być może w obszarze pamięci, który nie został przydzielony programowi przez system operacyjny. Podobna sytuacja ma miejsce podczas rzutowania w dół. Rzutowanie w dółPrzykład w języku C++: class Bazowa {
public:
int x;
};
class Pochodna : public Bazowa {
public:
int y;
};
void fun1( Bazowa& obiekt ) {
obiekt.x = 5;
}
void fun2( Pochodna& obiekt ) {
obiekt.y = 5;
}
int main() {
Bazowa obiektBazowy; // deklaracja obiektu klasy Bazowa
Pochodna obiektPochodny; // deklaracja obiektu klasy Pochodna
Bazowa* wskBazowa = &obiektBazowy;
Pochodna* wskPochodna = &obiektPochodny;
fun1( obiektPochodny ); // ok
fun1( *wskPochodna ); // ok
fun2( *((Pochodna*)wskBazowa) ); // nie ok!!
}
Możliwa jest konwersja standardowa obiektu typu pochodnego na typ bazowy. Zarówno obiekt bazowy, jak i pochodny posiadają w swojej strukturze pole Pomimo tego czasem celowo rzutuje się w dół, lecz należy dbać o bezpieczeństwo takiego rzutowania. Wskaźnik voidWskaźnik void(ang. Void pointer) to Wskaźnik bez określonego typu danych. Może przechowywać adres dowolnego typu i być rzutowany na dowolny typ. int a = 256;
char b = 'c';
void* ptr = &a; //ptr przechowuje adres zmiennej a
ptr = &b; //ptr przechowuje adres zmiennej b
Wskaźnik void nie może być dereferencjonowany ponieważ nie posiada informacji ile miejsca w pamięci powinna zajmować wartość danej zmiennej. #include <stdio.h>
int main(){
int a = 8;
void* ptr = &a;
printf("%d", *ptr);
return 0;
}
Z tego powodu rezultatem próby wypisania w linijce 5 wartości wskazywanej przez wskaźnik void będzie poniższy błąd kompilatora: Compiler Error: 'void*' is not a pointer-to-object type Przy wypisywaniu wartości wskazywanej przez wskaźnik void należy rzutować ją na odpowiedni typ #include <stdio.h>
int main(){
int a = 8;
void* ptr = &a;
printf("%d", *(int*)ptr); //Wskaźnik ptr jest rzutowany na wskaźnik typu int przy wypisywaniu
return 0;
}
W tym przypadku ponieważ wskaźnik typu void jest rzutowany na wskaźnik typu int posiada on informację ile miejsca w pamięci powinna zająć wartość wskazywana. Wskaźniki w Assembly x86Wskaźniki w asemblerze działają podobnie. Aby przeprowadzić dereferencję należy najpierw zapisać adres w rejestrze: ; intel
mov eax, adres ; Zapisz adres do rejestru eax
mov eax, dword ptr [eax] ; Zapisz do rejestru eax wartość 4 bajtową z adresu w eax
; at&t
movl adres, %eax ; Zapisz adres do eax
movl (%eax), eax ; Zapisz do eax wartość *(dword*)adres
Można też robić arytmetykę: ; intel
mov eax, dword ptr [eax+5+edx*4] ; eax <- *(dword*)((void*)(eax) + 5 + (void*)(edx)*4)
; at&t
movl 5(%eax,%edx,4), %eax ; to samo w składni at&t
obliczanie z użyciem działań na adresachW asm x86 jest instrukcja, która pozwala na obliczenie adresu i zapisanie go do rejestru. Jest nią ; intel
lea eax, dword ptr [eax * 4 + ecx] ; eax <- eax * 4 + ecx
; at&t
leal (%ecx, %eax, 4), %eax
Zobacz też |