LEKCJA 24 : SKĄD WZIĘŁY SIĘ KLASY I OBIEKTY W C++. ________________________________________________________________ W trakcie tej lekcji dowiesz się, skąd w C++ biorą się obiekty i jak z nich korzystać. ________________________________________________________________ Zajmiemy się teraz tym, z czego C++ jest najbardziej znany - zdolnością posługiwania się obiektami. Główną zaletą programowania obiektowego jest wyższy stopień "modularyzacji" programów. "Mudularyzacja" jest tu rozumiana jako możliwość podziału programu na niemal niezależne fragmenty, które mogą opracowywać różne osoby (grupy) i które później bez konfliktów można łączyć w całość i uruchamiać natychmiast. C++ powstał, gdy programy stały się bardzo (zbyt) długie. Możliwość skrócenia programów nie jest jednakże jedyną zaletą C++. W długich, rozbudowanych programach trudno spamiętać szczegóły dotyczące wszystkich części programu. Jeśli grupy danych i grupy funkcji uda się połączyć w moduły, do których można później sięgać, jak do pewnej odrębnej całości, znacznie ułatwia to życie programiście. Na tym, w pewnym uproszczeniu, polega idea programowania obiektowego. JAK STRUKTURY STAWAŁY SIĘ OBIEKTAMI. W C++ struktury uzyskują "trochę więcej praw" niż w klasycznym C. Przykładowy program poniżej demonstruje kilka sposobów posługiwania się strukturą w C++. [P90.CPP] #include struct Data { int dzien; int miesiac; int rok; }; Data NaszaStruktura = {3, 11, 1979}; //Inicjujemy strukture Data daty[16]; //Tablca struktur Data *p = daty; //Wskaznik do tablicy void Fdrukuj(Data); //Prototyp funkcji int i; //Licznik automat. 0 int main() { for (; i < 16; i++) { *(p + i) = NaszaStruktura; daty[i].rok += i; cout << "\nDnia "; Fdrukuj(daty[i]); cout << " Patrycja "; if ( !i ) cout << "urodzila sie, wiek - "; if (i > 0 && i < 14) cout << "miala "; if (i > 13) cout << "bedzie miec "; cout << i; if (i == 1) cout << " roczek"; else cout << " lat"; if (i > 1 && i < 5) cout << "ka"; cout << '.'; } return 0; } void Fdrukuj(Data Str) { char *mon[] = { "Stycznia","Lutego","Marca","Kwietnia","Maja","Czerwca", "Lipca","Sierpnia","Wrzesnia","Pazdziernika","Listopada", "Grudnia" }; cout << Str.dzien << ". " << mon[Str.miesiac-1] << ". " << Str.rok; } Prócz danych struktury w C++ mogą zawierać także funkcje. W przykładzie poniżej struktura Data zawiera wewnątrz funkcję, która przeznaczona jest do obsługi we właściwy sposób danych wchodzących w skład własnej struktury. [P091.CPP] #include struct Data //Definicja struktury { int dzien, miesiac, rok; void Fdrukuj(); //Prototyp funkcji Data(); //Konstruktor struktury }; void Data::Fdrukuj() //Definicja funkcji { char *mon[] = { "Stycznia","Lutego","Marca","Kwietnia","Maja","Czerwca", "Lipca","Sierpnia","Wrzesnia","Pazdziernika","Listopada", "Grudnia" }; cout << dzien << ". " << mon[miesiac-1] << ". " << rok; } Data::Data(void) //Poczatkowa data - Konstruktor { dzien = 3; miesiac = 11; rok = 1979; } int main() { Data NStruktura; //Inicjujemy strukture cout << "\n Sprawdzamy: "; NStruktura.Fdrukuj(); //Wywolanie funkcji cout << " = "; cout << NStruktura.dzien << " . " << NStruktura.miesiac << " . " << NStruktura.rok; for (int i=0; i < 16; i++, NStruktura.rok++) { cout << "\nDnia "; NStruktura.Fdrukuj(); cout << " Patrycja "; if ( !i ) cout << "urodzila sie, wiek - "; if (i > 0 && i < 14) cout << "miala "; if (i > 13) cout << "bedzie miec "; cout << i; if (i == 1) cout << " roczek"; else cout << " lat"; if (i > 1 && i < 5) cout << "ka"; cout << '.'; } return 0; } Zwróć uwagę, że * odkąd dane stały się elementem struktury, zaczęliśmy odwoływać się do nich tak: nazwa_struktury.nazwa_pola; * gdy funkcje stały się elementem struktury, zaczęliśmy odwoływać się do nich tak: nazwa_struktury.nazwa_funkcji; Pojawiły się również różnice w sposobie definiowania funkcji: void Data::Fdrukuj() //Definicja funkcji { ... } oznacza, że funkcja Fdrukuj() jest upoważniona do operowania na wewnętrznych danych struktur typu Data i nie zwraca do programu żadnej wartości (void). Natomiast zapis: Data::Data(void) //Poczatkowa data - Konstruktor oznacza, że funkcja Data(void) nie pobiera od programu żadnych parametrów i tworzy (w pamięci komputera) strukturę typu Data. Takie dziwne funkcje konstruujące (inicjujące) strukturę (o czym dokładniej w dalszej części książki), nazywane w C++ konstruktorami nie zwracają do programu żadnej wartości. Zwróć uwagę, że konstruktory to specjalne funkcje, które: -- mają nazwę identyczną z nazwą typu własnej struktury, -- nie posiadają wyspecyfikowanego typu wartości zwracanej do programu, -- służą do zainicjowania w pamięci pól struktury, -- nie są wywoływane w programie w sposób jawny, lecz niejawnie, automatycznie. Podstawowym praktycznym efektem dodania do struktur funkcji stała się możliwość skutecznej ochrony danych zawartych na polach struktury przed dostępem funkcji z zewnątrz struktury. Przed dodaniem do struktury jej własnych wewnętrznych funkcji - wszystkie funkcje pochodziły z zewnątrz, więc "hermetyzacja" danych wewnątrz była niewykonalna. Zasady dostępu określa się w C++ przy pomocy słów: public - publiczny, dostępny, protected - chroniony, dostępny z ograniczeniami, private - niedostępny spoza struktury. Przykładowy program poniżej demonstruje tzw. "hermetyzację" struktury (ang. encapsulation). W przykładzie poniżej: * definiujemy strukturę; * definiujemy funkcje; * przekazujemy i pobieramy dane do/od struktury typu Zwierzak. Zmienna int schowek powinna sugerować ukrytą przez strukturę i niedostępną dla nieuprawnionych funkcji część danych struktury a nie cechy anatomiczne zwierzaka. [STRUCT.CPP] # include "iostream.h" //UWAGA: schowek ma status private, jest niedostepny struct Zwierzak { private: int schowek; //DANE PRYWATNE - niedostepne public: void SCHOWAJ(int Xwe); //Funkcje dostepne zzewnatrz int ODDAJ(void); }; void Zwierzak::SCHOWAJ(int Xwe) //definicja funkcji { schowek = Xwe; } int Zwierzak::ODDAJ(void) { return (schowek); } main() { Zwierzak Ciapek, Azor, Kotek; // Struktury "Zwierzak" int Piggy; // zwykla zmienna Ciapek.SCHOWAJ(1); Azor.SCHOWAJ(22); Kotek.SCHOWAJ(-333); Piggy = -4444; cout << "Ciapek ma: " << Ciapek.ODDAJ() << "\n"; cout << "Azor ma: " << Azor.ODDAJ() << "\n"; cout << "Kotek ma: " << Kotek.ODDAJ() << "\n"; cout << "Panna Piggy ma: " << Piggy << "\n"; return 0; } // Proba nieautoryzowanego dostepu do danych prywatnych obiektu: // cout << Ciapek.schowek; // printf("%d", Ciapek.schowek); // nie powiedzie sie Powiedzie sie natomiast próba dostępu do "zwykłej" zmiennej - dowolną metodą - np.: printf("%d", Piggy); //Prototyp ! # include Jeśli podejmiesz próbę odwołania się do "zakapsułkowanych" danych w zwykły sposób - np.: cout << Ciapek.schowek; kompilator wyświetli komunikat o błędzie: Error: 'Zwierzak::schowek' is not accessible in function main() (pole schowek struktury typu Zwierzak (np. str. Ciapek) nie jest dostępne z wnętrza funkcji main(). ) Do klas i obiektów już tylko maleńki kroczek. Jak przekonasz się za chwilę - struktura Ciapek jest już właściwie obiektem, a typ danych Zwierzak jest już właściwie klasą obiektów. Wystarczy zamienić słowo "struct" na słowo "class". [CLASS.CPP] # include "iostream.h" //w klasach schowek ma status private AUTOMATYCZNIE //slowo private stalo sie zbedne class Zwierzak { int schowek; public: void SCHOWAJ(int Xwe); //Funkcje dostepne zzewnatrz int ODDAJ(void); }; void Zwierzak::SCHOWAJ(int Xwe) { schowek = Xwe; } int Zwierzak::ODDAJ(void) { return (schowek); } main() { Zwierzak Ciapek, Azor, Kotek; // obiekty klasy "Zwierzak" int Piggy; // zwykla zmienna Ciapek.SCHOWAJ(1); Azor.SCHOWAJ(22); Kotek.SCHOWAJ(-333); Piggy = -4444; cout << "Ciapek ma: " << Ciapek.ODDAJ() << "\n"; cout << "Azor ma: " << Azor.ODDAJ() << "\n"; cout << "Kotek ma: " << Kotek.ODDAJ() << "\n"; cout << "Panna Piggy ma: " << Piggy << "\n"; return 0; } Kompilator nawet nie mrugnął. Zmiana słowa struct na słowo class nie sprawiła mu zatem widocznie przykrości. Mało tego, zwróć uwagę, że długość wynikowego pliku STRUCT.EXE i CLASS.EXE jest IDENTYCZNA. Wynikałoby z tego, że sposób tworzenia wynikowego kodu przez kompilator w obu wypadkach był identyczny. O KLASACH I OBIEKTACH. Klasy służą do tworzenia formalnego typu danych. W przypadku klas wiadomo jednak "z definicji", że będzie to bardziej złożony typ (tzw. agregat) zawierający praktycznie zawsze i dane "tradycyjnych" typów i funkcje (nazywane "metodami"). Podobnie jak definiując strukturę tworzysz nowy formalny typ danych, tak i tu - definiując klasę tworzysz nowy typ danych. Jeśli zadeklarujesz użycie zmiennych danego typu formalnego, takie zmienne to właśnnie obiekty. Innymi słowy, klasy stanowią definicje formalnego typu, natomiast obiekty - to zmienne danego typu (danej klasy). Zamiast słowa struct stosujemy przy klasach słowo class. class Klasa { int prywatna_tab[80] public: int dane; void Inicjuj(void); int Funkcja(int arg); }; Nasza pierwsza świadomie tworzona klasa nazywa się "Klasa" i stanowi nowy formalny typ zmiennych. Jeśli zadeklarujesz zmienną takiej klasy (tego typu formalnego), to taka zmienna będzie właśnie OBIEKTEM. Nasza pierwsza prawdziwa Klasa zawiera dane: prywatna_tab[80] - prywatną tablicę; dane - publiczną daną prostą typu int; oraz funkcje: Inicjuj() - zainicjuj - utwórz obiekt danej klasy w pamięci; Funkcja() - jakaś funkcja publiczna. Gdyby była to zwykła struktura, jej definicja w programie wyglądałaby tak: struct Klasa { private: int prywatna_tab[80] public: int dane; void Inicjuj(void); int Funkcja(int arg); }; Jeżeli w dalszej części programu chcielibyśmy zastosować struktury takiego typu, deklaracja tych struktur musiałaby wyglądać tak: struct rodzaj_struktur { private: int prywatna_tab[80] public: int dane; void Inicjuj(void); int Funkcja(int arg); } str1, str2, .... , nasza_struktura; bądź tak: struct rodzaj_struktur { private: int prywatna_tab[80] public: int dane; void Inicjuj(void); int Funkcja(int arg); }; ... (struct) rodzaj_struktur str1, str2, .... , nasza_struktura; Słowo kluczowe struct jest opcjonalne. Moglibyśmy więc zadeklarować strukturę w programie, wewnątrz funkcji main(): struct rodzaj_struktur { private: int prywatna_tab[80] public: int dane; void Inicjuj(void); int Funkcja(int arg); }; main() { ... struct rodzaj_struktur nasza_struktura; //lub równoważnie: rodzaj_struktur nasza_struktura; Do pól struktury możemy odwoływać się przy pomocy operatora kropki (ang. dot operator). Podobnie dzieje się w przypadku klas. Jeśli zadeklarujemy zmienną typu Klasa, to ta zmienna będzie naszym pierwszym obiektem. class Klasa { int prywatna_tab[80] public: int dane; void Inicjuj(void) int Funkcja(int our_param); } Obiekt; Podobnie jak wyżej, możemy zadeklarować nasz obiekt wewnątrz funkcji main(): class Klasa { int prywatna_tab[80] public: int dane; void Inicjuj(void) int Funkcja(int argument); }; main() { ... Klasa Obiekt; ... Przypiszemy elementom obiektu wartości: main() { ... Klasa Obiekt; Obiekt.dane = 13; ... Taką samą metodą, jaką stosowaliśmy do danych - pól struktury, możemy odwoływać się do danych i funkcji w klasach i obiektach. main() { ... Klasa Obiekt; Obiekt.dane = 13; Obiekt.Funkcja(44); ... Przyporządkowaliśmy obiektowi nie tylko dane, ale także funkcje poprzez umieszczenie prototypów funkcji wewnątrz deklaracji klasy: class Klasa { ... public: ... void Inicjuj(void) /* Prototypy funkcji */ int Funkcja(int argument); }; [!!!] UWAGA! ________________________________________________________________ W C++ nie możemy zainicjować danych wewnątrz deklaracji klasy: class Klasa { private: int prywatna_tab[80] = { 1, 2, ... }; //ŹLE ! public: int dane = 123; //ŹŁE ! ... ________________________________________________________________ Inicjowanie danych odbywa się w programie głównym przy pomocy przypisania (dane publiczne), bądź za pośrednictwem funkcji należącej do danej klasy i mającej dostęp do wewnętrznych danych klasy/obiektu (dane prywatne). Inicjowania danych mogą dokonać także specjalne funkcje - tzw. konstruktory. Dane znajdujące się wewnątrz deklaracji klasy mogą mieć status public, private, bądź protected. Dopóki nie zażądasz inaczej - domyślnie wszystkie elementy klasy mają status private. Jeżeli część obiektu jest prywatna, to oznacza, że żaden element programu spoza obiektu nie ma do niej dostępu. W naszej Klasie prywatną część stanowi tablica złożona z liczb całkowitych: (default - private:) int prywatna_tab[80]; Do (prywatnych) elementów tablicy dostęp mogą uzyskać tylko funkcje związane (ang. associated) z obiektem danej klasy. Funkcje takie muszą zostać zadeklarowane wewnątrz definicji danej klasy i są nazywane członkami klasy - ang. member functions. Funkcje mogą mieć status private i stać się dzięki temu wewnętrznymi funkcjami danej klasy (a w konsekwencji również prywatnymi funkcjami obiektów danej klasy). Jest to jedna z najważniejszych cech nowoczesnego stylu programowania w C++. Na tym polega idea hermetyzacji danych i funkcji wewnątrz klas i obiektów. Gdyby jednak cała zawartość (i dane i funkcje) znajdujące się w obiekcie zostały dokładnie "zakapsułkowane", to okazałoby się, że obiekt stał się "ślepy i głuchy", a w konsekwencji - niedostępny i kompletnie nieużyteczny dla programu i programisty. Po co nam obiekt, do którego nie możemy odwołać się z zewnątrz żadną metodą? W naszym obiekcie, w dostępnej z zewnątrz części publicznej zadeklarowaliśmy zmienną całkowitą dane oraz dwie funkcje - Inicjuj() oraz Funkcja(). Jeśli dane i funkcje mają status public, to oznacza, że możemy się do nich odwołać z dowolnego miejsca programu i dowolnym sposobem. Takie odwołania przypominają sposób odwoływania się do elementów struktury: main() { ... Obiekt.dane = 5; //Przypisanie wartości zmiennej. Obiekt.Inicjuj(); //Wywołanie funkcji Inicjuj() ... Obiekt.Funkcja(3); //Wywołanie funkcji z argumentem [!!!] ZAWSZE PUBLIC ! ________________________________________________________________ Dane zawarte w obiekcie, podobnie jak zwykłe zmienne wymagają zainicjowania. Funkcja inicjująca dane - zawartość obiektu musi zawsze posiadać status public aby mogła być dostępna z zewnątrz i zostać wywołana w programie głównym - funkcji main(). Funkcje i dane dostępne z zewnątrz stanowią tzw. INTERFEJS OBIEKTU. ________________________________________________________________