This is an old revision of the document!
Wskaźnik (pointer) to zmienna, której wartość jest adresem w pamięci (podobnie jak np. wartością zmiennej typu int jest liczba całkowita).
Deklaracja wskaźnika:
<typ_wskazywanego_obiektu_danych>* nazwa_wskaznika;
Znak *
w tym przypadku informuje kompilator, że chodzi o zmienną wskaźnikową, a nie zwykłą zmienną.
Przykład:
char* pc;
Mówimy, że pc
to “wskaźnik do zmiennej typu char” (bądź w skrócie: “wskaźnik do char”).
Wskaźnikowi przypisujemy adres w następujący sposób:
int n = 11; int* ptr = &n;
Mówimy, że wskaźnik ptr
“wskazuje na” zmienną n
.
Oto schemat pokazujący, jak wygląda pamięć komputera po takiej operacji:
Nazwa tablicy jest równocześnie adresem jej pierwszego elementu – stąd jeśli
tab
jest tablicą, prawdziwe jest następujące stwierdzenie: tab == &tab[0]
.
Do wypisania wartości wskaźnika – a więc adresu w pamięci – można użyć specyfikatora konwersji %p
, który wyświetla wartość w kodzie szesnastkowym (taka zwykło się podawać adresy):
int n = 5; int* ptr = &n; printf("n: wartosc = %d , adres = %p\n", n, &n); printf("ptr: wartosc = %d badz rownowaznie %p\n", ptr, ptr);
Oczywiście, ponieważ adres to po prostu liczba całkowita, można równie dobrze użyć specyfikatora %d
.
Aby uzyskać wartość przechowywaną pod danym adresem, korzystamy z jednoargumentowego operatora dereferencji *
(dereference), zwanego w żargonie operatorem wyłuskiwania:
int n = 3; int* p_int = &n; printf("adres = %p , wartosc = %d\n", p_int, *p_int);
Nie pomyl unarnego operatora dereferencji z binarnym (dwuargumentowym) operatorem arytmetycznym mnożenia – oba oznaczane są tym samym symbolem
*
.
Operator dereferencji *
jest operatorem odwrotnym dla operatora adresu &
: p = *&q;
jest równoważne p = q;
Wartość wskazywaną można zmienić w następujący sposób:
int n = 1; int* ptr = &n; *ptr = 3; // wartosc `n` wynosi teraz 3
…i jeszcze jeden przykład na zmianę przeprowadzoną w identyczny sposób:
int i = 1; int j = 2; int *p, *q; p = &i; q = p; // obie zmienne `p` i `q` wskazuja na `i` q = &j; // teraz `p` i `q` wskazuja na rozne zmienne *q = *p; // przypisz wartosc wskazywana przez `p` w miejsce wskazywane // przez `q` - wartosc `j` wynosi teraz 1
Przypisanie
q = p
nie jest tym samym, co *q = *p
!
Wskaźnik nazywamy pustym (null pointer), gdy jego wartość wynosi 0. Należy pamiętać, że wskaźnik pusty (zerowy) nie może nigdy wskazywać danych uznawanych za poprawne. Aby zainicjalizować wskaźnik pusty wystarczy przypisać mu 0. (To będzie nam później przydatne…)
Napisz funkcję zamien()
, która umożliwi zamianę ze sobą wartości dwóch zmiennych typu całkowitego:
int a = 4; int b = 5; zamien(/* odpowiednie argumenty */); if (a == 5 && b == 4) { printf("OK\n"); } else { printf("Cos poszlo nie tak...\n"); }
Jak się zapewne domyślasz, konieczne będzie użycie wskaźników…
Komputery PC są adresowalne bajtowo (tzn. wszystkie bajty pamięci są ponumerowane w rosnącej kolejności, a zatem każda lokalizacja w pamięci ma swój niepowtarzalny adres). Adresem obiektu zajmującego kilka bajtów (np. typu int
) jest adres (numer) pierwszego bajtu.
Dodanie liczby całkowitej do wskaźnika zwiększa jego wartość o odpowiednią wielokrotność rozmiaru (w bajtach) wskazywanego obiektu (analogicznie: odejmowanie zmniejsza wartość):
#include <stdio.h> #include <stdlib.h> int main () { const int ROZMIAR = 4; // stala - wartosc, ktora nie ulega zmianie // podczas wykonywania programu short tab_short[ROZMIAR]; double tab_double[ROZMIAR]; short* ptr_short = tab_short; double* ptr_double = tab_double; int index; printf("%25s %10s\n", "short", "double"); for (index = 0; index < ROZMIAR; index = index + 1) { printf("wskazniki + %d: %10p %10p\n", index, ptr_short + index, ptr_double + index); } return 0; }
Poprzez odjęcie jednego wskaźnika od drugiego otrzymuje się przesunięcie między wskaźnikami, wyrażone w jednostce o rozmiarze typu (np. dla wskaźników typu int
, jeśli p2 – p1 == 2
, to między p1
i p2
znajdują się dwie wartości typu int
– a nie dwa bajty!). Przesunięcie może mieć wartość ujemną.
#include <stdio.h> #include <stdlib.h> int main () { int tab[] = {1, 2, 3}; int* p1 = tab; int* p2 = tab + 2; printf("p1 = %p\n" "p2 = %p\n" "p1 - p2 = %d\n" "p2 - p1 = %d\n", p1, p2, p1 - p2, p2 - p1); return 0; }
Operacja jest przydatna do obliczania wielkości tablicy (długości łańcucha znaków), jeżeli mamy wskaźnik na jej pierwszy i ostatni element.
Komputer nie sprawdza do jakiej zmiennej należy pamięć, po której aktualnie piszemy (dopóki znajdujemy się w pamięci naszego programu)!
Dla powtórzenia – nazwa tablicy jest równocześnie adresem jej pierwszego elementu, stąd:
tab + 2 == &tab[2]
– ten sam adres*(tab + 2) == tab[2]
– ta sama wartość
Ze względu na kolejność operatorów
*(tab + 2)
różni się od *tab + 2
. Odpowiednio, w pierwszym przypadku otrzymujemy wartość trzeciego elementu, a w drugim – dodajemy 2 do wartości pierwszego elementu.
Z punktu widzenia C notacja tablicowa i wskaźnikowa są równoważne, ale użycie notacji tablicowej pokazuje intencje programisty.
Komputer nie sprawdza, czy po wykonaniu operacji arytmetycznej na wskaźniku wciąż znajdujemy się w obszarze pamięci tablicy! Programista musi o to zadbać sam.
Nie można (tzn. standard języka C tego nie definiuje) skonstruować wskaźnika wskazującego gdzieś poza zadeklarowaną tablicę. Wyjątkiem jest wskaźnik na obiekt tuż za ostatnim elementem tablicy (one past last).
Przekazywanie wskaźników do zmiennych jako parametrów funkcji umożliwia zmienianie wartości tych zmiennych podczas wykonywania funkcji.
Do funkcji przekazujemy wartości zmiennej, chyba że występuje konieczność zmiany jej wartości wewnątrz funkcji (wtedy przekazujemy wskaźnik).
W przypadku tablic musimy przekazać wskaźnik – to wymóg języka związany z wydajnością (w przeciwnym razie trzeba by zarezerwować tyle miejsca w pamięci, by zmieścić kopię całej tablicy).
Oto przykład funkcji rozkładającej liczbę rzeczywistą na część całkowitą i ułamkową:
void rozloz(double x, int* czesc_calkowita, double* czesc_ulamkowa) { *czesc_calkowita = (int) x; *czesc_ulamkowa = x - *czesc_calkowita; }
Jej wywołanie wygląda następująco:
double x = 3.14; int i; double f; rozloz(x, &i, &f); printf("%.2f = %d + %.2f\n", x, i, f); // 3.14 = 3 + 0.14
Teraz pewnie rozumiesz, dlaczego przekazując zmienną do funkcji scanf()
poprzedzaliśmy ją operatorem &
– chcieliśmy przekazać jej adres w pamięci, a nie przechowywaną wartość. W ten sposób funkcja scanf()
mogła zmienić wartość zmiennej zgodnie z wczytanymi wartościami:
int n; scanf("%d", &n);
(poziom trudności: )
Napisz funkcję dlugosc()
, która obliczy długość łańcucha znaków. Nie korzystaj z funkcji strlen()
:
int dlugosc(char* str);
Przetestuj kod następującym programem:
#include <stdio.h> #include <stdlib.h> int dlugosc(char* str) { // TODO: uzupelnij } int main () { char str[] = "Jaka mam dlugosc?"; // lancuch dlugosci 17 znakow printf("Dlugosc lancucha \"%s\" wynosi %d.\n", str, dlugosc(str)); return 0; }
Wskazówka: Pamiętaj, że łańcuch znaków kończy się znakiem zerowym – znalezienie położenia tego znaku wystarczy do określenia długości całego łańcucha.
(poziom trudności: )
Napisz funkcję, która przy pomocy wskaźników wyświetli dane znajdujące się w tablicy liczb całkowitych.
Użyj tablicy bezwymiarowej, a rozmiar określ za pomocą operatora sizeof().
Deklaracja funkcji powinna wyglądać następująco:
void wyswietl(int* poczatek, int* koniec);
gdzie poczatek
to wskaźnik na pierwszy element tablicy, a koniec
– wskaźnik na obiekt tuż za ostatnim elementem tablicy.