User Tools

Site Tools


dydaktyka:cprog:2016:pointers

This is an old revision of the document!


Wskaźniki

Wskaźnik – czym to się je?

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

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”).

Inicjalizacja

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].

Wyświetlanie

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.

Uzyskiwanie i zmiana wartości wskazywanej

Aby uzyskać wartość przechowywaną pod danym adresem, korzystamy z jednoargumentowego operatora dereferencji * (ang. 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 jednoargumentowego operatora dereferencji z 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 pusty

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…)

Zadania

Zadanie CHG

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.

Arytmetyka 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>
 
#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)!

Wskaźniki a tablice

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).

Wskaźniki a funkcje

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);

Zadania podsumowujące

W poniższych zadaniach nie korzystaj z notacji tablicowej []!

Zadanie LEN

(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.

Zadanie DIS

(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.

Fotoszop

(poziom trudności: )

Wykonaj zadania w ramach polecenia Umieszczanie bajtów obrazu w tablicy.

Zadanie OFF

(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.

Zadanie PAL

(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.

dydaktyka/cprog/2016/pointers.1483430146.txt.gz · Last modified: 2020/03/25 11:46 (external edit)