Klasa (ang. class) stanowi definicję typu danych składającego się z pól (danych składowych) oraz metod (funkcji składowych). Jest to podstawowe pojęcie z zakresu programowania obiektowego. W języku C++ taki nowy typ danych możemy zdefiniować za pomocą słowa kluczowego class lub struct. Może się więc wydawać, że skoro istnieje takie rozróżnienie, musi nieść ze sobą istotne różnice. Tymczasem różnice między klasami zdefiniowanymi odpowiednio za pomocą słowa kluczowego struct i class są bardzo subtelne:
- domyślnym modyfikatorem dostępu (ang, access modifier) dla pól i metod wewnątrz klasy (class) jest private (dostęp prywatny), natomiast w przypadku struktury (struct) jest to public (dostęp publiczny)
- domyślnym typem dziedziczenia dla klasy (class) jest dziedziczenie prywatne, natomiast w przypadku struktury (struct) jest to dziedziczenie publiczne
Inaczej mówiąc struktura to też klasa, wprowadzająca nieco inne domyślne zachowanie.
Poniżej przedstawiam przykład równoważnej klasy Employee oraz struktury Employee_t (dostęp publiczny i dziedziczenie publicznie po klasie Person):
class Person { // ... }; class Employee : public Person { // ... public: unsigned id; // ... }; struct Employee_t : Person // public inheritance { // ... unsigned id; // public access // ... };
Analogicznie można zapisać (dostęp prywatny i dziedziczenie prywatne po klasie Person):
class Person { // ... }; class Employee : Person // private inheritance { // ... unsigned id; // private access // ... }; struct Employee_t : private Person { private: // ... unsigned id; // ... };
Słowa kluczowe struct i class wprowadzają pewną różnicę semantyczną. W dalszej części artykułu wyjaśnię dlaczego czasem powinieneś preferować klasę wyrażoną słowem kluczowym struct zamiast class i odwrotnie.
Struktura
Obecność struktur w języku C++ ma także podłoże historyczne, bowiem pozwala to na kompatybilność z językiem C (w języku tym także występuje słowo kluczowe struct). Proste struktury w języku C, są więc również prawidłowymi klasami w języku C++, przy czym są one w zasadzie tożsame ze strukturami POD (ang. Plain Old Data). Nieco szerszym pojęciem od POD jest agregat (każde POD jest agregatem ale nie odwrotnie), czyli struktura, którą także w naturalny sposób można wyrazić z wykorzystaniem słowa kluczowego struct (temat dotyczący struktury POD oraz agregatu z pewnością zasługuje na osobny artykuł). Aby taka struktura mogła być prostym agregatem musi spełniać pewne podstawowe warunki (wspominam o tym również w artykule z serii Akcja Rekrutacja C++):
- nie definiuje żadnego konstruktora
- nie definiuje prywatnych (private) oraz chronionych (protected) niestatycznych składowych
- nie dziedziczy (brak klasy bazowej)
- nie definiuje funkcji wirtualnych
POD i agregat stanowią przykład prostych struktur danych. Pozostają ona w zgodzie z konwencją, według której struktury powinno stosować się do definiowania prostych, pasywnych pojemników na publiczne dane składowe. Inaczej mówiąc, służą one do tworzenia małych obiektów, które udostępniają publiczne pola i często nie posiadają metod (lub posiadają kilka podstawowych metod). Struktury nie powinny być więc niczym innym jak tylko kontenerami na dane (ang. data container).
Klasa
Klasy wyrażone słowem kluczowym class powinny być wykorzystywane standardowo przy programowaniu obiektowym. Zgodnie z ogólnie przyjętą konwencją mają one charakter aktywny (w przeciwieństwie do pasywnych struktur), bowiem definiują one pewne zachowanie (logikę) w postaci metod. Klasy oferują interfejs, realizowany poprzez publiczne metody, pozwalający na bezpieczną enkapsulację prywatnych składowych. Inaczej mówiąc, klasy służą do tworzenia na ogół większych i bardziej złożonych (niż w przypadku struktur) obiektów, posiadających prywatne składowe (w przypadku braku dziedziczenia, składowe chronione zachowują się tak samo jak składowe prywatne), do których dostęp realizowany jest za pomocą publicznego interfejsu. Klasy służą więc do definiowania złożonych struktur danych (ang. complex data structure). Podejście oparte na tak zdefiniowanych klasach (oddzielenie interfejsu od implementacji) redukuje podatność na błędy oraz lepiej odwzorowuje rzeczywistość. Do najczęściej stosowanych metod należą ”getter” (akcesor) i ”setter” (mutator), które są bezpieczniejsze i wygodniejsze (zwłaszcza jeśli chcesz zmienić nazwę pola) niż ”gołe” pola publiczne. Oto przykład:
class Person { public: // ... void setName(std::string const & name) { this->name = name; } // ... std::string const & getName() const { return this->name; } // ... private: // ... std::string name; // ... }
W tym przypadku publiczne metody setName i getName określają interfejs, natomiast prywatne pole name stanowi implementację. Podejście to ułatwia także modyfikację naszego kodu źródłowego. Wyobraźmy sobie teraz, że pole name jest publiczne, a więc nie ma ani ”gettera” ani ”settera”. Jeżeli teraz użytkownik z jakiegoś powodu zechce zmienić nazwę tego pola na przykład na m_name lub name_, wówczas będzie musiał zmienić tę nazwę w całym kodzie źródłowym, czyli dla wszystkich odwołań do tego pola. Dużo łatwiej jest zmienić nazwę wyłącznie w obrębie ”getterów” i ”setterów’. Dotyczy to nie tylko zmiany nazwy pola, bowiem możemy umieścić tam pewną dodatkową akcję (implementujemy ją tylko raz) – przykładowo metoda setName może dbać o to, aby ustawiane imię zawsze zaczynało się od dużej litery. Pozwala to na uniknięcie naruszenia zasady DRY (ang. Don’t Repeat Yourself), zgodnie z którą nie powinniśmy duplikować naszego kodu. Jednocześnie należy unikać klas, w których dominuje interfejs oparty na ”getterach” i ”setterach”, bowiem oznacza to, iż dane ukryte za takim interfejsem powinny być może należeć do ”kogoś” innego (może to być przejaw nieprawidłowo zaprojektowanej klasy).
Oczywiście przedstawione wyżej zalecenia to tylko konwencja (dobre praktyki), ponieważ sam język C++ nie ma żadnych mechanizmów wymuszających ich stosowanie. Oznacza to przede wszystkim, że w strukturze (struct) możesz umieszczać pola prywatne, natomiast w klasie (class) – pola publiczne. Niemniej jednak uważam, że warto przestrzegać tej konwencji (chyba że masz bardzo dobry powód żeby tego nie robić).
Podsumowanie
- Klasa może być wrażona słowem kluczowym struct lub class
- Domyślnym modyfikatorem dostępu dla pól i metod wewnątrz klasy (class) jest private, natomiast w przypadku struktury (struct) – public
- Domyślnym typem dziedziczenia dla klasy (class) jest dziedziczenie prywatne, natomiast w przypadku struktury (struct) – dziedziczenie publiczne
- Stosuj struktury (struct) do realizacji pasywnych kontenerów na dane
- Standardowo używaj klas (class) podczas programowania obiektowego do definiowania aktywnych i złożonych struktur danych (klasy dostarczają publiczne metody określające interfejs, za którym kryje się pewne zachowanie / logika) zapewniających odpowiednią enkapsulację niepublicznych składowych
- Używaj ’getterów” (akcesorów) i ”setterów” (mutatorów) w celu odwoływania się do pól prywatnych, przy czym ich nadmiar może oznaczać, iż tak zdefiniowana klasa została nieprawidłowo zaprojektowana