5.3 Arytmetyka wskaźników

Do wskaźników można dodawać (i odejmować) liczby całkowite. Typ wskaźnikowy nie jest jednak typem całkowitym, takie dodawanie jest zdefiniowane w specjalny sposób.

Załóżmy, że p jest wskaźnikiem typu Typ* wskazującym na element tablicy, a zmienna shift jest zmienną typu całkowitego.

Wartością wyrażenia p+shift jest wtedy adres zawarty w zmiennej p powiększony o  shift wielokrotności wymiaru zmiennej typu Typ (czyli sizeof(Typ)).

Jeśli zatem shift wynosi 2, a  sizeof(Typ) wynosi 4 (jak dla int), wartością p+shift jest adres zawarty w  p zwiększony o 8 (= 2⋅4). Jeśli (przy tej samej wartości zmiennej shift) sizeof(Typ) wynosiłby 8, jak dla typu double, to wartością p+shift byłby adres zawarty w  p powiększony o 16 (= 2⋅8). Właśnie, między innymi, ze względu na arytmetykę wskaźników deklarując zmienną wskaźnikową trzeba określić, na jakiego typu zmienne będzie ona wskazywać. Z tego też powodu nie można stosować arytmetyki wskaźników do wskaźników generycznych (typu void*) — brak określonego typu powoduje, że nie wiadomo by było, o ile należy zwiększyć taki adres.

Spójrzmy na następujący przykład:


P21: arytmwsk.cpp     Arytmetyka wskaźników

      1.  #include <iostream>
      2.  using namespace std;
      3.  
      4.  int main() {
      5.      int tab[] = {11,22,33,44,55}, i = 3, *p, *q;
      6.  
      7.      p = &tab[0] + 3;                        
      8.      cout << "*p     = " << *p << endl;
      9.  
     10.      p = p - 2;                              
     11.      cout << "*p     = " << *p << endl;
     12.  
     13.      q = tab;                                
     14.      cout << "*(q+2) = " << *(q+2) << endl;
     15.      cout << "q[2]   = " << q[2]   << endl;
     16.  
     17.      cout << "q[i]   = " << q[i] << endl;    
     18.      cout << "i[q]   = " << i[q] << endl;    
     19.  }

którego rezultatem jest

    *p     = 44
    *p     = 22
    *(q+2) = 33
    q[2]   = 33
    q[i]   = 44
    i[q]   = 44
W linii wartością wyrażenia &tab[0] jest adres pierwszego elementu tablicy (czyli elementu tab[0] o wartości liczbowej  11). Zauważmy tu, że

wartość wyrażenia &tab[0] jest dokładnie tym samym, co wartość wyrażenia tab.

Po dodaniu do tej wartości liczby całkowitej 3 otrzymujemy ten sam adres, ale przesunięty o trzy wielokrotności długości jednej zmiennej typu int, czyli adres elementu czwartego (o indeksie 3 i wartości 44). Dlatego wypisując na ekranie wartość *p otrzymamy 44.

W linii  wartość p pomniejszamy o 2; adres zawarty w  p zostaje zatem pomniejszony o dwie wielokrotności długości jednej zmiennej typu int, a zatem jest to teraz adres elementu drugiego (o indeksie 1) i wartości liczbowej 22.

W linii  wartość tab, czyli adres elementu tab[0], wpisujemy do zmiennej q typu int*. Zastanówmy się, czym jest *(q+2) z następnej linii. Ponieważ q jest wskaźnikiem wskazującym na zmienną tab[0], więc dodanie 2 odpowiada adresowi powiększonemu o dwie długości zmiennej typu int. Adres ten zatem to adres elementu tablicy o indeksie 2. Operator dereferencji (gwiazdka) wyłuska wartość zmiennej wskazywanej, czyli 33. Zauważmy, że w takim razie dostaniemy dokładnie to samo, co daje wyrażenie tab[2] — wartość elementu tablicy o indeksie 2.

Ogólnie, jeśli p jest wskaźnikiem, a  i ma wartość całkowitą, to

wyrażenie p[i] jest dokładnie równoważne *(p+i).

W rzeczywistości forma *(p+i) jest formą podstawową. Zapis p[i] jest tylko ułatwieniem dla programisty: takie wyrażenie zostanie przez kompilator zamienione na formę ' *(p + i)' jeszcze przed dalszą analizą. Spójrzmy na przykład na dwie ostatnie linie. W linii  wyrażenie q[i] ma sens *(q+i). Ale w linii  mamy wyrażenie i[q]. Zmienna i nie jest tu wskaźnikiem, tylko zmienną całkowitą; z kolei rolę indeksu pełni tu wskaźnik. A mimo to jest to wyrażenie dziwne, ale jak najbardziej legalne: zostanie przekształcone do formy *(i+q), a to jest prawidłowe: z punktu widzenia arytmetyki wskaźników jest ono równoważne wyrażeniu *(q+i), a zatem również wyrażeniu q[i].

Rozpatrzmy jeszcze następujący przykład, gdzie zastosowano konwersje typów wskaźnikowych:


P22: littlebig.cpp     Konwersje wskaźników

      1.  #include <iostream>
      2.  using namespace std;
      3.  
      4.  int main() {
      5.      // starszy bajt: 'a'; młodszy bajt: 'b'
      6.      short sh = 'b'+256*'a';
      7.  
      8.      void *v = static_cast<void*>(&sh);
      9.      char *c = static_cast<char*>(v);
     10.      cout << "Kolejność w pamięci: najpierw "
     11.           << c[0] << " potem " << c[1] << endl;
     12.  }

Tworzymy tu dwubajtową zmienną typu short i wpisujemy tam 'b'+256*'a', a zatem liczbę, w której starszym bajtem jest kod ASCII litery 'a' (czyli 97), a młodszym bajtem jest kod ASCII litery 'b' (czyli 98). Adres tej zmiennej konwertujemy najpierw do typu void*, a potem do typu char*. Użyty tu operator static_cast zostanie wyjaśniony w rozdziale o konwersjach . Po tych operacjach w zmiennej c zawarty jest adres początkowego bajtu z pary bajtów tworzących sh, przy czym typem c jest char* (przypomnijmy, że zmienne typu char są jednobajtowe). Możemy zatem „zajrzeć do wnętrza” zmiennej sh traktując jej kolejne bajty jako kolejne elementy tablicy c. Pytanie jest, czy bajtem wskazywanym przez c jest starszy czy młodszy bajt liczby sh. Program drukuje

    Kolejność w pamięci: najpierw b potem a
co świadczy o tym, że liczby zapisywane są w kolejnych komórkach pamięci od bajtu najmłodszego do najstarszego. Jest to tzw. kolejność little endian. Mogliśmy też dostać rezultat
    Kolejność w pamięci: najpierw a potem b
co świadczyłoby o tym, że pracujemy w architekturze big endian (terminy pochodzą z Podróży Guliwera Jonathana Swifta). Warto wiedzieć, że standardem przy przesyłaniu danych przez sieć jest właśnie kolejność big endian.


Wracając do dyskusji wskaźników zauważmy, że nie ma sensu przesuwanie, czyli dodawanie wartości całkowitych, dla wskaźników generycznych (void*). Nie wiadomo by bowiem było, o ile bajtów ma nastąpić przesunięcie; wskaźniki takie przechowują surowy adres i nie są związane z żadnym typem danych, który określiłby wielkość pojedynczej „porcji” danych.

Podobnie nie wolno przesuwać wskaźników funkcyjnych, o których powiemy w rozdziale o wskaźnikach funkcyjnych .

Ma natomiast sens odejmowanie wskaźników: jeśli p1p2 wskazują na dwa elementy tej samej tablicy tab o indeksach odpowiednio i1i2, to wyrażenie ' p2-p1' jest równe „odległości” między adresami zmiennych wskazywanych przez p1p2 mierzonej w jednostkach równych długości pojedynczej zmiennej w tablicy. A zatem ' p2-p1' ma tę samą wartość co ' i2-i1'.


Dodawanie wskaźników żadnego sensu nie ma.

T.R. Werner, 23 lutego 2019; 23:59