Programowanie w Języku C 2 Laboratorium 10 Dziedziczenie i polimorfizm Opracował: mgr inż. Leszek Ciopiński Wstęp teoretyczny Wst p - Co to jest dziedziczenie Cz sto podczas tworzenia klasy napotykamy na sytuacj, w której klasa ta powi ksza mo liwo ci innej klasy, nierzadko precyzuj c jednocze nie jej funkcjonalno. Dziedziczenie daje nam mo liwo wykorzystania nowych klas w oparciu o stare klasy. Nie nale y jednak traktowa dziedziczenia jedynie jako sposobu na wspó dzielenie kodu mi dzy klasami. Dzi ki mechanizmowi rzutowania mo liwe jest interpretowanie obiektu klasy tak, jakby by obiektem klasy z której si wywodzi. Umo liwia to skonstruowanie szeregu klas wywodz cych si z tej samej klasy i korzystanie w przejrzysty i spójny sposób z ich wspólnych mo liwo ci. Nale y doda, e dziedziczenie jest jednym z czterech elementów programowania obiektowego (obok abstrakcji, enkapsulacji i polimorfizmu). Klas z której dziedziczymy nazywamy klas bazow, za klas, która po niej dziedziczy nazywamy klas pochodn. Klasa pochodna mo e korzysta z funkcjonalno ci klasy bazowej i z za o enia powinna rozszerza jej mo liwo ci (poprzez dodanie nowych metod, lub modyfikacj metod klasy bazowej). Sk adnia Sk adnia dziedziczenia jest bardzo prosta. Przy definicji klasy nale y zaznaczy po których klasach dziedziczymy. Nale y tu zaznaczy, e C++ umo liwia Wielodziedziczenie, czyli dziedziczenie po wielu klasach na raz. Jest ono opisane w rozdziale Dziedziczenie wielokrotne. class nazwa_klasy :[operator_widocznosci] nazwa_klasy_bazowej, [operator_widocznosci] nazwa_klasy_bazowej... definicja_klasy ; operator_widoczno ci mo e przyjmowa jedn z trzech warto ci: public, protected, private. Operator widoczno ci przy klasie, z której dziedziczymy pozwala ograniczy widoczno elementów publicznych z klasy bazowej. public - oznacza, e dziedziczone elementy (np. zmienne lub funkcje) maj tak Programowanie w Języku C2 Laboratorium strona: 1 z 10
widoczno jak w klasie bazowej. public public protected protected private brak dost pu w klasie pochodnej protected - oznacza, e elementy publiczne zmieniaj si w chronione. public protected protected protected private brak dost pu w klasie pochodnej private - oznacza, e wszystkie elementy klasy bazowej zmieniaj si w prywatne. public private protected private private brak dost pu w klasie pochodnej brak operatora - oznacza, e niejawnie (domy lnie) zostanie wybrany operator private.. public private protected private private brak dost pu w klasie pochodnej Dost p do elementów klasy bazowej mo na uzyska jawnie w nast puj cy sposób: [klasa_bazowa::...]klasa_bazowa::element Zapis ten umo liwia dost p do elementów klasy bazowej, które s "przykryte" przez elementy klasy nadrz dnej (maj takie same nazwy jak elementy klasy nadrz dnej). Je eli nie zaznaczymy jawnie o który element nam chodzi kompilator uzna e chodzi o element klasy nadrz dnej, o ile taki istnieje (przeszukiwanie b dzie prowadzone w g b a kompilator znajdzie "najbli szy" element). Przyk ad 1 Definicja i sposób wykorzystania dziedziczenia Najcz stszym powodem korzystania z dziedziczenia podczas tworzenia klasy jest ch sprecyzowania funkcjonalno ci jakiej klasy wraz z implementacj tej funkcjonalno ci. Pozwala to na rozró nianie obiektów klas i jednocze nie umo liwia stworzenie funkcji korzystaj cych ze wspólnych cech tych klas. Za ó my e piszemy program symuluj cy zachowanie zwierz t. Ka de zwierze powinno móc je. Tworzymy odpowiedni klas : class Zwierze Zwierze(); void jedz(); ; Nast pnie okazuje si, e musimy zaimplementowac klasy Ptak i Ryba. Ka dy ptak i ryba jest zwierz ciem. Oprócz tego ptak mo e lata, a ryba p yn. Wykorzystanie dziedziczenia wydaje si tu naturalne. class Ptak : public Zwierze Ptak(); Programowanie w Języku C2 Laboratorium strona: 2 z 10
; void lec(); class Ryba : public Zwierze Ryba(); void plyn(); ; Co istotne tworz c takie klasy mo emy wywo a ich metod pochodz c z klasy Zwierze: Ptak ptak; ptak.jedz(); //metoda z klasy Zwierze ptak.lec(); //metoda z klasy Ptak Ryba *ryba=new Ryba(); ryba->jedz(); //metoda z klasy Zwierze ryba->plyn(); //metoda z klasy Ryba Mo emy te zrzutowa obiekty klasy Ptak i Ryba na klas Zwierze: Ptak *ptak=new Ptak(); Zwierze *zwierze; zwierze=ptak; zwierze->jedz(); Ryba ryba; ((Zwierze)ryba).jedz(); Je eli tego nie zrobimy, a rzutowanie jest potrzebne, kompilator sam wykona rzutowanie niejawne: Zwierze zwierzeta[2]; zwierzeta[0]=ryba(); //rzutowanie niejawne zwierzeta[1]=ptak(); //rzutowanie niejawne for (int i=0; i<2; ++i) zwierzeta[i].jedz(); Dost p do elementów przykrytych Elementy chronione - operator widoczno ci protected Sekcja protected klasy jest c i le zwi zana z dziedziczeniem - elementy i metody klasy, które si w niej znajduj, mog by swobodnie u ywane w klasie dziedzicznej ale poza klas dziedziczn i klas bazow nie s widoczne. Elementy powi zane z dziedziczeniem Chcia bym zwróci uwag na inne, bardzo istotne elementy dziedziczenia, które s opisane w nast pnych rozdzia ach tego podr cznika, a które mog by wr cz niezb dne w prawid owym korzystaniu z dziedziczenia (przede wszystkim Funkcje wirtualne). Funkcje wirtualne Programowanie w Języku C2 Laboratorium strona: 3 z 10
Przykrywanie metod, czyli definiowanie metod w klasie pochodnej o nazwie i parametrach takich samych jak w klasie bazowej, ma zwykle na celu przystosowanie metody do nowej funkcjonalno ci klasy. Bardzo cz sto wywo anie metody klasy bazowej mo e prowadzi wr cz do katastrofy, poniewa nie bierze ona pod uwag zmian miedzy klas bazow a pochodn. Problem powstaje, kiedy nie wiemy jaka jest klasa nadrz dna obiektu, a chcieliby my e by zawsze by a wywo ywana metoda klasy pochodnej. W tym celu j zyk C++ posiada funkcje wirtualne. S one opisane w rozdziale Funkcje wirtualne. Wielodziedziczenie - czyli dziedziczenie wielokrotne J zyk C++ umo liwia dziedziczenie po wielu klasach bazowych na raz. Proces ten jest opisany w rozdziale Dziedziczenie wielokrotne. Przyk ad 2 #include <iostream> class Zwierze Zwierze() void jedz( ) for ( int i=0; i<10; ++i ) std::cout << "Om Nom Nom Nom\n"; void pij( ) for ( int i=0; i<5; ++i ) std::cout << "Chlip, chlip\n"; ; void spij( ) std::cout << "Chrr...\n"; class Pies : public Zwierze Pies() void szczekaj() std::cout << "Hau! hau!...\n"; Programowanie w Języku C2 Laboratorium strona: 4 z 10
; void warcz() std::cout << "Wrrrrrr...\n";... Za pomoc... class Pies : public Zwierze... utworzyli my klas Psa, która dziedziczy klas Zwierze. Dziedziczenie umo liwia przekazanie zmiennych, metod itp. z jednej klasy do drugiej. Mo emy funkcj main zapisa w ten sposób:... int main() Pies burek; burek.jedz(); burek.pij(); burek.warcz(); burek.pij(); burek.szczekaj(); burek.spij(); return 0; Źródło: http://pl.wikibooks.org/wiki/c%2b%2b/dziedziczenie [dostęp: 28 listopada 2011] Funkcje wirtualne to specjalne funkcje sk adowe, które przydaj si szczególnie, gdy u ywamy obiektów pos uguj c si wska nikami lub referencjami do nich. Dla zwyk ych funkcji z identycznymi nazwami to, czy zostanie wywo ana funkcja z klasy podstawowej, czy pochodnej, zale y od typu wska nika, a nie tego, na co faktycznie on wskazuje. Dysponuj c funkcjami wirtualnymi b dziemy mogli u y prawdziwego polimorfizmu - u ywa metod klasy pochodnej wsz dzie tam, gdzie spodziewana jest klasa podstawowa. W ten sposób b dziemy mogli korzysta z metod klasy pochodnej korzystaj c ze wska nika, którego typ odnosi si do klasy podstawowej. W tej chwili mo e si to wydawa niepraktyczne, lecz za chwil przekonasz si, e funkcje wirtualne nios naprawd sporo nowych mo liwo ci. Opis Na pocz tek rozpatrzymy przyk ad, który poka e, dlaczego zwyk e, niewirtualne funkcje sk adowe nie zdaj egzaminu gdy pos ugujemy si wska nikiem, który mo e wskazywa i na obiekt klasy podstawowej i na obiekt dowolnej z jej klas pochodnych. Maj c klas bazow wyprowadzamy od niej klas pochodn : Programowanie w Języku C2 Laboratorium strona: 5 z 10
class Baza void pisz() std::cout << "Tu funkcja pisz z klasy Baza" << std::endl; ; class Baza2 : public Baza void pisz() std::cout << "Tu funkcja pisz z klasy Baza2" << std::endl; ; Je eli teraz w funkcji main stworzymy wska nik do obiektu typu Baza, to mo emy ten wska nik ustawia na dowolne obiekty tego typu. Mo na te ustawi go na obiekt typu pochodnego, czyli Baza2: int main() Baza *wsk; Baza objb; Baza2 objb2; wsk = &objb; wsk -> pisz(); // Teraz ustawiamy wska nik wsk na obiekt typu pochodnego wsk = &objb2; wsk -> pisz(); return 0; Po skompilowaniu na ekranie zobaczymy dwa wypisy: "Tu funkcja pisz z klasy Baza". Sta o si tak dlatego, e wska nik jest do typu Baza. Gdy ustawili my wska nik na obiekt typu pochodnego (wolno nam), a nast pnie wywo ali my funkcj sk adow, to kompilator "na l epo" si gn po funkcj pisz z klasy bazowej (bo wska nik wskazuje na klas bazow ). Mo na jednak okre li e by kompilator nie si ga po funkcj z klasy bazowej, ale sam si zorientowa na co wska nik pokazuje. Do tego s u y przydomek virtual, a funkcja sk adowa nim oznaczona nazywa si wirtualn. Ró nica polega tylko na dodaniu s owa kluczowego virtual, co wygl da tak: class Baza virtual void pisz() Programowanie w Języku C2 Laboratorium strona: 6 z 10
std::cout << "Tu funkcja pisz z klasy baza" << std::endl; ; class Baza2 : public Baza virtual void pisz() std::cout << "Tu funkcja pisz z klasy Baza2" << std::endl; ; Konsekwencje Gdy funkcja jest oznaczona jako wirtualna, kompilator nie przypisuje na sta e wywo ania funkcji z tej klasy, na któr pokazuje wska nik, ju podczas kompilacji. Pozostawia decyzj co do wyboru w a ciwej wersji funkcji a do momentu wykonania programu - jest to tzw. pó ne wi zanie. Wtedy program skorzysta z krótkiej informacji zapisanej w obiekcie a okre laj cej klas, do jakiej nale y dany obiekt. Dopiero po odczytaniu informacji o klasie danego obiektu wybierana jest w a ciwa metoda. Je li klasa ma cho jedn funkcj wirtualn, to do ka dego jej obiektu dopisywany jest identyfikator tej klasy a do wywo ania funkcji dopisywany jest kod, który ten identyfikator czyta i odnajduje odpowiedni funkcj. Gdy klasa funkcji wirtualnych nie posiada, takie informacje nie s dodawane, bo nie s potrzebne. Zauwa my te, e nie zawsze decyzja o wyborze funkcji jest dokonywana dopiero na etapie wykonania. Gdy do obiektów odnosimy si przez zmienn, a nie przez wska nik lub referencj to kompilator ju na etapie kompilacji wie, jaki jest typ (klasa) danej zmiennej (bo do zmiennej w przeciwie stwie do wska nika lub referencji nie mo na przypisa klasy pochodnej). Tak wi c wirtualno nie gra roli gdy nie u ywamy wska ników; kompilator generuje wtedy taki sam kod, jakby wszystkie funkcje by y niewirtualne. Przy wska nikach musi orientowa si czytaj c informacj o klasie obiektu, na który wskazuje wska nik, bo mogliby my np. losowa, czy do wska nika przypiszemy klas bazow czy jej pochodn - wtedy przy ka dym uruchomieniu programu by aby wywo ywana inna funkcja. Jak wida, za wirtualno si p aci - zarówno drobnym narzutem pami ciowym na ka dy obiekt (identyfikator klasy), jak i drobnym narzutem czasowym (odnajdywanie przy ka dym wywo aniu odpowiedniej klasy i jej funkcji sk adowej). Jednak zyskujemy mo liwo c p ynnego rozwoju naszego programu przez zast powanie klas ich podklasami, co bez wirtualno ci jest niewykonalne. Przy mo liwo ciach obecnych komputerów koszt wirtualno ci jest zaniedbywalny, ale wci warto przemy le, czy potrzebujemy wirtualno ci dla wszystkich funkcji. Przyk ad Poni szy program zawiera deklaracje 3 klas: Figura, Kwadrat i Kolo. W klasie Figura zosta a zadeklarowana metoda wirtualna (s owo kluczowe virtual) Programowanie w Języku C2 Laboratorium strona: 7 z 10
virtual float pole(). Ka da z klas pochodnych od klasy Figura ma zaimplementowane swoje metody float pole(). Nast pnie (w funkcji main) znajduj si deklaracje obiektów ka dej z klas i wska nika mog cego pokazywa na obiekty klasy bazowej Figura. #include <iostream> const float pi = 3.14159; class Figura virtual float pole() const return -1.0; ; class Kwadrat : public Figura Kwadrat( const float bok ) : a( bok ) float pole() const return a * a; private: float a; // bok kwadratu ; class Kolo : public Figura Kolo( const float promien ) : r( promien ) float pole() const return pi * r * r; private: float r; // promien kola ; void wyswietlpole( Figura& figura ) std::cout << figura.pole() << std::endl; return; int main() // deklaracje obiektow: Figura jakasfigura; Programowanie w Języku C2 Laboratorium strona: 8 z 10
Kwadrat jakiskwadrat( 5 ); Kolo jakieskolo( 3 ); Figura* wskjakasfigura = 0; // deklaracja wska nika // obiekty ------------------------------- std::cout << jakasfigura.pole() << std::endl; // wynik: -1 std::cout << jakiskwadrat.pole() << std::endl; // wynik: 25 std::cout << jakieskolo.pole() << std::endl; // wynik: 28.274... // wskazniki ----------------------------- wskjakasfigura = &jakasfigura; std::cout << wskjakasfigura->pole() << std::endl; // wynik: -1 wskjakasfigura = &jakiskwadrat; std::cout << wskjakasfigura->pole() << std::endl; // wynik: 25 wskjakasfigura = &jakieskolo; std::cout << wskjakasfigura->pole() << std::endl; // wynik: 28.274... // referencje ----------------------------- wyswietlpole( jakasfigura ); // wynik: -1 wyswietlpole( jakiskwadrat ); // wynik: 25 wyswietlpole( jakieskolo ); // wynik: 28.274... return 0; Wywo anie metod sk adowych dla ka dego z obiektów powoduje wykonanie metody odpowiedniej dla klasy danego obiektu. Nast pnie wska nikowi wskjakasfigura zostaje przypisany adres obiektu jakasfigura i zostaje wywo ana metoda float pole(). Wynikiem jest "-1" zgodnie z tre ci metody float pole() w klasie Figura. Nast pnie przypisujemy wska nikowi adres obiektu klasy Kwadrat - mo emy tak zrobi poniewa klasa Kwadrat jest klas pochodn od klasy Figura - jest to tzw. rzutowanie w gór. Wywo anie teraz metody float pole() dla wskaznika nie spowoduje wykonania metody zgodnej z typem wska nika - który jest typu Figura* lecz zgodnie z aktualnie wskazywanym obiektem, a wi c wykonana zostanie metoda float pole() z klasy Kwadrat (gdy ostatnie przypisanie wska nikowi warto ci przypisywa o mu adres obiektu klasy Kwadrat). Analogiczna sytuacja dzieje si gdy przypiszemy wska nikowi adres obiektu klasy Kolo. Nast pnie zostaje wykonana funkcja void wyswietlpole(figura&) która przyjmuje jako parametr obiekt klasy Figura przez referencj. Tutaj równie zosta y wykonane odpowiednie metody dla obiektów klas pochodnych a nie metoda zgodna z obiektem jaki jest zadeklarowany jako parametr funkcji czyli float Figura::pole(). Takie dzia anie jest spowodowane przez przyjmowanie obiektu klasy Figura przez referencj. Gdyby obiekty by y przyjmowane przez warto (parametr bez &) zosta aby wykonana 3 krotnie metoda float Figura::pole() i 3 krotnie wy wietlona warto -1. Wy ej opisane dzia anie zosta o spowodowane przez okre lenie metody w klasie Programowanie w Języku C2 Laboratorium strona: 9 z 10
bazowej jako wirtualnej. Gdyby zosta o usuni te s owo kluczowe virtual w deklaracji metody w klasie bazowej, zosta yby wykonane metody zgodne z typem wska nika lub referencji, a wi c za ka dym razem zosta aby wykonana metoda float pole() z klasy Figura. Źródło: http://pl.wikibooks.org/wiki/c%2b%2b/funkcje_wirtualne [dostęp: 28 listopada 2011] Zadania Napisz program, w którym należy: zadeklarować klasę abstrakcyjną określającą jakiś rodzaj bytu (np. drzewo, kształt) i posiadającą przykładowe pola, z których część będzie miała rodzaj widoczności protected. (1 punkt) w klasie bazowej z poprzedniego punktu umieść metodę, która będzie w tej klasie w pełni zaimplementowana. (1 punkt) utworzyć trzy klasy pochodne, dziedziczące po klasie z punktu pierwszego, które będą implementowały wszystkie metody wirtualne. (1 punkt) dla jednej z klas z punktu poprzedniego utwórz klasę pochodną, w której należy rozwinąć działanie jednej z metod z klasy bazowej (np.: podanie dodatkowej informacji podczas wyświetlania zawartości klasy) (1 punkt) zaimplementować część testującą w ten sposób, że użytkownik będzie mógł wybrać którą spośród czterech klas chce testować, a następnie program ma umożliwić wywołanie wszystkich metod tej klasy poprzez pracę na rzucie tej klasy na typ bazowy. (1 punkt). Programowanie w Języku C2 Laboratorium strona: 10 z 10