Wskaźniki



1. Wskaźniki
Najogólniej mówiąc, wskaźnik jest zmienną, która przechowuje adres innej zmiennej. Język C jest językiem tak zorganizowanym, że często posługuje się wskaźnikami. Są nimi argumenty wielu funkcji bibliotecznych, czy też wartości przez nie zwracane. Co prawda, istnienie wskaźników nie jest konieczne (można by się było bez nich obejść), ale właśnie dzięki nim język C tworzy tak zwarte i efektywne kody. Ponieważ wskaźniki i tablice są blisko ze sobą spokrewnione, przy okazji omawiania wskażników uzupełnimy nieco informacje o tablicach.
Wskaźnikami możemy posłużyć się wobec dowolnej zmiennej podstawowej, tablicy, funkcji, innego wskaźnika, struktury itp.Wskaźniki deklarujemy używając następującej składni:
typ_zmiennej * nazwa_zmiennej
np:
char * wsk1;
int *wsk2;
float *wsk3;
Wskaźniki są 16 - to lub 32 bitową liczbą, która przechowuje adres zmiennej danego typu: i tak czytając powyższe zapisy:
wsk1 jest wskaźnikiem do wskazywania (wskazujący) na zmienną typu char
wsk2 jest zmienną wskazującą na zmienną typy integer
wsk3 wskazuje na zmienną typu flaot



Przypomnijmy sobie na wstępie jak zorganizowana jest pamięć. Jest to tablica kolejno numerowanych lub adresowanych komórek pamięci: można nimi manipulować pojedyńczo lub posługując sie grupami sąsiednich komórek. Każda zmienna ma unikalny adres wskazujący początkowy obszar pamięci zajmowany przez tą zmienną. Ilość pamięci zajmowanej przez zmienną zalęży jak pamiętamy od typu zmiennej. Zamiast odwoływać się do wartości zmiennej, można mieć do niej dostęp poprzez manipulowanie zmiennymi, które zawierają ich adres, czyli wskaźniki. Przydaje się to szczególnie przy manipulowaniu na tablicach, czy strukturach.
Treścią wskaźnika jest informacja, gdzie wskazany obiekt się znajduje, a nie to co się w nim znajduje.
Operator * informuje nas, że mamy do czynienia ze wskaźnikiem. Podobnie jak nawiasy [] informowały nas o tablicach. Sam wskaźnik nazywa się tak jak nazwa_zmiennej. Typami pochodnymi są też referencje i tzw. pola bitowe. Referencja nie jest obiektem (jak np. tablica) i do niej nie ma wskaźników. Do poszczególnych pól bitowych też nie (ponieważ zwiększenie adresu o jeden powoduje przesunięcie się w pamięci o 8 bitów(bajt) a nie o jeden bit).
Należy też pamiętać, że wskaźnik stworzony do wskazywania na obiekty jednego typu nie może wskazywać na inne typy:
int *wsk;
float a;
wsk = &a; //nieprawidłowo.

Operator & jest operatorem adresu i może być stosowany tylko do obiektów zajmujacych pamięć: zmienne, elementy tablic. Nie można go stosować do wyrażeń, stałych i tzw. zmiennych register( o tym później). Pomaga on nadać zadeklarowanemu wskaźnikowi wartość początkową, czyli przypisać go do konkretnego obiektu. Kiedy wskaźnik pokazuje już na konkretnie miejsce możemy odnieść się do tego obiektu na który wskazuje( odczytać jego wartość, lub zapisać coś do niego- obiektu nie wskaźnika).
Operator * jest jak mówiłam operatorem adresowania pośredniego, ale także odwołania pośredniego: zastosowany do wskaźnika daje zawartość obiektu wskazanego przez ten wskaźnik np.:
int *wsk1 ;
float wsk2;
int zmienna1 = 10;
float zmienna2 = 1.2;
wsk1 = &zmienna1;
o a<<"Wskaźnik wsk1 wskazuje na zmiennąestedresie"<<wsk1
<<"zawierającą zmienną = "<<*wsk1
wsk2 = &zmienna2;
cout<<"Wskaźnik wsk2 wskazuje na zmienną o adresie"<<wsk2
<<"zawierającą zmienną = "<<*wsk2
Przypisanie adresu konkretnej zmiennej można skrócić. Zapis:
int *wsk;
int zmienna;
wsk = &zmienna;
jest równoważny:
int zmienna = 100;
int *wsk = &zmienna;
Teraz do zmiennej chcemy wpisać nową wartość:
*wsk = 200;
cout<<zmienna;
Czyli:Do obiektu można coś wpisać albo używając nazwy albo wskaźnika pokazującego na ten obiekt.
Jeśli wsk wskazuje na zmienną całkowitą, to *wsk może wystąpić wszędzie tam gdzie może wystąpic zmienna, a więc np:
int *wsk;
int zmienna = 10;
wsk = &zmienna;
*wsk = *wsk + 2; // zmienna = 12;
Ponieważ operatory * oraz & są silniejsze niż operatory arytmetyczne, to dla wyrażenia:
x = *wsk + 1;
nie trzeba nawiasu: najpierw pobierana jest wartość z adresu wsk i zwiększona o 1 zostaje zapisana do x.
*wsk +=1 ++*wsk (*wsk)++ są równoważne.
W ostatnim przypadku nawiasy są niezbędne bo jednoargumentowe operatory * i ++ wykonywane są od prawej do lewej. Czyli operacja wykonała by zwiększenie wskaźnika o jeden, a nie obiektu.
Ponieważ wskaźniki są zwykłymi zmiennymi można ich używać bez adresowania pośredniego:
wsk = wsk1;
Zapis ten mówi, że teraz wsk będzie wskazywał na to samo co wsk1.

Wskaźnik void
Przy okazji powiemy sobie o wskaźniku void. Jak pamiętamy, deklaracja wskaźnika niosła w sobie dwie informacje: adres jakiegoś miejsca w pamięci, oraz na jakiego typu zmienną pokazuje:int, char float itp.
Możemy jednak zdefiniować wskaźnik, który informacji o typie nie przenosi - jest to tzw. wskaźnik void. Definiujemy go następująco:
void *w;
Służy on najogólniej mówiąc do reprezentowania bloku pamięci np.
void *wsk_blok = malloc(100);
O funkcji malloc mówić będziemy przy okazji omawiania dynamicznej alokacji pamięci. Teraz omawiając zapis powyższy mówimy, że zmienna wsk_blok wskazuje adres 100 bajtowego bloku pamięci.
W związku z tym, że wskaźnik void nie określa typu zmiennej na jaki wskazuje, jest rzeczą oczywistą, że nie można nim posłużyć się do odczytania pamięci. Nie można się nim poruszać po sąsiednich adresach: nie wiemy co ile bajtów poruszać się po pamięci.
Aby móc teraz traktować ten obszar pamięci jako np tablicę znaków czy innych zmiennych, musimy dokonać odpowiednich konwersji do żądanych typów, przy pomocy operatora rzutowania, o którym mówiliśmy na poprzednich wykładach , i tak np:
void wsk_blok = malloc(100);
char *wsk;
// int *wsk1;
.................
wsk_blok = wsk;
// wsk_blok = wsk1;
Zapisy te oznaczają, że teraz wskaźnik typu void wskazuje na to samo, na co wskazuje wskaznik typu char (int).
Wskaźnik każdego typu można przypisać wskaźnikowi typu void, bez konieczności rzutowania.
Przy okazji możemy napomknąć, że operator rzutowania może być stosowany również do wskaźników np:
int *wsk1, *wsk2;
float *wsk;
...............
wsk1 = wsk2; //poprawny zapis
wsk = wsk1; //bład kompilatora, bo chcemy wskaźnikiem do float pokazywać //na int
wsk = (float *)wsk1; //świadomie każemy kompilatorowi to zrobi, kompilacja bez //błędu
Operatora rzutowania musimy użyć, gdy chcemy przypisać wskaźnikowi rzeczywistemy wskaźnik typu void:
float *wsk;
voiod *wsk1;
....................
wsk = wsk1; //błąd
wsk = (float *)wsk1; //poprawnie z operatorem rzutowania
Jest różnica między ANSII C a C++. W języku C niezależnie od tego po której stronie operatora przypisania stał wskaźnik void nie trzeba było używać rzutowania.

1a. Wskaźniki a tablice
Wskaźniki generalnie stosuje sie w 4 przypadkach:
- usprawnienie pracy z tablicami
- w funkcjach mogących zmieniać przychodzące do niej argumenty
- dostęp do specjalnych komórek pamięci
- rezerwacja pamięci
Najpowszechniej na poczatku stosuje się wskaźniki do pracy z tablicami. Zadeklarujmy wskaźnik i tablicę:
int *wsk;
int tab_a[10];
1. instrukcja:
wsk = &tab_a[n];
ustawia wskaźnik na n-tym elemencie tablicy. Operator & jest operatorem adresu. Typ wskaźnika zgadzać się musi z typem tablicy.
2. instrukcja:
wsk = &tab_a[0];
jest równoważna instrukcji:
wsk = tab_a;
i oznacza ustawienie wskaźnika na pierwszy element tablicy(na początek). Zapisy są równoważne, ponieważ jak już wspomnieliśmy, nazwa tablicy stanowi adres jej zerowego (pierwszego) elementu.
Po ustawieniu wskaźnika na n-ty element tablicy:
wsk = tab_a[7];
to przejście do następnego elementu tablicy umożliwia
instrukcja 3 :
wsk = wsk + 1; lub wsk ++;
aby przesunąć się o n elementów w tablicy napiszemy
instrukcję 4 :
wsk + = n;
Prostota zapisu jest zaskakująca. Można zapytac skąd wskaźnik wie, gdzie się przesunąć gdy np: zwiększamy go o 1. Otórz w definicji wskaźnika powiedzieliśmy, że np. wsk = jest wskaźnikiem na int. Stąd kompilator wnioskuje, że aby odnaleźć następny element typu int należy przesunąć się o 2 bajty. (typ int - 2 bajty)








Powyższe rysunki ilustrują mechanizm poruszania się wskaźnika do tablicy dla różnych typów zmiennych. Przepiszemy teraz program z poprzedniego wykładu tak, aby posługiwał się wskaźnikami, a nie klasycznym odwoływaniem sie do n-tego elementu tablicy:
Przykład 1.
#include<iostream.h>
#include<stdio.h>
#include<conio.h>
main()
{
int *wsk_a, *wsk_b,i, j,pom;
int a[4][4];
int b[4]={0,0,0,0};
clrscr();
wsk_a = a; //inicjowanie wskaźników
wsk_b = b;

for(i = 0;i<4;i++)
for(j = 0;j<4;j++)
{
printf("a[%i][%i] = ",i,j); //wprowadzenie danych do tablicy
scanf("%i",wsk_a++);
/* *wsk_a++ = i+1; */
}
printf("Tablica a:
");
for(i = 0,wsk_a = a;i<4;i++)
for(j = 0;j<4;j++) //Wydruk tablicy wprowadzonej
printf("
%i",*wsk_a++);
for(wsk_a = a,i = 0;i<4;i++)
{
pom = 0;
for(j = 0;j<4;j++)
pom += *wsk_a++; //sumowanie elementów wiersza
*wsk_b++ = pom;
}
for(j = 0,wsk_b = b;j<4;j++) //wydruk wyników
printf("%i ",*wsk_b++);
while(!kbhit());
return 0;
}
Jak widać, posłużenie się wskaźnikami nic szczególnego nam nie dało. Pozornie tylko. W efekcjie otrzymujemy szybszy od poprzedniego program. Zapis np a[5] powoduje żmudne liczenie adresu czygo przy wskaźnikach nie ma.
Pamiętajmy!!!
Mimo, że możemy zapiasć:
wsk = tab_a;
to o ile możemy postąpić:
wsk++;
to nie możemy napisać:
tab_a++;
Różnicą między wskaźnikiem a nazwą tablicy jest to, że wskaźnik jest obiektem w pamięci, w związku z tym można np. ustalić jego adres, natomiast nazwa tablicy nie jest obiektem i na niej nie można przeprowadzać żadnych operacji.
Dla tak zadeklarowanego wskaźnika:
int *wsk;
adresem tego wskaźnika jest wartość wyrażenia:
&wsk;

1b.Arytmetyka wskaźników
1.
Możemy dodawać i odejmować liczby całkowite do wskaźników tak, aby w potrzebny sposób przesuwać je po tablicy. Operacje te nie są sprawdzane przez kompilator, i wtedy możemy przesunąć wskaźnik poza zadeklarowaną tablicę i np. zniszczyć istniejące tam potrzebne dane. Błędy takie są najtrudniejsze do wykrycia, dlatego przy przesuwaniu wskaźników należy zachować daleko idącą ostrożność.
2.
Możemy odjąć dwa wskaźniki od siebie:
wsk_a - wsk_b
Gdy pokazują one na różne elementy tej samej tablicy to wynikiem takiej operacji jest liczba dzielących je elementów. Liczba może byc ze znakiem - lub +.
3.
Wskaźniki można ze sobą porównać. Do tego celu służą nam operatory:
== != < > <= >=
Dla dwóch wskaźników:
int *wsk1, *wsk2;
wyrażenie : wsk1 = wsk2 oznacza, że wskazują one na ten sam obiekt.
if(wsk1 == wsk2)
cout<<"Oba wskaźniki pokazują na ten sam obiekt";
Jeśli wskaźniki wskazują na jakieś elementy tej samej tablicy, to wyrażenie wsk1 < wsk2 oznacza, że wsk1 wskazuje na element tablicy o mniejszym indeksie.
4.
Każdy wskaźnik można porównać z adresem 0 zwanym NULL. Takie ustawienie wskaźnika:
wsk = 0; //lub wsk = NULL
informuje, że wskaźnik nie pokazuje na nic konkretnego (czasami niektóre funkcje biblioteczne zwracają wskaźnik NULL (null pointer) np funkcja gets(string)). Potem możemy łatwo sprawdzać:
if(wsk == 0) // if(wsk ==NULL) if(!wsk)
............. .............. .......

1c. Inicjowanie wskaźników
Przed rozpoczęciem pracy ze wskaźnikami należy pamiętać o tym , że przed ich pierwszym użyciem muszą być one ustawione.
W tym punkcie zbierzemy wszystkie sposoby ustawiania wskaźników i te o których mówilismy oraz te o których nie wspominaliśmy.
1. tak aby wskazywał na konkretny obiekt:
wsk = &obiekt;
2. można ustawić go na to samo na co wskazuje inny wskaźnik:
wsk = wsk1;
3. ustawić wskaźnik na początek jakiejś tablicy:
wsk = tab_a; // wsk = &tab_a[0];
4.ustawić wskaźnik na jakąś funkcję.
5.ustwić wskaźnik na zarezerwowany dynamicznie obszar
6. ustawić wskaźnik na konkretny adres którego np. zawartość chcemy sprawdzić
7. ustawienie wskaźnika na string
wsk = "to jest string";

1.d Tablice wskaźników
Pamiętamy, że tablica to ciągły obszar w pamięci, w którym przechowywany jest ciąg zmiennych tego samego typu. Skoro moga być tablice int , float , char , to dlaczego nie miałoby być tablic wskaźników. W końcu adres to też liczba. Np.:
float *wsk1[3];
int *wsk2[10];
powyższe zapisy czytamy:
- wsk1 jest tablicą trzy elementową wskaźników do float
- wsk2 jest tablicą 10 -elementową wskaźników na int.

1.e Wskaźniki a stringi
Wielokrotnie mówiliśmy,że string to tablica znaków zakończona znakiem NULL.
Jeśli jakiś wskaźnik ma pokazywac na ciąg znaków, to można go zadeklarować jako:
char *text;
i zainicjować:
text = "To jest próbny tekst";
lub razem w jednej linii:
char *text = "to jest próbny tekst";
Zapis 1 nie oznacza kopiowania, jest to przypisanie wskaźnika konkretnemu stringowi. W języku C nie ma instrukcji do obsługi ciągu znaków jako całości.
Porównajmy zapis:
char tab_text [] = "To jest próbny tekst";
char *text = "to jest próbny tekst";
Pierwsze jest tablicą, poszczególne znaki tablicy można zmieniać, ale nazwa tab_text zawsze będzie odwołaniem do tego samego miejsca w pamięci. Z drugiej strony text jest wskaźnikiem zainicjowanym tak, aby wskazywał na string. Wskaźnik można później zmieniać, tak aby wskazywał na cokolwiek, ale zmiana w stałej napisowej ma skutek nieokreślony




W jaki sposób można wykorzystać wskaźniki do stringu, zademonstrujemy na przykładzie funkcji append_string, dodającej dwa stringi do siebie:

Przykład 2.
append_string(char *string1, char *string2)
{
//znajdywanie końca stringu2 do ktorego dolączymy string1
while(*string2 != NULL)
string2++;
//dołączanie stringów
while((*string2 = *string1) != NULL)
{
string1++;
string2++;
}
}

W praktyce funkcję pisze się nieco inaczej. Można to zrobić tak:


append_string(char *string1, char *string2)
{
//znajdywanie końca stringu2 do ktorego dolączymy string1
while(*string2++)
;
//dołączanie stringów
while(*string2++ = *string1++)
;
}
Zwiększanie wskaźników string1 i string2 przeniesiono do części warunkowej. Jak mówiliśmy wcześniej, wartością operacji: *string1++ jest znak na który wskazywał wskaźnik przed zwiększeniem. Znak ze stringu 1 wstawiany jest na końcu stringu2 i na koniec dołączany jest znak NULL. Porównanie ze znakiem '

Wróć do strony głównej