15.6 Wskaźniki do składowych

Obiekty tej samej klasy zawsze mają tę samą długość w pamięci. Poszczególne składowe w każdym obiekcie są wewnątrz tego obiektu usytuowane jednakowo, czyli ich początek jest zawsze tak samo przesunięty względem początku obiektu. Pozwala to na istnienie specjalnego rodzaju wskaźników, wskaźników do składowych, których wartością nie jest bezwzględny adres składowej obiektu, ale jej przesunięcie względem początku obiektu. Gdy znany jest adres obiektu (jego początku) oraz ten właśnie wskaźnik do składowej, system może wyliczyć z tych informacji adres bezwzględny tej składowej.

Wskaźnik do składowej typu Typ w klasie Klasa definiuje się tak:

       Typ Klasa::*wskaz;
i oznacza on, że wartością zmiennej wskaz będzie przesunięcie pewnej publicznejniestatycznej składowej typu Typ w obiektach klasy Klasa. Widać, że definicja ta różni się od definicji zwykłego wskaźnika obecnością specyfikacji zakresu klasy (w naszym przypadku ' Klasa::'). Tak zdefiniowany wskaźnik do składowej nie ma na razie żadnej sensownej wartości.

Przypuśćmy, że klasa Klasa ma dwa publiczne, niestatyczne pola, pole1pole2, oba typu Typ. Można wtedy przypisać

       wskaz = &Klasa::pole1;
albo
       wskaz = &Klasa::pole2;
Symbol ' &' nie oznacza tu pobrania bezwzględnego adresu jakiegoś obiektu — przypisanie to oznacza, że wartością wskaźnika do składowej wskaz będzie przesunięcie składowej pole1 (albo, w drugim przypadku, pole2) względem początku dowolnego obiektu klasy Klasa. Zauważmy, że to przesunięcie jest bezużyteczne, jeśli nie wiemy, o jaki obiekt chodzi, bo nie wiadomo względem czego przesuwać. Po takiej definicji, dla konkretnego obiektu obiekt klasy Klasa można teraz odwołać się do jego składowej pole1 poprzez operator ' .*':
       obiekt.*wskaz
Tutaj obiekt wskazuje, o który obiekt chodzi, a  wskaz mówi gdzie, w obrębie obiektu, szukać odpowiedniej składowej. Gdybyśmy dysponowali nie nazwą obiektu, ale wskaźnika do niego,
       Klasa* wsk_do_obiektu = &obiekt;
to do tej samej składowej tego obiektu moglibyśmy się odnieść za pomocą operatora ' ->*'
       wskaznik_do_obiektu->*wskaz
W podobny sposób można definiować wskaźniki do składowych metod. Teraz trudno to sobie wyobrazić jako przesunięcie względem początku obiektu, bo metody nie są zawarte fizycznie w obiektach. Jest problemem implementatorów kompilatorów C++, jak to zostanie rozwiązane: z punktu widzenia programisty zasada jest taka sama jak dla pól klasy.

Jeśli na przykład w klasie Klasa są dwie metody, obie typu double double,

       double fun1(double);
       double fun2(double);
to wskaźnik do składowej wf mogący wskazywać na jedną z tych metod można zdefiniować tak (uwaga na nawiasy!):
       double (Klasa::*wf)(double);
Czytamy: wf jest wskaźnikiem do składowej w klasie Klasa wskazującym na metodę (publiczną i niestatyczną) pobierającą double i zwracającą double (patrz rozdział o wskaźnikach funkcyjnych ). I znowu, po takiej definicji wskaźnik wf nie wskazuje na nic sensownego. Możemy teraz dokonać przypisania
       wf = Klasa::fun1;
i teraz wskaźnik wf będzie wskazywał na metodę fun1. Zauważmy, że w definicji podajemy tylko nazwę metody — nie ma nawiasów, które oznaczałyby jej wywołanie. Ponieważ jest to metoda, aby użyć tego wskaźnika i poprzez niego tę metodę wywołać, musimy określić, o który obiekt nam chodzi, a więc na rzecz którego obiektu ma nastąpić wywołanie. Jak dla pól, możemy to zrobić za pomocą operatora ' .*' lub ' ->*', czyli:
       (obiekt.*wf)(5.5)
albo, jeśli dysponujemy wskaźnikiem do obiektu,
       (wskaznik_do_obiektu->*wf)(5.5)
Nawiasy są tu konieczne, ze względu na priorytet operatorów.


Często używa się takich wskaźników do wskazywania metod klasy przy projektowaniu różnego rodzaju menu. Definiuje się wtedy tablicę takich wskaźników, które wskazują na różne metody wywoływane po dokonaniu pewnego wyboru przez użytkownika. Na przykład:

       double (Klasa::*wf[8])(double);
       Klasa menu;
       // ...
       wf[0] = Klasa::fun1;
       wf[1] = Klasa::fun2;
       // ...
       cin >> k;
       (menu.*wf[k])( argument );
       // ...
Przyjrzyjmy się następującemu programowi:


P123: wsklas.cpp     Wskaźniki do składowych

      1.  #include <iostream>
      2.  #include <cmath>
      3.  using namespace std;
      4.  
      5.  struct Punkt {
      6.      double x, y;
      7.      Punkt(double x = 0, double y = 0)
      8.          : x(x), y(y)
      9.      { }
     10.      double r2() { return  x*x + y*y; }
     11.      double dd() { return sqrt(r2()); }
     12.  };
     13.  
     14.  int main() {
     15.      double  Punkt::*wi[2];
     16.      double (Punkt::*wf[2])();
     17.  
     18.      wi[0] = &Punkt::x;
     19.      wi[1] = &Punkt::y;
     20.  
     21.      wf[0] = &Punkt::r2;
     22.      wf[1] = &Punkt::dd;
     23.  
     24.      Punkt P(3,4), *p = &P;
     25.  
     26.      cout << " P.*wi[0]     = " <<  P.*wi[0]     << endl;
     27.      cout << " P.*wi[1]     = " <<  P.*wi[1]     << endl;
     28.      cout << "(P.*wf[0])()  = " << (P.*wf[0])()  << endl;
     29.      cout << "(P.*wf[1])()  = " << (P.*wf[1])()  << endl;
     30.  
     31.      cout << endl;
     32.  
     33.      cout << " p->*wi[0]    = " <<  p->*wi[0]    << endl;
     34.      cout << " p->*wi[1]    = " <<  p->*wi[1]    << endl;
     35.      cout << "(p->*wf[0])() = " << (p->*wf[0])() << endl;
     36.      cout << "(p->*wf[1])() = " << (p->*wf[1])() << endl;
     37.  }

Definiujemy tu klasę z dwoma polami typu double i dwoma metodami typu double double. W funkcji main definiujemy dwie tablice wskaźników do składowych: tablicę wi wskaźników do pól i tablicę wf wskaźników do metod. W liniach 18-22 przypisujemy im wartości, a następnie używamy ich w liniach 26-36 ilustrując to, o czym mówiliśmy wyżej. Wydruk z programu

     P.*wi[0]     = 3
     P.*wi[1]     = 4
    (P.*wf[0])()  = 25
    (P.*wf[1])()  = 5

     p->*wi[0]    = 3
     p->*wi[1]    = 4
    (p->*wf[0])() = 25
    (p->*wf[1])() = 5
przekonuje nas, że wszystko działa zgodnie z przewidywaniem.

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