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”).
Adres, pod którym w pamięci znajduje się zmienna, można uzyskać używając jednoargumentowego operatora &
, zatem 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 &
, zatem 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.
Przetestuj ją używając poniższego fragmentu kodu:
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…
Dla powtórzenia przeczytaj uważnie trzeci punkt z tej listy.
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> #define ROZMIAR 4 int main () { 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; }
Jak widzisz, dodanie 1
do wskaźnika na typ short
zwiększa wartość wskaźnika (czyli adres) mniej niż w przypadku wskaźnika na typ double
(bo typ short
zajmuje mniej bajtów pamięci niż typ double
).
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; }
Funkcja korzysta z operatora rzutowania. Operator rzutowania (typ)
umożliwia wyraźne określenie zamiany jednego typu na inny (tj. rzutowania; ang. cast).
W tym przypadku (int)
mówi “potraktuj wartość stojącą po prawej stronie jak wartość typu całkowitego” (co powoduje pominięcie części ułamkowej).
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);
W poniższych zadaniach nie korzystaj z notacji tablicowej []
!
(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.
(poziom trudności: )
Wykonaj zadania w ramach polecenia Umieszczanie bajtów obrazu w tablicy.
(poziom trudności: )
Wyświetl ilość komórek pamięci między zmiennymi v1
i v2
. Nie odejmuj wskaźników (gdyż w tym przypadku nie jest to dozwolone).
#include <stdio.h> #include <stdlib.h> int main () { int v1; double x; int v2; // TODO: uzupelnij return 0; }
Wskazówka: Pamiętaj, że adres to po prostu liczba całkowita.
(poziom trudności: )
Napisz funkcję palindrom()
, która rekurencyjnie sprawdzi, czy dany łańcuch znaków jest palindromem:
int palindrom(char* str);
Funkcja powinna zwrócić wartość 1
, jeśli łańcuch znaków jest palindromem, a w przeciwnym przypadku 0
.
Wskazówka 1: Zapisz słownie rekurencyjną definicję palindromu (analogicznie jak to było np. z silnią czy potęgowaniem). Nie myśl w kategoriach programu w języku C.
Wskazówka 2: Napisz pomocniczą funkcję o nagłówku zbliżonym do funkcji wyswietl()
z zadania DIS – operowanie na zakresie wskaźników będzie pomocne.