14.2 Dostępność składowych

Wszystkie składowe klasy są widoczne wewnątrz klasy. Oznacza to, że mają do nich dostęp funkcje (metody) zadeklarowane jako funkcje składowe danej klasy. Mogą jednak mieć różny poziom dostępności z zewnątrz, a więc z funkcji które nie sa skłądowymi danej klasy: poziom dostępności jest określany jednym ze słów kluczowych: public, private, lub protected.. W odróżnieniu od Javy, nie stawia się go jednak przed każdą ze składowych osobno, a definicję klasy dzieli się na tzw. sekcje: każda sekcja rozpoczyna się od jednego z tych słów kluczowych z następującym po nim dwukropkiem. Sekcja rozciąga się do końca definicji klasy lub do rozpoczęcia innej sekcji, na przykład:

      1.      class Klasa {
      2.          int s1;
      3.      public:
      4.          int s2;
      5.          double d2;
      6.      private:
      7.          double s3;
      8.          void fun3(int,double);
      9.      public:
     10.          int s4;
     11.          char c4;
     12.      };
Zauważmy, że pole s1 zostało zdefiniowane przed pojawieniem się jakiegokolwiek specyfikatora poziomu dostępności. Przyjmuje się wtedy dostępność domyślną, według następującej zasady:

Klasy można definiować w C++ za pomocą słowa kluczowego class lub struct. Jeśli użyliśmy słowa class, to domyślnie składowe są prywatne (private), jeśli użyliśmy słowa struct, to domyślnie składowe są publiczne (public). Innych różnic między klasami i strukturami w C++ nie ma!

Tak więc w powyższym przykładzie występują cztery sekcje: pierwsz i trzecia private a druga i czwarta public. Tak więc prywatne są pola s1, s3 i funkcja (metoda) fun3, natomiast publiczne s2, d2, s4c4. Jak widzimy, w definicji klasy (struktury) może być wiele sekcji publicznych czy prywatnych.

Pod względem dostępności składowe dzielą się zatem na:

Te same zasady dotyczą nie tylko składowych, ale i innych nazw (jak na przykład nazw typów czy aliasów definiowanych za pomocą typedef) wprowadzanych w zakresie klasy. Wszystkie one należą do przestrzeni nazw klasy, tak więc spoza klasy trzeba się do nich odwoływać albo poprzez obiekty tej klasy (składowe niestatyczne), albo poprzez kwalifikację nazw nazwą klasy przy użyciu operatora zakresu ' ::' (składowe statyczne, nazwy typów zdefiniowanych w zakresie klasy, aliasy zdefiniowane za pomocą typedef).

Zauważmy, że ograniczenie dostępności dotyczy nazw, a nie na przykład obszarów pamięci zajmowanych przez składowe prywatne.

Rozpatrzmy króciutki program


P101: pozdro.cpp     Dostępność składowych

      1.  #include <iostream>
      2.  using namespace std;
      3.  
      4.  class Pozdro {
      5.      int k1;                            
      6.  public:
      7.      enum Kraj { PL, DE, FR };
      8.      int k2;                            
      9.      void fun(Kraj kraj) {
     10.          switch (kraj) {
     11.              case PL:
     12.                  cout << "Dzien dobry\n"; k1 = 1; break;
     13.              case DE:
     14.                  cout << "Guten Tag\n";   k1 = 2; break;
     15.              case FR:
     16.                  cout << "Bonjour\n";     k1 = 3; break;
     17.          }
     18.      }
     19.  };
     20.  
     21.  int main() {
     22.      Pozdro dd;                         
     23.  
     24.      dd.fun(Pozdro::DE);                
     25.  
     26.      int *pk1 = &dd.k2 - 1;             
     27.  
     28.      cout << "sizeof(dd) = " << sizeof(dd) << endl;   
     29.      cout << "dd.k1      = " << *pk1       << endl;   
     30.  }

Definiujemy tu klasę Pozdro. Pole k1 jest prywatne (), pole k2 (), definicja wyliczenia Kraj i metoda (funkcja) fun są publiczne. W programie głównym tworzymy obiekt dd tej klasy (). Zauważmy, że składnia jest taka jak przy tworzeniu zwykłej zmiennej typu int — nazwa typu i nazwa wprowadzanej zmiennej.

W linii  wywołujemy na jego rzecz metodę fun kwalifikując nazwę funkcji nazwą obiektu — bardziej szczegółowo omówimy tę kwestię poniżej. Argument jest typu Kraj, ale ten typ (wyliczeniowy) został zdefiniowany w klasie Pozdro; jego nazwa nie jest widoczna poza zasięgiem klasy. Nie możemy więc użyć po prostu nazwy DE oznaczającej jeden z elementów wyliczenia; musimy poprzedzić ją nazwą klasy Pozdro i operatorem zasięgu ' ::' (czyli kwalifikować). Piszemy zatem Pozdro::DE, aby poinformować kompilator, że chodzi nam o nazwę DE z przestrzeni nazw (w tym przypadku klasy) Pozdro.

Metoda fun wywołana z argumentem Pozdro::DE wpisała do prywatnej składowej k1 obiektu na rzecz którego została wywołana wartość 2. Oczywiście mogła to zrobić, gdyż wszystkie metody (funkcje) klasy „widzą” wszystkie nazwy zdefiniowane w zasięgu klasy, niezależnie od tego, czy są one publiczne czy nie (a samą funkcję mogliśmy wywołać, bo jest ona publiczna). Ale jak wewnątrz funkcji main przekonać się, że rzeczywiście wartość składowej dd.k1 wynosi 2, skoro ta składowa jest prywatna i użycie nazwy k1 poza klasą spowoduje błąd? W linii  drukujemy rozmiar obiektu dd:

    Guten Tag
    sizeof(dd) = 8
    dd.k1 = 2
Wynosi on 8 bajtów, co przekonuje nas, że rzeczywiście obiekt ten zawiera dwie składowe typu int i nic więcej (zakładamy, że uszeregowane są w kolejności odpowiadającej kolejności ich deklaracji w definicji klasy). W linii  pobieramy zatem adres składowej k2, do której mamy dostęp, bo jest publiczna (i wystarczy odnieść się do niej poprzez nazwę obiektu). Od niego odejmujemy 1, co zgodnie z arytmetyką wskaźników powinno dać nam adres poprzedniej składowej, czyli prywatnej składowej k1 (zmienna wskaźnikowa pk1). Wydrukowanie wartości zmiennej spod tego adresu () daje 2, co przekonuje nas, że jest to istotnie ta prywatna składowa, o którą nam chodziło.

Jak więc widać, to nie sama składowa jest chroniona za pomocą kwalifikatora private; chroniona jest nazwa składowej. Jeśli potrafimy „dobrać się” do tej składowej bez użycia jej nazwy, to język na to pozwala. Wprowadzenie podziału na składowe publiczne i prywatne jest raczej elementem wspierającym tworzenie przejrzystszego i mniej podatnego na błędy kodu, niż sposobem na crackerów. Sztuczne omijanie zabezpieczeń, jakie ten podział daje prowadzi do kodu niezrozumiałego, niebezpiecznego i niemożliwego do pielęgnacji.

Dostępność wszystkich składowych klasy przez jej składowe funkcje (statyczne, niestatyczne, konstruktory, destruktor) dotyczy klasy, a nie konkretnych obiektów. Na przykład w poniższym programie


P102: acc.cpp     Dostępność składowych innych obiektów klasy

      1.  #include <iostream>
      2.  using namespace std;
      3.  
      4.  class Vector {
      5.      double x, y, z;
      6.  public:
      7.      void set(double xx, double yy, double zz) {
      8.          x = xx;
      9.          y = yy;
     10.          z = zz;
     11.      }
     12.      double dot_product(const Vector& w) {
     13.          return x*w.x + y*w.y + z*w.z;
     14.      }
     15.  };
     16.  
     17.  int main() {
     18.      Vector w1, w2;
     19.      w1.set(1, 1, 2);
     20.      w2.set(1,-1, 2);
     21.      cout << "w1*w2 = " << w1.dot_product(w2) << endl;  
     22.  }

funkcja dot_product (iloczyn skalarny), jako składowa klasy, ma dostęp do prywatnych składowych x, y, z zarówno obiektu w1, na rzecz którego została wywołana (i do których odnosi się poprzez ich niekwalifikowane nazwy), jak i do składowych obiektu w2, przesłanego przez referencję jako argument wywołania (). Świadczy o tym wydruk programu:

    w1*w2 = 4

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