Programowanie obiektowe III rok EiT dr inż. Jerzy Kotowski Wykład XIII
Program wykładu Język C++ –Dziedziczenie Przykład problemu Podstawy języka JAVA Klasówka
Literatura C++ for C programmers, Ira Pohl, The Benjamin/Cummings Publishing Company, Inc. Ćwiczenia z języka C++, Claude Delannoy Symfonia C++, Jerzy Grębosz, Oficyna Kallimach, Kraków 1999
Dziedziczenie – terminologia i podstawy Inherit - dziedziczyć Inheritance – dziedziczenie Derive – pochodzić Deriving – pochodzący Derived – pochodny A derived class – klasa pochodna Virtual - czynny, prawdziwy, faktyczny Pojęcie dziedziczenia należy do podstaw programowania obiektowego. Dziedziczenie jest mechanizmem wyprowadzania nowej klasy ze starej. Dziedziczenie umożliwia zdefiniowanie nowej klasy B, nazywanej pochodną, na podstawie już istniejącej klasy A, nazywanej bazową. Wiele struktur to warianty jednej podstawowej struktury i często jest nudnym produkowanie tego samego kodu dla każdej klasy pochodnej z osobna.
Dziedziczenie – podstawowe idee Klasa pochodna dziedziczy opis podstawowej klasy. Może on być następnie zmieniony przez dodawanie nowych składników, polimorfizm istniejących już member functions i modyfikowanie przywilejów związanych z regułami dostępu. Idea: Klasyfikacja bazowa z reguły obejmuje rozległe obszary wiedzy. Przykład: znając definicję ssaka i wiedząc, że zarówno mysz jak i słoń to ssaki można stworzyć ich opis w sposób dużo bardziej zwięzły niż przy wykorzystywaniu innych technik. Virtual member functions – funkcje wirtualne. Są to funkcje deklarowane w klasie bazowej i overloaded w klasach pochodnych. Hierarchia ADT, definiowana przez proces dziedziczenia tworzy relację w zbiorze typów zdefiniowanych przez użytkownika. Obiekty typów pochodnych mogą być wskazywane przez wskaźnik do klasy bazowej. Pozwala to na dynamiczne rozróżnianie typów. Jest to typowa cecha OOP. Czyste funkcje wirtualne klasy abstrakcyjne.
Klasa pochodna Klasa może pochodzić (can be derived) od klasy istniejącej przy pomocy konstrukcji: class class_name: (public|private) base_class {…} public|private są opcjonalne. Jak zwykle słowo kluczowe class może być zastąpione przez słowo kluczowe struct. Wtedy wszystkie składniki klasy są przez domniemanie public. Najbardziej skomplikowanym aspektem klas pochodnych jest widzialność ich dziedziczonych składników. Jedno z trzech słów kluczowych public, private, protected może być opcjonalnie wykorzystane do wyspecyfikowania jak składniki klasy bazowej mają być dostępne w klasie pochodnej. Słowo kluczowe public po dwukropku w nagłówku klasy pochodnej oznacza, że wszystkie public members klasy bazowej są public members klasy pochodnej. Klasa pochodna może być restrykcyjna. Może zmniejszać widzialność swoich składników i/lub zmieniać ich znaczenie.
Modyfikatory widzialności Słowa kluczowe public, private i protected mogą być wykorzystane jako modyfikatory widzialności składników klasy. A public member jest widzialny w obszarze dostępności całego obiektu. A private member jest widzialny przez wszystkie funkcje składowe wewnątrz swojej klasy. A protected member jest widzialny przez wszystkie funkcje składowe wewnątrz swojej klasy oraz przez funkcje składowe klasy bezpośrednio dziedziczącej. Modyfikatory widzialności mogą być używane wewnątrz klasy w dowolnej kolejności i dowolną liczbę razy. Modyfikatory widzialności mogą być wykorzystywane w nagłówku klasy pochodnej do ograniczenia widzialności elementów klasy bazowej. W razie potrzeby, dla wybranych elementów klasy bazowej rodzaj widzialności można zmieniać indywidualnie wewnątrz klasy pochodnej.
Dostęp do składowych klasy bazowej W przypadku dziedziczenia publicznego wszystkie składowe publiczne klasy bazowej stają się składowymi publicznymi klasy pochodnej. W przypadku dziedziczenia prywatnego, składowe publiczne klasy bazowej stają się składowymi prywatnymi klasy pochodnej. Użytkownik klasy pochodnej nie ma dostępu do składowych prywatnych klasy bazowej. W obu typach dziedziczenia składowe prywatne klasy bazowej nie są udostępniane funkcjom składowym klasy pochodnej. Jest to OCZYWISTE: Mechanizm dziedziczenia nie może łamać reguł dostępu wprowadzonych przez twórcę klasy bazowej.
Dostęp do składowych klasy bazowej, c.d. Dziedziczenie prywatne jest stosunkowo rzadkie i używane w szczególnych przypadkach (całkowita zmiana interfejsu klasy). Składowe klasy bazowej mogą być chronione ( protected ). W tym przypadku są one uważane za: –publiczne dla funkcji składowych klasy pochodnej, –prywatne dla użytkownika klasy pochodnej. Nawet jeżeli jakaś składowa z klasy bazowej (dana lub funkcja) została przedefiniowana w klasie pochodnej, to funkcje składowe i użytkownik klasy pochodnej mogą w dalszym ciągu z niej korzystać jeżeli nie jest to zabronione. Dostęp do przedefiniowanej składowej umożliwia operator zakresu ::
Wywołanie konstruktorów i destruktorów Niech B będzie klasą pochodną do klasy bazowej A. Jeżeli B ma jakiś konstruktor, to utworzenie obiektu typu B powoduje zawsze niejawne wywołanie konstruktora. W konstruktorze B powinno się przewidzieć argumenty dla konstruktora A, chyba że A nie ma konstruktora lub ma konstruktor bezargumentowy. Argumenty dla konstruktora A są podawane jak w przykładzie: B(int x, int y, char kol): A(x,y); Argumenty dla A mogą mieć postać wyrażeń. Konstruktory i destruktory nie są dziedziczone. Nie jest również dziedziczony operator przypisania (podstawiania) nawet jeżeli został przeciążony w klasie bazowej. Uzasadnienie: Jeżeli dokonano takiego przeciążenia to znaczy, że czynność ta jest nietypowa i należy traktować ją ze szczególną uwagą również w klasie pochodnej.
Przypadek konstruktora kopiującego Jeżeli klasa B nie ma konstruktora kopiującego to jest wywoływany jej automatyczny konstruktor kopiujący. Działa on następująco: –wywołuje konstruktor kopiujący z A (automatyczny lub kopiujący) –inicjalizuje te dane składowe B, które nie pochodzą z A. Jeżeli w klasie pochodnej został jawnie zdefiniowany konstruktor kopiujący to jego wywołanie spowoduje: –wywołanie konstruktora klasy bazowej wymienionej w nagłówku jak w przykładzie: B(B& b): A(b); wywołanie konstruktora kopiującego z A, do którego zostanie przekazana część B odziedziczona a A. –wywołanie konstruktora bezargumentowego (domniemanego), jeżeli w nagłówku nie został wymieniony żaden konstruktor. Jeżeli klasa bazowa nie ma konstruktora bezargumentowego to wystąpią błędy kompilacji.
Zgodność obiektu klasy bazowej i obiektu klasy pochodnej Niech B będzie klasą pochodną do klasy bazowej A. A a; obiekt typu bazowego B b; obiekt typu pochodnego A *wska; wskaźnik obiektu typu bazowego B *wskb; wskaźnik obiektu typu pochodnego W języku C++ mają miejsce dwie poniższe niejawne konwersje: –Przypisanie a=b; jest legalne i polega na przekształceniu b do typu A, czyli rozważeniu w A tylko składowych należących także do A i przypisaniu wyniku do a. Jest przy tym wywoływany operator przypisania z A, przeciążony lub automatyczny. Przypisanie odwrotne, czyli b=a; jest zabronione. –Przypisanie wskaźnika do klasy pochodnej we wskaźnik do klasy bazowej. Można napisać przykładowo: wska=wskb; natomiast napisanie bezpośrednio wskb=wska; jest zabronione. Ewentualnie można użyć rzutu: wskb=(B *)wska;
Dziedziczenie wielobazowe (wielokrotne) Dziedziczenie wielobazowe umożliwia dziedziczenie po kilku klasach jak w przykładzie: class B: public A1, public A2 Każde dziedziczenie może być publiczne lub prywatne. Wykorzystanie składowych każdej z klas bazowych jest podobne jak w przypadku zwykłego dziedziczenia. W przypadku napisu: class B: public A1, A2 dziedziczenie po klasie A2 będzie prywatne przez domniemanie. Operator zakresu :: może być używany: –jeżeli chce się użyć składowej klasy bazowej, przedefiniowanej w klasie pochodnej, albo –gdy dwie klasy bazowe mają składową o tej samej nazwie i trzeba sprecyzować o którą chodzi.
Wywołania konstruktorów i destruktorów Utworzenie obiektu pociąga za sobą wywołania konstruktorów klas bazowych w takiej kolejności w jakiej są wymienione w deklaracji klasy pochodnej (od lewej do prawej) W nagłówku konstruktora klasy pochodnej można wymienić informacje do przekazania każdemu z konstruktorów klas bazowych. Przykład: B(typ1 a1, typ2 a2, typ3 a3): A1(a1,a2), A2(a3) {} Nie jest to obowiązkowe, gdy klasa bazowa ma konstruktor bezargumentowy (domniemany) lub gdy nie ma konstruktora. Przypadek ogólny: klasa pochodna ma składowe, które same w sobie są obiektami innych klas, które też mogą być klasami pochodnymi. Problem kolejności wywołania konstruktorów (J. Grębosz): Klasa uszanuje najpierw starszych, potem swoich gości, a dopiero na samym końcu zajmie się sobą. Jeżeli dziadek miał swojego gościa, to ta sprawa zostanie załatwiona już przy konstrukcji dziadka (J. Grębosz).
Przykład – klasa punkt_kolorowy1.. \test0.sln.. \test0.sln punkt_kolorowy1.cpp Idea: tworzymy klasę pochodną ze znanej od dawna klasy punkt dodając nową składową o nazwie kolor. Klasa punkt jest znana. Interesujące elementy programu: –punkt_kolorowy(int=0, int=0, int=0); –Definicja konstruktora klasy pochodnej punkt_kolorowy::punkt_kolorowy(int xx, int yy, int kolor): punkt(xx,yy) { kol=kolor; } –Alternatywa: punkt_kolorowy::punkt_kolorowy(int xx, int yy, int kolor): punkt(xx,yy), kol(kolor){} –Wywołanie funkcji z klasy bazowej –Niebezpieczeństwo nieskończonej rekursji –Wywołania funkcji operator !() –Niejawne wywołanie destruktora klasy bazowej
Klasy wirtualne Przy kilkustopniowym dziedziczeniu może zajść sytuacja, w której dana klasa będzie dziedziczyć dwukrotnie po innej klasie. Przykład: class B: public A class C: public A class D: public B, public C Składowe klasy A pojawiają się dwukrotnie w klasie D. Aby tego uniknąć trzeba w deklaracjach pochodnych klas B i C zadeklarować z atrybutem virtual klasę, której powielania chce się uniknąć. Słowo virtual może być umieszczone zarówno po jak i przed modyfikatorem widzialności: class B: virtual public A class C: public virtual A Konstruktory klas pochodnych muszą przekazywać argumenty konstruktorowi klasy wirtualnej w sposób jawny: D(arg dla D):B(arg dla B),C(arg dla C),A(arg dla A) W konstruktorach B i C, w których zadeklarowano klasę wirtualną nie trzeba umieszczać wywołania konstruktora klasy wirtualnej A. Konstruktor klasy wirtualnej jest zawsze wywoływany na początku.
Przykład – amfibia.cpp.. \test0.sln.. \test0.sln Idea: Klasa samochod i klasa boat dziedziczą po klasie pojazd. Klasa amfibia dziedziczy po obu klasach samochod i boat. Klasa pojazd przechowuje nazwę typu pojazdu. Dlatego ma konstruktor z dynamiczną rezerwacją pamięci. Destruktor, nie powinien być dwa razy wywoływany. Wymaga to odpowiedniej precyzji: –Klasy samochod i boat mają po dwa konstruktory – jeden dla tworzenia obiektu swojego typu a drugi na potrzeby dziedziczenia wielokrotnego. –Klasa pojazd ma konstruktor z domniemanymi argumentami – aby oszukać kompilator gdy analizuje konstruktor z mniejszą liczbą argumentów w klasie samochod i boat. –Funkcja nazwa w klasie pojazd pozwala na ominięcie ograniczeń na dostęp do pola s w klasach pochodnych. –Wywołania destruktorów w kodzie klienta.
Funkcje wirtualne – sformułowanie problemu Klasyka: Niezależnie od tego na co wskazuje wskaźnik to jest to zawsze wskaźnik do typu wynikającego z deklaracji wskaźnika. Dlatego wska->fun(); jest zawsze wywołaniem składowej z klasy bazowej. Statyczne rozpoznawanie typu – kompilator Dynamiczne rozpoznawanie typu – program Język C++ pozwala na dynamiczne rozpoznawanie typu z powodu omówionej wcześniej zgodności klasy bazowej z klasą pochodną. Jeżeli w klasie bazowej zadeklarujemy funkcję wirtualną przy pomocy słowa kluczowego virtual, to wywołania tej funkcji i funkcji w klasach pochodnych będą wybierane wyłącznie w zależności od rzeczywistego typu obiektu. Nazywa się to dynamicznym nadawaniem typu lub dynamicznym wiązaniem funkcji. class A {.... public: void fun(...);..... } a; class B: public A {..... public: void fun(....); } b;..... A *wska = &a; B *wskb = &b; wska = wskb;
Funkcje wirtualne – reguły stosowania Słowo kluczowe virtual jest używane tylko raz dla danej funkcji i nie powinno być używane dla funkcji przedefiniowywanych w klasach pochodnych. Metoda zadeklarowana w klasie bazowej jako wirtualna nie musi być przedefiniowywana w klasach pochodnych. Funkcja wirtualna może być przeciążona. Każda funkcja przeciążona może być wirtualna ale nie musi. Konstruktor nie może być wirtualny a destruktor może. Wiązanie dynamiczne jest z oczywistych względów wykorzystywane tylko przy hierarchii klas (dziedziczeniu).
Przykład – klasa punkt_kolorowy2.cpp.. \test0.sln.. \test0.sln Tworzymy klasę pochodną z dwóch klas bazowych: klasy punkt oraz klasy o nazwie kolor. Klasa bazowa punkt posiada funkcję wirtualną kto. Funkcja ta jest przeciążana w klasie pochodnej punkt_kol. W klasie bazowej funkcja kto wyprowadza tekst: Jestem punktem. W klasie pochodnej funkcja kto wyprowadza wpierw tekst: Jestem kolorowym punktem w kolorze:. W następnej linii jest wyprowadzany kolor punktu. Operator zakresu nie jest obligatoryjny! Funkcja kto jest wykorzystywana przez funkcję składową print klasy bazowej punkt. Zgodnie z metodyką funkcji wirtualnych rodzaj użytej metody zależy od typu argumentu. Oznacza to, że efekt działania linii p.print(); zależy od typu p, to znaczy od tego, czy p jest obiektem typu bazowego punkt czy też typu pochodnego punkt_kol. Dynamiczne rozpoznawanie typu widać najlepiej w trzech ostatnich liniach segmentu main.