1. Programowanie obiektowe

1.3. Ochrona danych w klasach

Najczęściej do korzystania z obiektów nie potrzebujemy głębokiej wiedzy i wszystkich tajników ich implementacji – by jeździć samochodem nie muszę znać i rozumieć procesu spalania paliwa w cylindrach, zasad działania skrzyni biegów czy też nie muszę wiedzieć z ilu kół zębatych wspomniana skrzynia jest zbudowana. Popatrzcie na klasę CDioda – co prawda ciężko w tym przypadku mówić o jakichkolwiek tajnikach implementacji, lecz w praktyce do jej wykorzystania wystarczy nam wiedza, jak wywołać trzy metody. Wartości pól nie musimy znać, co więcej – nawet nie musimy wiedzieć że jakieś pola istnieją. Ponoć obciążanie sobie głowy niepotrzebną wiedzą jest niezdrowe – więc po co mamy wiedzieć, jak działa obiekt danej klasy, skoro wystarczy nam wiedzieć jak z niego skorzystać?

Mówiąc bardziej poważnie – ochrona danych to nie tylko wygoda, to także technika, która pozwala nam tak przygotować klasę, by żaden obiekt będący jej instancją w żadnym momencie swojego życia nie znalazł się w stanie niedozwolonym. Przykładowo – jeśli twierdzimy, że czarny kot nie ma prawa bytu, możemy zdefiniować klasę kota, w której będzie pole kolor. I dla tego pola wartość „czarny” będzie niedozwolona, a mechanizmy klasy zapewnią, że nigdy żaden jej użytkownik nie będzie mógł przypisać wartości „czarny” polu kolor.

Fachowo takie mechanizmy nazywa się ochroną pól lub metod. Ochrona oznacza, że autor danej klasy ma możliwość zdefiniowania obszaru widoczności jej pól, i idąc dalej tym tropem – zdefiniować możliwe wartości pola, a poprzez to – zdefiniować możliwe stany obiektu. Standard programowania obiektowego (o ile można o czymś takim mówić) przewiduje występowanie trzech typów ochrony zmiennych:

  • Pola i metody mogą być publiczne (ang. public), co oznacza, że są one dostępne z dowolnego miejsca w programie – nie są chronione. W C++ jest to zachowanie domyślne dla klasy definiowanej przy pomocy słowa kluczowego struct.
  • Jeśli chcemy uniemożliwić jakikolwiek dostęp do zmiennej lub metody spoza implementacji metod wchodzących w jej skład, deklarujemy ją jako prywatną (ang. private). W C++ jest to zachowanie domyślnie dla klasy definiowanej przy pomocy słowa kluczowego class (jeśli nie zadeklarujemy inaczej, wszystkie pola i metody będą prywatne).
  • Trzecim typem są pola i metody chronione (ang. protected) - dostępne podczas implementacji danego obiektu i wszystkich obiektów pochodnych, dla których zastosowano dziedziczenie publiczne lub chronione.

Do sekcji chronionej wrócimy nieco później, na razie zajmiemy się dokładniejszą interpretacją tego, co oznacza część publiczna i część prywatna. Odpuśćmy sobie na chwilę naszą diodę, i zajmijmy się innym przykładem. Napiszemy klasę wektor, która będzie służyła do przechowywania pewnej zadanej ilości liczb.  Dla większej jasności kodu - nie będziemy go rozbijali na deklarację i definicję, dodatkowo definiując w nim własną postać operatora indeksowania - o czym dokładniej napiszę w dalszej części podręcznika. 


#include "iostream"

class Wektor {
public:
    int rozmiar{0};
    double* dane{nullptr};
    double ustawRozmiar(int r) {
        dane = new double[r];
        rozmiar=r;
    };
    void drukuj() {
        std::cout << "[";
        if (rozmiar>0) {
            std::cout << dane[0];
        }
        for (int i=0; i<rozmiar; i++) {
            std::cout << " " << dane[i];
        }
        std::cout << "]\n";
    }
    double sumuj() {
        double rval{0};
        for (int i=0; i<rozmiar; i++) {
            rval += dane[i];
        }
        return rval;
    }
    double& operator[](int idx) {
        return dane[idx];
    }
};

int main() {
    Wektor w1, w2;
    w1.ustawRozmiar(10);
    for (int i{0}; i<10; i++) {
        w1[i] = 10.0* rand()/RAND_MAX;
    }
    w1.drukuj();
    std::cout << "Średnia: " << w1.sumuj()/10.0 << "\n";
    
    w1.rozmiar = 100; // problem - drukuj przestanie działać
    w2.rozmiar = 10; // problem - w2 jest niespójne, nie ma przyznanej pamięci 
    
    return 0;
}

Jeśli dostęp do pól nie jest blokowany - to od dobrej woli korzystającego z naszej klasy zależy, czy wykona on prawidłową inicjalizację, oraz - czy nie zmieni wartości pól na niepasujące do siebie (pomijając już ten szczegół, iż prosto mógłby podać wartość ujemną w rozmiarze, i nikt by nie protestował).

W praktyce powinniście unikać jak ognia eksponowania stanu klasy w jej części publicznej bez ochrony. 

Dopóki nie ma innych istotnych wskazań - wszystkie pola w klasie powinny być w części prywatnej lub chronionej. 

Błędów z powyższego kodu  popełnicie jeśli przeniesiecie pola rozmiar i dane do części chronionej. A dokładniej - możecie je popełnić, ale kompilator to wychwyci i zgłosi błąd kompilacji. 


#include "iostream"

class Wektor {
public:
    double ustawRozmiar(int r) {
        dane = new double[r];
        rozmiar=r;
    };
    void drukuj() {
        std::cout << "[";
        if (rozmiar>0) {
            std::cout << dane[0];
        }
        for (int i=0; i<rozmiar; i++) {
            std::cout << " " << dane[i];
        }
        std::cout << "]\n";
    }
    double sumuj() {
        double rval{0};
        for (int i=0; i<rozmiar; i++) {
            rval += dane[i];
        }
        return rval;
    }
    double& operator[](int idx) {
        return dane[idx];
    }

private:
    int rozmiar{0};
    double* dane{nullptr};    
};

int main() {
    Wektor w1, w2;
    w1.ustawRozmiar(10);
    for (int i{0}; i<10; i++) {
        w1[i] = 10.0* rand()/RAND_MAX;
    }
    w1.drukuj();
    std::cout << "Średnia: " << w1.sumuj()/10.0 << "\n";
    
    // teraz poniższe linie powodują błąd kompilacji. 
    w1.rozmiar = 100; // problem - drukuj przestanie działać
    w2.rozmiar = 10; // problem - w2 jest niespójne, nie ma przyznanej pamięci 
    
    return 0;
}

Teraz moja klasa wektora jest odrobinę lepsza. Ciągle jej daleko do ideału - m. inn. brakuje domyślnej alokacji i zwalniania zasobów, przenoszenia i kopiowania, zmiany rozmiaru bez utraty zawartości, itp ... ale pamiętajcie - to tylko ilustracja. W praktyce pisanie własnych klas wektorów nie ma sensu. Ich implementacje w bibliotece standardowej, czy w dodatkowych bibliotekach typu Qt są dopracowane do perfekcji, uwzględniając zarówno wydajność, jak i bezpieczeństwo stosowania. 

Klasy zamieszczone w tej części podręcznika traktujcie jako ilustracje poruszanych zagadnień - nie jako przykłady kodu "produkcyjnego".

Ochrona danych między obiektami tej samej klasy

Po tym co przeczytaliście wyżej niektórzy dochodzą do wniosku, że część prywatna jest częścią prywatną obiektu. Nie jest to prawda. Ochrona dotyczy klasy - a nie jej instancji, tak więc metody tej klasy mogą spokojnie odwoływać się do prywatnych części innych obiektów tej samej klasy  - a nie tylko tego, dla którego metoda została wywołana. Ilustruje to poniższy przykład: 


#include "iostream"

class Wektor {
public:
    double ustawRozmiar(int r); 
    void drukuj(); 
    double sumuj();
    double& operator[](int idx);
    void kopiuj(const Wektor& vs) {
       // można odwołać się do pól prywatnych vs
       dane = new double[vs.rozmiar];
       rozmiar = vs.rozmiar; 
       for (int i=0; i!=rozmiar; ++i) 
         dane[i]=vs.dane[i]; 
    }
private:
    int rozmiar{0};
    double* dane{nullptr};    
};