TEMAT : KLASY DZIEDZICZENIE Wprowadzenie do dziedziczenia w języku C++ Język C++ możliwa tworzenie nowej klasy (nazywanej klasą pochodną) w oparciu o pewną wcześniej zdefiniowaną klasę (nazywaną klasą podstawową). Klasa pochodna dziedziczy dane oraz funkcje składowe z klasy podstawowej. W ten sposób nie musimy tworzyć całego kodu klasy pochodnej a jedynie nowe jej elementy, które nie były do tej pory zaimplementowane w klasie podstawowej. Oczywiście klasa pochodna może stać się klasą podstawową dla kolejnej tworzonej przez nas klasy. W programowaniu obiektowym rozróżniamy następujące rodzaje dziedziczenia: dziedziczenie jednokrotne (pojedyncze) gdy klasa pochodna tworzona jest na podstawie tylko jednej klasy podstawowej (oczywiście klasa bazowa może dziedziczyć również z jakiejś innej klasy), dziedziczenie wielokrotne gdy klasa pochodna tworzona jest na podstawie wielu klas podstawowych. Zadanie 1 Przeanalizuj sposób implementacji przedstawionych poniżej dwóch klas CPunkt oraz CKolo. Jak mogłaby wyglądać definicja klasy CPunkt, gdyby programista nie zaplanował wykorzystania mechanizmu dziedziczenia danych i funkcji składowych klasy CPunkt przez klasę CKolo? Implementacja klasy CPunkt jako klasy podstawowej class CPunkt friend ostream & operator << (ostream &, const CPunkt &); public: CPunkt(double x = 0.0, double y = 0.0); ~CPunkt(); void SetPunkt (double x_t, double y_t); double GetX () const return x;; double GetY () const return y;; void Drukuj () const; protected: double x,y; ; CPunkt::CPunkt(double x_t, double y_t) SetPunkt(x_t,y_t); CPunkt::~CPunkt() void CPunkt::SetPunkt (double x_t, double y_t) x = x_t; y = y_t; void CPunkt::Drukuj () const cout << "[" << x << ", " << y << "]"; ostream & operator << (ostream & output, const CPunkt & pkt) output << "[" << pkt.x << ", " << pkt.y << "]"; return output; 1
Implementacja klasy CKolo dziedziczącej z klasy CPunkt class CKolo : public CPunkt friend ostream & operator << (ostream &, const CKolo &); public: CKolo(double R = 0.0, double x = 0.0, double y = 0.0); ~CKolo(); void SetR (double R_t); double GetR () const; double ObliczObszar () const; protected: double R; ; CKolo::CKolo(double R_t, double x_t, double y_t) : CPunkt(x_t,y_t) SetR(R_t); CKolo::~CKolo() void CKolo::SetR (double R_t) if (R_t >= 0.0) R = R_t; else R = 0.0; double CKolo::GetR () const return R; double CKolo::ObliczObszar () const return 3.14159*R*R; ostream & operator << (ostream & output, const CKolo & kolo) output << "Srodek kola = " << CPunkt(kolo) << ", R = " << kolo.r; return output; Analiza istotnych elementów klas CPunkt oraz CKolo z punktu widzenia mechanizmu dziedziczenia W powyższym przykładzie widzimy propozycję dwóch klas. Pierwsza z nich, tj. klasa CPunkt, jest klasą podstawową dla klasy pochodnej CKolo. Zadaniem klasy CPunkt jest reprezentowanie w programie abstrakcyjnego obiektu punktu, o którym zakładamy, że definiują go jego współrzędne x i y. Klasa CKolo reprezentuje natomiast abstrakcyjny obiekt koła, które jest zdefiniowane przez współrzędne środka tego koła oraz długość jego promienia. Do ważnych elementów wykorzystywanych w powyższych klasach możemy zaliczyć: wybór sposobu dziedziczenia przez klasę CKolo class CKolo : public CPunkt Standard języka C++ przewiduje trzy rodzaje dziedziczenia: publiczne, chronione i prywatne. W rozważanym przykładzie klasa CKolo dziedziczy z klasy CPunkt wykorzystując do tego zadania interfejs publiczny. Dziedziczenie oznaczone jest dwukropkiem w nagłówku definicji klasy. Słowo kluczowe public określa rodzaj dziedziczenia, tj. dziedziczenie publiczne. Dalej w kolejności podajemy nazwę klasy podstawowej dla definiowanej właśnie klasy pochodnej. Dziedziczenie publiczne zakłada, że dane i funkcje składowe klasy podstawowej zadeklarowane jako public i private, stają się automatycznie danymi i funkcjami składowymi odpowiednio publicznymi i prywatnymi klasy pochodnej. Należy jednak mieć na 2
uwadze fakt, że prywatne składowe klasy podstawowej nie są bezpośrednio dostępne dla klas pochodnych. Klasa pochodna ma do nich dostęp za pośrednictwem publicznych funkcji dostępowych zdefiniowanych w klasie podstawowej. sposób określania dostępu do danych klasy podstawowej CPunkt protected: double x,y; Zadeklarowanie składowych x, y przy użyciu specyfikatora dostępu protected uniemożliwia bezpośredni dostęp do nich dla klientów klasy CPunkt w programie. Dostęp do danych mają funkcje składowe klasy oraz funkcje nią zaprzyjaźnione, natomiast bezpośredni dostęp do tych danych z programu jest możliwy wyłącznie poprzez odpowiednie publiczne funkcje dostępowe. W przypadku, gdy chcemy zastosować mechanizm dziedziczenia, w definicji klasy podstawowej najczęściej wykorzystujemy tryb dostępu protected. W ten sposób dostęp do danych mają funkcje składowe i funkcje zaprzyjaźnione zdefiniowane zarówno w klasie podstawowej jak i w klasie pochodnej. Wszystkie składowe publiczne i chronione klasy CPunkt są dziedziczone jako odpowiednio, składowe publiczne i chronione w klasie CKolo. Oznacza to, ze interfejs publiczny klasy CKolo zawiera zarówno publiczne składowe klasy CPunkt, jak i publiczne składowe klasy CKolo. sposób implementacji konstruktora klasy pochodnej CKolo / wywoływanie konstruktora klasy podstawowej przez konstruktor klasy pochodnej CKolo::CKolo(double R_t, double x_t, double y_t) : CPunkt(x_t,y_t) SetR(R_t); Konstruktor klasy CKolo wywołuje konstruktor klasy CPunkt, który jest odpowiedzialny za zainicjowanie tej części obiektu klasy CKolo, która pochodzi z klasy podstawowej CPunkt. Wartości x_t, y_t są przekazywane z konstruktora CKolo do konstruktora CPunkt, który zainicjuje nimi dane składowe klasy podstawowej. Dodatkowo inicjowana jest wartość promienia R poprzez funkcję SetR (w celu sprawdzenia poprawności wartości zadanego promienia). Należy pamiętać, że w przypadku, gdy konstruktor klasy podstawowej CPunkt nie zostanie przez nas wywołany jawnie w konstruktorze klasy pochodnej CKolo, operacja wywołania domyślnego konstruktora klasy CPunkt i tak zostałaby przeprowadzona dla domyślnych wartości x i y. implementacja funkcji zaprzyjaźnionych z klasą pochodną CKolo ostream & operator << (ostream & output, const CKolo & kolo) output << "Srodek kola = " << CPunkt(kolo) << ", R = " << kolo.r; return output; Funkcje zaprzyjaźnione z klasą podstawową nie są dziedziczone przez klasę pochodną. Tak więc w rozważanym przykładzie klasa pochodna CKolo nie może bezpośrednio wywołać zaprzyjaźnionej funkcji operatorowej klasy CPunkt przeciążającej operator wstawiania do strumienia <<. Istnieje jednak możliwość przeciążenia operatora << klasy CKolo w taki sposób, aby oprócz danych specyficznych dla obiektu koła, tzn. w naszym przykładzie promienia R, wyświetlone zostały również współrzędne środka koła. Operację taką umożliwia operacja rzutowania referencji do obiektu CKolo na klasę CPunkt. W wyniku takiego działania zostanie wywołany przeciążony operator << klasy CPunkt. deklaracja funkcji składowej obiektu jako funkcji stałej const void Drukuj () const; void CPunkt::Drukuj () const cout << "[" << x << ", " << y << "]"; Deklaracja funkcji składowej obiektu jako funkcji stałej const jest ważnym elementem programowania obiektowego. Wiąże się to z tym, że w przypadku deklaracji stałego obiektu pewnej klasy w kodzie programu, mamy możliwość nadania mu wartości jedynie w chwili jego tworzenia (poprzez konstruktor danej klasy), jednak wywołanie dowolnej z jego funkcji składowych 3
(nawet tych, które nie modyfikują wartości danych) nie jest dozwolone. Wyjątek stanowią funkcje zadeklarowane w obiekcie jako const zarówno w prototypie jak też w definicji definicji funkcji. Funkcje tak zdefiniowane nie mogą jednak modyfikować danych obiektu. Wykorzystanie klas CKolo i CPunkt w programie // ------------------------------------------------------------------------------ // // Czesc 1 - Wykorzystanie klasy CPunkt oraz CKolo w programie // ------------------------------------------------------------------------------ // CPunkt * ptrpunkt = 0; CPunkt punkta (30,50); CKolo * ptrkolo = 0; CKolo kolob (2.5,120,90); cout << "Czesc 1" << endl; cout << "--------------------------------------------------------------------------" << endl; cout << "Punkt A: " << punkta << endl; cout << "Kolo B: " << kolob << endl << endl; // Czesc 2 - Traktujemy kolo jako punkt ('widzimy' jedynie czesc z klasy podstawowej) cout << "Czesc 2" << endl; cout << "--------------------------------------------------------------------------" << endl; // Zad.1 Przypisanie adresu kola wskaznikowi ptrpunkt cout << "Kolo B (poprzez *ptrpunkt): " << *ptrpunkt << endl << endl; // Zad.2 Traktujemy kolob jako kolo (z rzutowaniem) // Przypisanie adresu kola wskaznikowi ptrpunkt // Rzutowanie wskaznika klasy podstawowej na wskaznik klasy pochodnej cout << "Kolo B (przez *ptrkolo): " << *ptrkolo << endl; cout << "Powierzchnia kola B (przez ptrkolo): " << ptrkolo->obliczobszar() << endl << endl; // Czesc 3 - Niebezpieczna operacja - Traktowanie punktu jako kola // Przypisanie adresu punktu wskaznikowi ptrpunkt ptrpunkt = &punkta; // Rzutowanie wskaznika klasy podstawowej na wskaznik klasy pochodnej cout << "Czesc 3" << endl; cout << "--------------------------------------------------------------------------" << endl; cout << "Punkt A (poprzez *ptrkolo): " << *ptrkolo << endl; cout << "Powierzchnia obiektu ptrkolo: " << ptrkolo->obliczobszar() << endl << endl; Do ważnych elementów przedstawionych w kolejnych częściach w programie możemy zaliczyć: sposób deklaracji obiektów klasy CPunkt i CKolo oraz odpowiednich wskaźników do tych obiektów CPunkt * ptrpunkt = 0; CPunkt punkta (30,50); CKolo * ptrkolo = 0; CKolo kolob (2.5,120,90); Powyższy kod przedstawia deklarację obiektu punkta. Wywołaniu konstruktora towarzyszy nadaje wartości współrzędnym x 4
oraz y punktu w taki sposób, że x = 30, y = 50. Deklaracja wskaźnika ptrpunkt do typu CPunkt połączona jest z inicjacją wartością 0 (co oznacza, że wskaźnik nie wskazuje w tej chwili na żaden obiekt klasy CPunkt). Wskaźnik ptrpunkt służy do wskazywania na obiekty klasy CPunkt. Podobne operacje są wykonane w odniesieniu do obiektu klasy CKolo oraz wskaźnika do typu CKolo. Zauważmy, że kolob jest obiektem klasy pochodnej CKolo, która dziedziczy dane i funkcje składowe klasy podstawowej CPunkt. sposób wykorzystania zaprzyjaźnionych funkcji operatorowych wstawiania do strumienia << cout << "Punkt A: " << punkta << endl; cout << "Kolo B: " << kolob << endl << endl; Wypisując obiekty klasy podstawowej i pochodnej wykorzystujemy odpowiednie zaprzyjaźnione funkcje operatorowe zaimplemetowane zarówno w klasie CPunkt jak i w klasie CKolo. "traktowanie kola jako punktu" (w wyniku takiej operacji 'widzimy' jedynie tą cześć danych i funkcji składowych, która należy do klasy podstawowej) a) Przypisanie adresu obiektu kolob wskaźnikowi ptrpunkt. W przypadku dziedziczenia publicznego zawsze możliwe jest przypisanie wskaźnikowi klasy podstawowej CPunkt wskaźnika klasy pochodnej CKolo. Operacja taka jest dozwolona, gdyż obiekt klasy pochodnej CKolo jest jednocześnie obiektem klasy podstawowej CPunkt. Jednak wskaźnik do klasy podstawowej CPunkt 'widzi' wyłącznie tę część obiektu klasy pochodnej CKolo, która znalazła się w nim w wyniku dziedziczenia z klasy podstawowej. Wykonana jest przez kompilator niejawna konwersja wskaźnika do klasy pochodnej CKolo na wskaźnik do klasy podstawowej CPunkt. b) cout << "Kolo B (poprzez *ptrpunkt): " << *ptrpunkt << endl << endl; Wykonanie operacji wypisania obiektu kolob z wykorzystaniem do tego celu wskaźnika ptrpunkt spowoduje, że zobaczymy na ekranie jedynie informację o współrzędnych środka koła. "traktowanie koła jako koła" (z wykorzystaniem operacji rzutowania) a) Przypisanie adresu obiektu kolob wskaźnikowi ptrpunkt. b) Rzutowanie wskaźnika klasy podstawowej CPunkt na wskaznik klasy pochodnej CKolo. c) cout << "Kolo B (przez *ptrkolo): " << *ptrkolo << endl; cout << "Powierzchnia kola B (przez ptrkolo): " << ptrkolo->obliczobszar() << endl << endl; W wyniku wykonania powyższych instrukcji wypisane zostaną wszystkie informacje dotyczące obiektu kolob (wywołana zostanie funkcja operatorowa przeciążona w klasie CKolo) oraz wypisane zostanie pole koła reprezentowanego przez obiekt kolob (czyli wywołana zostanie właściwa funkcja obliczająca pole koła zaimplementowana w klasie CKolo). "traktowanie punktu jako koła" (niebezpieczna operacja) a) ptrpunkt = &punkta; Przypisanie adresu obiektu punkta wskaźnikowi ptrpunkt. b) Przypisanie adresu punktu (tj. adresu znajdującego się w zmiennej wskaźnikowej ptrpunkt) czyli wykonanie operacji rzutowania wskaźnika klasy podstawowej CPunkt na wskaźnik klasy pochodnej CKolo. Formalnie nie istnieje bezpośrednia możliwość przypisania wskaźnikowi klasy pochodnej CKolo wskaźnika do klasy 5
podstawowej CPunkt. Przypisanie takie jest bardzo niebezpieczne. W tym przypadku kompilator nie przeprowadza konwersji typów. c) cout << "Punkt A (poprzez *ptrkolo): " << *ptrkolo << endl; cout << "Powierzchnia obiektu ptrkolo: " << ptrkolo->obliczobszar() << endl << endl; W pierwszej z powyższych instrukcji mamy do czynienia z próbą wypisania obiektu punkta klasy CPunkt za pomocą przeciążonego w klasie CKolo operatora << oraz zdereferowanego wskaźnika ptrkolo. W takiej sytuacji promień R ma wartość 0 lub przyjmuje pewną bliżej niezdefiniowaną wartość znajdującą się w pamięci pod adresem, który powinien zawierać daną składową R. Dzieje się tak dlatego, że składowa R w tym przypadku nie istnieje (wskaźnik ptrkolo wskazuje bowiem na obiekt klasy CPunkt). Podobnie w drugiej instrukcji, wykorzystujemy wskaźnik ptrkolo (wskazujący w rzeczywistości na obiekt klasy CPunkt) do obliczenia pola obszaru koła. Wyznaczona powierzchnia ma wartość 0 lub inną wartość niezdefiniowaną, gdyż do stosownych obliczeń użyto nieokreśloną bliżej wartość promienia R. Co więcej należy pamiętać, że wywołanie w programie nie istniejącej funkcji składowej klasy (w tym przypadku funkcji, która nie istnieje w klasie CPunkt) może spowodować zakończenie działania programu i wyświetlenie komunikatu o błędzie. Zadanie 2 Cześć A: Implementacja klas. Napisać program umożliwiający tworzenie oraz dalsze wykorzystanie obiektów dwóch klas: COsoba oraz CPracownik. Klasą bazową jest klasa COsoba. W klasie tej przewidziane są chronione dane składowe: nazwisko (typu string) oraz wiek (typu int; wiek osoby powinien być z zakresu od 0 do 120). W klasie powinien zostać zdefiniowany konstruktor domyślny, który umożliwia również nadanie pewnych domyślnych wartości danym składowym klasy (nazwisko = "", wiek = 0). Wykorzystuje on odpowiednie funkcje dostępowe Set zdefiniowane w obiekcie (funkcje te nadają wartości danym składowym w klasie oraz sprawdzają poprawność zaproponowanych przez użytkownika danych). Ponadto w klasie powinny zostać zdefiniowane funkcje dostępowe Get (określone jako funkcje stałe), funkcja stała Drukuj wyświetlająca informacje o osobie na ekranie oraz działający w podobny sposób przeciążony operator wstawiania do strumienia <<. Drugą klasą jest klasa CPracownik, która dziedziczy dane i funkcje składowe klasy COsoba. Co więcej w klasie tej przewidziana jest prywatna dana składowa: zarobek_mies (typu int; zarobek pracownika podany w walucie PLN powinien być z zakresu od 0 do 5000). W klasie CPracownik powinien się znaleźć również odpowiednio: konstruktor domyślny, który umożliwia również nadanie pewnych domyślne wartości danym składowym klasy (nazwisko = "", wiek = 0, zarobek_mies = 0), funkcje Set, stałe funkcje Get, stała funkcja Drukuj wyświetlająca informacje o pracowniku na ekranie oraz działający w podobny sposób przeciążony operator wstawiania do strumienia <<. Dodatkowo w klasie powinna zostać zdefiniowana funkcja składowa Dochod_Roczny. Zadaniem funkcji jest przekazywanie do programu (przez return) rocznego dochodu uzyskanego przez pracownika (12 x zarobek_mies). Cześć B: Zadania testowe w programie. 1) Zadeklaruj w programie obiekt osobaa typu COsoba (przyjmujemy, że dane o osobie będą określone w dalszej części programu) oraz obiekt praca typu CPracownik (dane o pracowniku nadane zostaną przy tworzeniu obiektu). 2) Nadaj wartości danym obiektu osobaa wykorzystując do tego funkcje dostępowe Set. 3) Wypisz na ekranie dane o osobie osobaa wykorzystując do tego funkcję Drukuj oraz dane o pracowniku praca wykorzystując do tego przeciążony operator <<. 4) Zadeklaruj wskaźnik ptrosoba do typu COsoba oraz ptrpracownik do typu CPracownik. 5) Przypisz adres obiektu osobaa dla wskaźnika ptrosoba oraz adres obiektu praca dla wskaźnika ptrpracownik. 6) Wypisz na ekranie dane o osobie osobaa oraz pracowniku praca wykorzystując do tego funkcję Drukuj oraz wskaźniki ptrosoba i ptrpracownik. 7) Zadeklaruj wskaźnik ptr1 do typu COsoba i zainicjuj go adresem obiektu praca. 8) Wypisz na ekranie informacje o pracowniku praca wykorzystując do tego przeciążony operator << oraz zdereferowany wskaźnik ptr1. 9) Zadeklaruj wskaźnik ptr2 do typu COsoba i zainicjuj go adresem obiektu praca. 10) Zadeklaruj wskaźnik ptr3 do typu CPracownik. 11) Wykonaj operację rzutowania wskaźnika ptr2 do klasy COsoba na wskaźnik ptr3 klasy CPracownik. 12) Wykorzystując operator << oraz zdereferowany wskaźnik ptr3 wypisz na ekranie informacje o pracowniku praca. 13) Przypisz adres ptrosoba dla wskaźnika ptr2. 14) Wykonaj operację rzutowania wskaźnika ptr2 na wskaźnik ptrpracownik. 15) Wykorzystując operator << oraz wskaźnik ptrpracownik wypisz na ekranie informacje o pracowniku. 6