menu

darmowe eBooki

Delphi - 31 przydatnych programów

okładka

Naucz się pełniej wykorzystywać możliwości Delphi

Sprawdź sam, czytając darmowy fragment eBooka Delphi - 31 przydatnych programów.

Czy ta wiedza okazała się dla Ciebie przydatna? Chcesz wiedzieć więcej? Zobacz tutaj.

Google Adsense

migawka z forum

Dowcipy o programistach/informatykach

Witam, grunt to potrafić śmiać sie z samego siebie  ;DPrzychodzi informatyk do domu po pra...

Rysiek z "Klanu"

W związku z ogromnym poruszeniem we wszystkich mediach w naszym kraju, dotyczącym tak kultowej postaci jak Rysiek z &quo...

Propozycje zmian w serwisie Guide C++

Witam, ze względu na to że nie znalazłem podobnego tematu na forum, a mam kilka sugestii   ;) postanowiłem je ...

buttony

SDJ
vortal programistów
Vademecum sieci komputerowych
Soldiers'04 - fan klub Legii Warszawa
www.katalog.bajery.pl
katalog stron
katalog najlepszych stron
jestem w katalogu
Wszystko o C++
[zamknij]

Korzystamy z plików cookies i umożliwiamy zamieszczanie ich osobom trzecim. Pliki cookie pozwalają na poznanie twoich preferencji na podstawie zachowań w serwisie.
Uznajemy, że jeżeli kontynuujesz korzystanie z serwisu, wyrażasz na to zgodę.

jesteś w: Kurs C++ / Polimorfizm i dziedziczenie
Szkoła Hakerów - Kurs Hackingu Bez Cenzury

Polimorfizm i dziedziczenie

To jest chyba najistotniejszy rozdział w całym kursie. Dziedziczenie to specjalny mechanizm, który powstał w celu udoskonalenia już wspaniałych klas. Umożliwia on tworzenie klas pochodnych, czyli będących niejako rozbudowaną wersją klas już istniejących. W połączeniu z funkcjami wirtualnymi, czyli polimorfizmem stanowi świetne narzędzie programistyczne. Dopiero umiejętność tworzenia klas pochodnych i definiowania funkcji wirtualnych daje możliwość stworzenia stuprocentowego programu orientowanego obiektowo [nie mylić z programowaniem obiektowym, to dwie różne rzeczy!]. Zacznijmy od dziedziczenia.

dziedziczenie

Dziedziczenie jak już powiedziałem na wstępie to świetne narzędzie programistyczne. Ogólnie mówiąc polega ono na tworzeniu nowych klas na podstawie już istniejących. Tylko co to oznacza w praktyce? Aby łatwo było to zrozumieć znów mała analogia. Powiedzmy, że piszesz program w stylu wyścigów samochodowych. Podstawowym elementem programu będą właśnie owe pojazdy. Najwygodniej jest zrealizować to w postaci klas. Tak więc zdefiniujmy sobie taką klasę.

  1. class pojazd
  2. {
  3. public:
  4. int predkosc;
  5. int przyspieszenie;
  6. int ilosc_kol;
  7. int kolor;
  8. };

Taka mała klasa na początek powinna wystarczyć. Znajdują się w niej zaledwie cztery składniki opisujące pojazd. Dla ułatwienia wszystkie one są publiczne. Jeden pojazd to jednak zbyt mało. Chcesz dać graczowi możliwość wyboru i tworzysz pojazd z dopalaczem :)

  1. class super_pojazd
  2. {
  3. public:
  4. int predkosc;
  5. int przyspieszenie;
  6. int ilosc_kol;
  7. int kolor;
  8. int dopalacz;
  9. };

Przypatrz się teraz obu tym klasom. Czy zauważasz coś ciekawego? Obie klasy są niemal jednakowe. Odróżnia je zaledwie jeden detal. Jest to dopalacz w klasie drugiej. Zatem klasa super_pojazd jest jakby rozbudowaną klasą pojazd. Taka mała różnica i trzeba było definiować od nowa całą klasę. Czy nie można by zrobić tego jakoś szybciej i krócej? Okazuje się, że można. Wystarczy skorzystać tutaj z dziedziczenia. Realizacja tego jest na prawdę prosta, a późniejsze korzyści są ogromne. Zamiast pisać na piechotę całą klasę super_pojazd wystarczy poinformować kompilator, że ta klasa jest rozwiniętą wersją klasy pojazd. Robi się to tak:

  1. class super_pojazd : public pojazd
  2. {
  3. public:
  4. int dopalacz;
  5. };

Prawda, że krócej! W wyniku takiego zapisu otrzymaliśmy klasę super_pojazd, która jest pochodną od klasy pojazd. Oznacza to, że zawiera ona wszystkie wady i zalety klasy swojego przodka. Składniowo chyba wszystko jest jasne. Tuż na nazwą klasy pochodnej [u nas super_pojazd] stawiamy dwukropek. Teraz trzeba jeszcze określić sposób dziedziczenia. Ustala się to za pomocą etykiet, które już poznałeś. Dziedziczenie prywatne oznacza, że wszystkie składniki klasy podstawowej staną się niedostępne w klasie pochodnej. Niezależnie od sposobu dziedziczenia, prywatne składniki klasy podstawowej zawsze pozostaną niedostępne w klasie pochodnej! Podczas dziedziczenia chronionego, składniki prywatne klasy podstawowej, staną się niedostępne w klasie pochodnej. Zmienią się składniki publiczne i chronione. W klasie pochodnej będą chronione. Dziedziczenie publiczne jest najprostsze i chyba najczęściej stosowane. Praktycznie nie powoduje żadnych zmian. Dostęp do składników odziedziczonych jest nadal taki sam, jak w klasie podstawowej. Jeżeli jest to dla Ciebie nieco zawiłe, to poniżej znajduje się bardziej obrazowa wersja togo, co powiedziałem.

składniki w klasie podstawowej sposób dziedziczenia składniki w klasie pochodnej
prywatne
chronione
publiczne
prywatne niedostępne
niedostępne
niedostępne
prywatne
chronione
publiczne
chronione niedostępne
chronione
chronione
prywatne
chronione
publiczne
publiczne niedostępne
chronione
publiczne

Chciałem jeszcze przypomnieć o możliwości zawierania przyjaźni pomiędzy klasami. Klasa pochodna, będąca przyjacielem klasy podstawowej posiada pełny dostęp do wszystkich jej składników oraz funkcji składowych, niezależnie od sposobu dziedziczenia.

Podczas dziedziczenia zawartość klasy podstawowej staje się automatycznie zawartością klasy pochodnej. Zatem wszystkie składniki i funkcje składowe stają się dostępne w klasie odziedziczonej. Oczywiście ta dostępność jest uwarunkowana sposobem dziedziczenia. W powyższym przykładzie celowo użyłem składników publicznych. Co jednak stałyby się gdybyśmy użyli składników prywatnych? Odpowiedź jest prosta. Wówczas nie mielibyśmy do nich dostępu. Dodajmy teraz kilka funkcji do naszych klas.

  1. class pojazd
  2. {
  3. private:
  4. int predkosc;
  5. int przyspieszenie;
  6. int ilosc_kol;
  7. int kolor;
  8.  
  9. public:
  10. int wez_predkosc ();
  11. int wez_przyspieszenie ();
  12. };
  13.  
  14. int pojazd::wez_predkosc ()
  15. {
  16. return predkosc;
  17. }
  18.  
  19. int pojazd::wez_przyspieszenie ()
  20. {
  21. return przyspieszenie;
  22. }

Do naszej klasy pojazd dodaliśmy dwie funkcje informujące o aktualnej prędkości i przyspieszeniu pojazdu. Rozbudowaliśmy trochę klasę pojazd. Teraz zajmijmy się klasą super_pojazd.

  1. class super_pojazd : public pojazd
  2. {
  3. private:
  4. int dopalacz;
  5.  
  6. public:
  7. int wez_dopalacz ();
  8. void wlacz_dopalacz ();
  9. };
  10.  
  11. int super_pojazd::wez_dopalacz ()
  12. {
  13. return dopalacz;
  14. }
  15.  
  16. void super_pojazd::wlacz_dopalacz ()
  17. {
  18. if (predkosc < 200) return;
  19. else predkosc += dopalacz;
  20. }

Do klasy super_pojazd dodaliśmy dwie funkcje. Pierwsza zwraca stan dopalacza. Druga jest nieco ciekawsza - daje nam kopa włączając dopalacz :D W klasie pojazd znajduje się prywatny składnik predkosc. Jest on dziedziczony przez klasę super_pojazd. Dziedziczenie jest publiczne, zatem dostęp do składników nie zmienia się. W klasie super_pojazd wszystkie składniki są nadal prywatne. Teraz uważaj! Oznacza to, że w klasie nie można się do nich odwołać z klasy pochodnej :-O Wiesz, co to oznacza? Składniki klasy pojazd są teraz zawartością klasy super_pojazd. Problem w tym, że nie ma do nich dostępu! Skoro tak jest to funkcja wlacz_dopalacz nie zostanie skompilowana. Jak więc temu zaradzić? Jak można odnieść się do prywatnych składników klasy podstawowej z wnętrza klasy pochodnej? Jest sposób i już go znasz. Przypomnij sobie etykietę protected. Nadmieniłem o niej jakiś czas temu. Powiedziałem, że działa podobnie, jak etykieta private. Teraz wyjaśnię, jakie są różnice. Etykieta protected została zaprojektowana z myślą o dziedziczeniu. Składniki klasy oznaczone tą etykietą są traktowane w klasie podstawowej jako prywatne. Jednakże w przeciwieństwie do składników prywatnych są dostępne w klasach pochodnych. Zatem, aby funkcja wlacz_dopalacz zadziałała należy zastosować etykietę protected zamiast private w klasie pojazd i po sprawie. Należy tutaj jednak pamiętać, że nie dziedziczy się kilku rzeczy. Po pierwsze nie dziedziczy się konstruktorów. To chyba oczywiste. Konstruktor zawsze posiada nazwę swojej klasy. Klasa pochodna ma inną nazwę, zatem konstruktor też będzie się inaczej nazywał. Drugim elementem, którego się nie dziedziczy jest destruktor. Tutaj również jest sprawa prosta. Destruktor nazywa się tak jak klasa, do której należy. Jego nazwa jest dodatkowo poprzedzona wężykiem. Kolejna rzecz to operator przypisania. Jak sądzę teraz jeszcze nie dostrzegasz korzyści płynących z dziedziczenia. Samo dziedziczenie to jeszcze nic. Dopiero w połączeniu z funkcjami wirtualnymi staje się przydatne. Właśnie o tym teraz pogadamy.

funkcje wirtualne

Zacznę trochę nietypowo. Przypuśćmy, że piszesz program. Niech to będzie program graficzny do rysowania brył przestrzennych. Wszystkie bryły będą powiązane ze sobą związkami dziedziczenia. W zależności od rodzaju bryły sposób jej narysowania będzie inny. Zatem w programie umieszczasz liczne instrukcje warunkowe, jak if, czy switch. Po pewnym czasie zamierzasz dodać kilka brył. Należy zdefiniować nową klasę i.. no właśnie dokonać modyfikacji kodu. Niby nie problem. Wystarczy odszukać wszystkie wystąpienia instrukcji warunkowych dotyczących poszczególnych brył i dodać kilka linijek kodu. Wydaje się proste. Jednakże w rzeczywistości jest inaczej. Tego typu modyfikacje są bardzo czasochłonne, a ich wykonanie naprawdę bardzo męczące. Ponadto trzeba ingerować w kod źródłowy, co nie zawsze jest osiągalne. Jak może zauważyłeś, zawsze kiedy tak narzekam chcę przedstawić jakieś rewelacyjne rozwiązanie. Tym razem nie będzie inaczej. Polimorfizm to narzędzie, które na pewno polubisz. Funkcje wirtualne, bo o nich właśnie mowa to jakby 'inteligentne' funkcje, które potrafią się dostosować do samego programu :-/ Stworzenie takiej funkcje jest naprawdę banalne i skupia się na dodaniu tylko jednego słówka przed deklaracją funkcji! Wystarczy poprzedzić deklarację słówkiem virtual i program stanie się 'inteligentny' :)

  1. class bryla
  2. {
  3. protected:
  4. int ilosc_wierzcholkow;
  5. int kolor;
  6.  
  7. public:
  8. void rysuj ();
  9. };
  10.  
  11. void bryla::rysuj ()
  12. {
  13. //instrukcje rysujące bryłe
  14. }

Zdefiniowaliśmy sobie klasę bryla, która służy do przechowywania informacji o bryle przestrzennej. Sama klasa nie reprezentuje żadnego konkretnego obiektu. Jest klasą abstrakcyjną i służy jedynie do dziedziczenia.

  1. class szescian : public bryla
  2. {
  3. private:
  4. int krawedz;
  5.  
  6. public:
  7. void rysuj ();
  8. };
  9.  
  10. void szescian::rysuj ()
  11. {
  12. //instrukcje rysujące szescian
  13. }
  14.  
  15. class walec : public bryla
  16. {
  17. private:
  18. int promien;
  19. int wysokosc;
  20.  
  21. public:
  22. void rysuj ();
  23. };
  24.  
  25. void walec::rysuj ()
  26. {
  27. //instrukcje rysujące walec
  28. }
  29.  
  30. class stozek : public bryla
  31. {
  32. private:
  33. int promien;
  34. int wysokosc;
  35.  
  36. public:
  37. void rysuj ();
  38. };
  39.  
  40. void stozek::rysuj ()
  41. {
  42. //instrukcje rysujące stożek
  43. }

Teraz dodaliśmy kilka klas. Każda z nich jest pochodną od klasy bryla. Każda z nich posiada składnik kolor oraz ilosc_wierzcholkow. Dodatkowo niektóre z brył posiadają jeszcze inne składniki. Oczywiście to ma tylko charakter demonstracyjny. Skorzystajmy teraz z naszych klas.

  1. bryla bryla_1;
  2. szescian bryla_2;
  3. walec bryla_3;
  4. stozek bryla_4;

Mając już zdefiniowane obiekty możemy się do nich odwoływać za pomocą funkcji składowych w taki sposób:

  1. bryla_1.rysuj ();
  2. bryla_2.rysuj ();
  3. bryla_3.rysuj ();
  4. bryla_4.rysuj ();

Wówczas wywołaliśmy na rzecz każdego z obiektów funkcję składową rysuj. To nie jest żadna nowość. O tym już wiemy od dawna. Teraz definiujemy sobie kilka wskaźników mogących pokazywać na poszczególne bryły:

  1. bryla *wsk_bryla;
  2. szescian *wsk_szescian;
  3. walec *wsk_walec;
  4. stozek *wsk_stozek;

Teraz ustawiamy odpowiednio wskaźniki. Pamiętasz jeszcze jak się to robi?

  1. wsk_bryla = &bryla_1;
  2. wsk_szescian = &bryla_2;
  3. wsk_walec = &bryla_3;
  4. wsk_stozek = &bryla_4;

Za pomocą tak ustawionych wskaźników także możemy wywoływać funkcje składowe. Zobacz, to zrealizować:

  1. wsk_bryla->rysuj ();
  2. wsk_szescian->rysuj ();
  3. wsk_walec->rysuj ();
  4. wsk_stozek->rysuj ();

Prawda, że fajnie? Jednak co nam po tym? Praktycznie to tylko niewielkie przyspieszenie, bo jak pamiętasz operacje na wskaźnikach są wykonywane szybciej. W przykładzie zdefiniowaliśmy sobie cztery różne wskaźniki mogące pokazywać na obiekty poszczególnych klas. Teraz będzie najważniejsze! Okazuje się, że wcale nie potrzebujemy aż czterech wskaźników! Wystarczy tylko jeden, który będzie mógł pokazywać na wszystkie cztery obiekty. Pamiętasz jeszcze operator rzutowania? Przy jego użyciu mogliśmy zmienić rodzaj wskaźnika. Na nasze szczęście wskaźniki zostały tak zaprojektowane, aby usprawnić tego typu operacje. Zapamiętaj sobie: wskaźnik do klasy podstawowej może zostać niejawnie skonwertowany na wskaźnik do klasy pochodnej. Czy zdajesz sobie z tego sprawę? Mamy tylko jeden wskaźnik do klasy bryla. Może on pokazywać na obiekty klasy bryła, jak również na obiekty klas pochodnych. Konkretnie można go ustawić na obiekcie klasy szescian, walec, czy stozek! Nic nie stoi na przeszkodzie, aby to zrobić. Tutaj dotarliśmy do sedna sprawy. To jest właśnie polimorfizm, czyli ta 'inteligencja'. Popatrz teraz jak przedstawia się on od strony składniowej. Żeby było przejrzyściej napiszmy raz jeszcze definicje powyższych klas. Jednak tym razem zróbmy to 'inteligentnie'. Popatrz uważnie:

  1. class bryla
  2. {
  3. protected:
  4. int ilosc_wierzcholkow;
  5. int kolor;
  6.  
  7. public:
  8. virtual void rysuj ();
  9. };
  10.  
  11. void bryla::rysuj ()
  12. {
  13. //instrukcje rysujące bryłe
  14. }
  15.  
  16. class szescian : public bryla
  17. {
  18. private:
  19. int krawedz;
  20.  
  21. public:
  22. void rysuj ();
  23. };
  24.  
  25. void szescian::rysuj ()
  26. {
  27. //instrukcje rysujące szescian
  28. }
  29.  
  30. class walec : public bryla
  31. {
  32. private:
  33. int promien;
  34. int wysokosc;
  35.  
  36. public:
  37. void rysuj ();
  38. };
  39.  
  40. void walec::rysuj ()
  41. {
  42. //instrukcje rysujące walec
  43. }
  44.  
  45. class stozek : public bryla
  46. {
  47. private:
  48. int promien;
  49. int wysokosc;
  50.  
  51. public:
  52. void rysuj ();
  53. };
  54.  
  55. void stozek::rysuj ()
  56. {
  57. //instrukcje rysujące stożek
  58. }

Przy deklaracji funkcji rysuj w klasie bryla pojawiło się słówko virtual. Informuje ono kompilator, iż dana funkcja jest funkcją wirtualną. Oznacza to, że podczas wywoływania funkcji za pomocą wskaźnika wystartuje funkcja obiektu aktualnie pokazywanego przez wskaźnik. Popatrz na przykład, a zrozumiesz:

  1. bryla *wsk_bryla;
  2. szescian bryla_1;
  3. walec bryla_2;
  4. stozek bryla_3;
  5.  
  6. wsk_bryla = &bryla_1;
  7. wsk_bryla->rysuj ();
  8. wsk_bryla = &bryla_2;
  9. wsk_bryla->rysuj ();
  10. wsk_bryla = &bryla_3;
  11. wsk_bryla->rysuj ();

Najpierw definiujemy sobie wskaźnik do obiektów klasy bryla. Później są definicje kilku obiektów klas pochodnych od niej. Sam wskaźnik służy do pokazywania na obiekty klasy bryla. W wyniku ustawienia go na obiekt klasy szescian, która jest klasą pochodną nastąpi niejawna konwersja wskaźnika. Korzystając teraz z tak ustawionego wskaźnika możemy wywołać dowolną nie-prywatną funkcję składową. Wywołujemy funkcję rysuj, która jest funkcją wirtualną. Raz jeszcze przypomnijmy. Wskaźnik wsk_bryla służy do pokazywania na obiekty klacy bryla. Zatem w wyniku wywołania funkcji za jego pośrednictwem do pracy powinna ruszyć funkcja rysuj z klasy bryla. A jednak tak się nie stanie! Wystartuje funkcja rysuj z klasy szescian. Wszystko to za sprawą tego jednego słówka virtual postawionego prze deklaracji funkcji rysuj w klasie podstawowej. Na koniec mała zagadka. Napisałeś program z użyciem klasy bryla. Sama klasa jest w wersji skompilowanej, zatem nie masz do niej dostępu. Chcesz rozbudować swój program. Tworzysz kilka klas pochodnych od klasy bryla. Oczywiście wszystkie one posiadają funkcję rysuj. W programie występuje taki fragment:

  1. bryla *wsk_bryla;
  2. nowa_bryla bryla_1;
  3.  
  4. wsk_bryla = &bryla_1;
  5. wsk_bryla->rysuj ();

Na początku jest definicja wskaźnika. Następnie definicja obiektu klasy nowa_bryla będącą klasą pochodną od klasy bryla. Sama klasa bryla jest już skompilowana i nic nie wie o klasie nowa_bryla. Czy zatem wywołanie funkcje rysuj z klasy nowa_bryla zostanie uznane za błąd? Okazuje się, że nie! Dzięki temu, że funkcja rysuj w klasie podstawowej jest funkcją wirtualną program staje się bardzo elastyczny na takie modyfikacje. To jest podstawa dobrego programu orientowanego obiektowo. Dzięki temu program jest bardziej 'inteligentny' i potrafi odpowiednio zareagować na późniejsze zmiany. To jest naprawdę świetne! Być może jeszcze tego nie dostrzegasz, ale wkrótce na pewno docenisz wspaniałość dziedziczenia i polimorfizmu. Wierz mi, umiejętność zaprojektowania takiego programu bardzo się przydaje i pozwala zaoszczędzić mnóstwo pracy i czasu.