BAZA WIEDZY
KURSY
Bazy danych w PHP
Kurs AdvancedAJAX
Kurs ASP
Kurs ASP.NET
Kurs C++
Kurs CSS
Kurs HTML
Kurs HTML drugi
Kurs JavaScript
Kurs MySQL
Kurs PHP
Kurs RSS
Kurs XHTML
Obiekty DOM
MANUALE
CSS1 - W3C
DOM - w budowie
PHP 2005
PHP 2006
Wyrażenia regularne
SHOUTBOX
STAT
Online: 3 | UU: 208

Metody wirtualne i polimorfizm

Dziedziczenie jest oczywiście niezwykle ważnym, a wręcz niezbędnym skadnikiem programowania obiektowego. Stanowi jednak tylko podstawę dla dwóch kolejnych technik, mających dużo większe znaczenie i pozwalających na o wiele efektywniejsze pisanie kodu. Mam tu na myśli tytułowe metody wirtualne oraz częściowo bazujący na nich polimorfizm. Wszystkie te dziwne terminy zostaną wkrótce wyjaśnione, zatem nie wpadajmy zbyt pochopnie w panikę ;)

Wirtualne funkcje składowe

Idea dziedziczenia w znanej nam dotąd postaci jest nastawiona przede wszystkim na uzupełnianie definicji klas bazowych o kolejne składowe w klasach pochodnych. Tylko czasami zastępowaliśmy już istniejące metody ich nowymi wersjami , właściwymi dla tworzonych klas.

Takie sytuacje są jednak w praktyce dosyć częste - albo raczej korzystne jest prowokowanie takich sytuacji, gdyż niejednokrotnie dają one świetne rezultaty i niespotykane wcześniej możliwości przy niewielkim nakładzie pracy. Oczywiście dzieje się tak tylko wtedy, gdy mamy odpowiednie podejście do sprawy.

To samo, ale inaczej

Raz jeszcze zajmijmy się naszą hierarchią klas zwierząt. Tym razem skierujemy uwagę na metodę Oddychaj z klasy Zwierzę .

Jej obecność u szczytu diagramu, w klasie, z której początek biorą wszystkie inne, jest z pewnością uzasadniona. Każde zwierzę, niezależnie od gatunku, musi przecież pobierać z otoczenia tlen niezbędny do życia, a proces ten nazywamy potocznie właśnie oddychaniem. Jest to bezdyskusyjne.

Mniej oczywisty jest natomiast fakt, że "techniczny" przebieg tej czynności może się zasadniczo różnić u poszczególnych zwierząt. Te żyjące na lądzie używają do tego narządów zwanych płucami, zaś zwierzęta wodne - chociażby ryby - mają w tym celu wykształcone skrzela, funkcjonujące na zupełnie innej zasadzie.

Spostrzeżenia te nietrudno przełożyć na bliższy nam sposób myślenia, związany bezpośrednio z programowaniem. Oto więc klasy wywodzące się do Zwierzęcia powinny w inny sposób implementować metodę Oddychaj ; jej treść musi być odmienna przynajmniej dla Ryb y, a i Ssak oraz Gad mają przecież własne patenty na proces oddychania.

Rzeczona metoda podpada zatem pod redefinicję w każdej z klas dziedziczących od klasy Zwierzę :


Schemat 27. Przedefiniowanie metody z klasy bazowej w klasach pochodnych

Deklaracja metody wirtualnej

Teoretycznie klasa Zwierzę mogłaby być całkowicie "nieświadoma" tego, że jedna z jej metod jest definiowana w inny sposób w klasie pochodnej. Lepiej jednak, abyśmy przewidzieli taką konieczność i poczynili odpowiedni krok. Jest nim uczynienie funkcji Oddychaj metodą wirtualną w klasie Zwierzę .

Metoda wirtualna jest przygotowana na zastąpienie siebie przez nową wersję, zdefiniowaną w klasie pochodnej.

Aby daną funkcję składową zadeklarować jako wirtualną, należy poprzedzić jej prototyp słowem kluczowym virtual :

#include <iostream>
class CAnimal
{
// (pomijamy pozostałe składowe klasy)
public :
virtual void Oddychaj()
{ std::cout << "Oddycham..." << std::endl; }
};

W ten sposób przygotowujemy ją na ewentualne ustąpienie miejsca bardziej wyspecjalizowanym wersjom, podanym w klasach pochodnych. Skorzystanie z mechanizmu metod wirtualnych jest tutaj lepszym rozwiązaniem niż zignorowanie go, gdyż uaktywnia to możliwości polimorfizmu związane z obiektami. Zapoznamy się z nimi w dalszej części tekstu.

Przedefiniowanie metody wirtualnej

Celem wprowadzenia funkcji wirtualnej Oddychaj() do klasy CAnimal było, jak to zaznaczyliśmy na początku, jej późniejsze przedefiniowanie (ang. override ) w klasach pochodnych. Operacji tej dokonujemy prostą drogą, bowiem zwyczajnie definiujemy nową wersję metody w owych klasach:

class CFish : public CAnimal
{
public :
void Oddychaj() // redefinicja metody wirtualnej
{ std::cout << "Oddycham skrzelami..." << std::endl; }
void Plyn();
};
class CMammal : public CAnimal
{
public :
void Oddychaj() // jak wyżej
{ std::cout << "Oddycham płucami..." << std::endl; }
void Biegnij();
};
class CBird : public CAnimal
{
public :
void Oddychaj() // i znowu jak wyżej :)
{ std::cout << "Oddycham płucami..." << std::endl; }
void Lec();
};

Kompilator sam "domyśla się", że nasza metody jest tak naprawdę redefinicją metody wirtualnej z klasy bazowej. Możemy jednak wyraźnie to zaznaczyć poprzez ponowne zastosowanie słowa virtual .

Według mnie jest to mało szczęśliwe rozwiązanie składniowe, ponieważ może często powodować pomyłki. Nie sposób bowiem odróżnić deklaracji przedefiniowanej metody wirtualnej od jej pierwotnej wersji (jeżeli jeszcze raz użyliśmy virtual ) lub od zwykłej funkcji składowej (gdy nie skorzystaliśmy ze wspomnianego słówka).
Bardziej przejrzyście rozwiązano to na przykład w Delphi, gdzie nową wersję metody wirtualnej trzeba opatrzyć frazą override ; .

Nowa wersja metody całkowicie zastępuje starą, która jest jednak dostępna i w razie potrzeby możemy ją wywołać. Służy do tego konstrukcja:

nazwa_klasy_bazowej :: nazwa_metody ( [ parametry ] );

W powyższym przypadku byłoby to wywołanie CAnimal::Oddychaj() .

W Visual C++ zamiast nazwy_klasy_bazowej możliwe jest użycie specjalnego słowa kluczowego __super , opisanego tutaj .

Pojedynek: metody wirtualne przeciwko zwykłym

Czytając powyższe objaśnienie metod wirtualnych, zadawałeś sobie zapewne proste pytanie o głębokiej treści, a mianowicie: "Po co mi to?" ;-) Najlepszą odpowiedzią na nie będzie wyjaśnienie różnicy pomiędzy zwykłymi oraz wirtualnymi metodami.

Posłuży nam do tego następujący kod, tworzący obiekt jednej z klasy pochodnych i wywołujący jego metodę Oddychaj() :

CAnimal* pZwierzak = new CMammal;
pZwierzak->Oddychaj();
delete pZwierzak;

Zauważmy, że wskaźnik pZwierzak , poprzez który odwołujemy się do naszego obiektu, jest zasadniczo wskaźnikiem na klasę CAnimal . Stwarzany przez nas (poprzez instrukcję new ) obiekt należy natomiast do klasy CMammal . Wszystko jest jednak w porządku. Klasa CMammal dziedziczy od klasy CAnimal , zatem każdy obiekt należący do tej pierwszej jednocześnie jest także obiektem tej drugiej. Wyjaśniliśmy to sobie całkiem niedawno, prezentując dziedziczenie.

Zajmijmy się raczej drugą linijką powyższego kodu, zawierającą wywołanie interesującej nas metody Oddychaj() . Różnica między zwykłymi a wirtualnymi funkcjami składowymi będzie miała okazję uwidocznić się właśnie tutaj. Wszystko bowiem zależy od tego, jaką metodą jest rzeczona funkcja Oddychaj() , zaś rezultatem rozważanej instrukcji może być zarówno wywołanie CAnimal::Oddychaj() , jak i CMammal::Oddychaj() ! Dowiedzmy się więc, kiedy zajdzie każda z tych sytuacji.

Łatwiejszym przypadkiem jest chyba "niewirtualność" rozpatrywanej metody. Kiedy jest ona zwyczajną funkcją składową, wtedy kompilator nie traktuje jej w żaden specjalny sposób. Co to jednak w praktyce oznacza?.

To dosyć proste. W takich bowiem wypadkach decyzja, która metoda jest rzeczywiście wywoływana, zostaje podjęta już na etapie kompilacji programu. Nazywamy ją wtedy wczesnym wiązaniem (ang. early binding ) funkcji. Do jej podjęcia są zatem wykorzystane jedynie te informacje, które są znane w momencie kompilacji programu; u nas jest to typ wskaźnika pZwierzak , czyli CAnimal . Nie jest przecież możliwe ustalenie, na jaki obiekt będzie on faktycznie wskazywał - owszem, może on należeć do klasy CAnimal , jednak równie dobrze do jej pochodnej, na przykład CMammal . Wiedza ta nie jest jednak dostępna podczas kompilacji 1 , dlatego też tutaj zostaje asekuracyjnie wykorzystany jedynie znany typ CAnimal . Faktycznie wywoływaną metodą będzie więc CAnimal::Oddychaj() !

Huh, to raczej nie jest to, o co nam chodziło. Skoro już tworzymy obiekt klasy CMammal , to w zasadzie logiczne jest, że zależy nam na wywołaniu funkcji pochodzącej z tej właśnie klasy, a nie z jej bazy! Spotyka nas jednak przykra niespodzienka.

Czy uchroni od niej zastosowanie metod wirtualnych? Domyślasz się zapewne, iż tak właśnie będzie, i na dodatek masz tutaj absolutną rację :) Kiedy użyjemy magicznego słówka virtual , kompilator wstrzyma się z decyzją co do faktycznie przywoływanej metody. Jej podjęcie nastąpi dopiero w stosowanej chwili podczas działania gotowej aplikacji; nazywamy to późnym wiązaniem (ang. late binding ) funkcji. W tym momencie będzie oczywiście wiadome, jaki obiekt naprawdę kryje się za naszym wskaźnikiem pZwierzak i to jego wersja metody zostanie wywołana. Uzyskamy zatem skutek, o jaki nam chodziło, czyli wywołanie funkcji CMammal::Oddychaj() .

Prezentowany tu problem wyraźnie podpada już pod idee polimorfizmu, które wyczerpująco poznamy niebawem.

Wirtualny destruktor

Atrybut virtual możemy przyłączyć do każdej zwyczajnej metody, a nawet takiej niezupełnie zwyczajnej :) Czasami zresztą zastosowanie go jest niemal powinnością.

Jeżeli chodzi o konstruktory, to stosowanie tego modyfikatora w stosunku do nich nie ma zbyt wielkiego sensu. Są one przecież domyślnie "jakby wirtualne": wywołanie konstruktora z klasy pochodnej powoduje przecież uruchomienie także konstruktorów z klas bazowych. Ich przedefiniowanie nie jest przy tym niczym nadzwyczajnym, tak więc użycie słowa virtual w tym przypadku mija się z celem.

Zupełnie inaczej sprawa ma się z destruktorami. Tutaj użycie omawianego modyfikatora jest nie tylko możliwe, ale też prawie zawsze konieczne i zalecane . Nieobecność wirtualnego destruktora w klasie bazowej może bowiem prowadzić do tzw. wycieków pamięci, czyli bezpowrotnej utraty zaalokowanej pamięci operacyjnej.

Dlaczego tak się dzieje? Do wyjaśnienia posłużymy się po raz kolejny naszymi wysłużonymi klasami zwierząt :D Przypuśćmy, że czujemy potrzebę, aby dokładniej odpowiadały one rzeczywistości; by nie były tylko zbiorami danych, ale też zawierały obiektowe odpowiedniki narządów wewnętrznych, na przykład serca czy płuc. Poczynimy więc najpierw pewne zmiany w bazowej klasie CAnimal :

// klasa serca
class CHeart { /* ... */ };
// bazowa klasa zwierząt
class CAnimal
{
// (pomijamy nieistotne, pozostałe składowe)
protected :
CHeart* m_pSerce;
public :
// konstruktor i destruktor
CAnimal() { m_pSerce = new CHeart; }
~CAnimal() { delete m_pSerce; }
};

Serce jest oczywiście organem, który posiada każde zwierzę, zatem obecność wskaźnika na obiekt klasy CHeart jest tu uzasadniona. Odwołuje się on do obiektu tworzonego w konstruktorze, a niszczonego w destruktorze klasy CAnimal .

Naturalnie, nie samym sercem zwierzę żyje :) Ssaki na przykład potrzebują jeszcze płuc:

// klasa płuc
class CLungs { /* ... */ };
// klasa ssaków
class CMammal : public CAnimal
{
protected :
CLungs* m_pPluca;
public :
// konstruktor i destruktor
CMammal() { m_pPluca = new CLungs; }
~CMammal() { delete m_pPluca; }
};

Podobnie jak wcześniej, obiekt specjalnej klasy jest tworzony w konstruktorze i zwalniany w destruktorze CMammal . W ten sposób nasze ssaki są zaopatrzone zarówno w serce (otrzymane od CAnimal ), jak i niezbędne płuca, tak więc pożyją sobie jeszcze trochę i będą mogły nadal służyć nam jako przykład ;)

OK, gdzie zatem tkwi problem?. Powróćmy teraz do trzech linijek kodu, za pomocą których rozstrzygnęliśmy pojedynek między wirtualnymi a niewirtualnymi metodami:

CAnimal* pZwierzak = new CMammal;
pZwierzak->Oddychaj();
delete pZwierzak;

Przypomnijmy, że pZwierzak jest tu zasadniczo zmienną typu "wskaźnik na obiekt klasy CAnimal ", ale tak naprawdę wskazuje na obiekt należący do pochodnej CMammal . Ów obiekt musi oczywiście zostać usunięty, za co powinna odpowiadać ostatnia linijka.

No właśnie - powinna. Szkoda tylko, że tego nie robi. To zresztą nie jest jej wina, przyczyną jest właśnie brak wirtualnego destruktora.

Jak bowiem wiemy, zniszczenie obiektu oznacza w pierwszej kolejności wywołanie tej kluczowej metody. Podlega ono identycznym regułom, jakie stosują się do wszystkich innych metod, a więc także efektom związanym z wirtualnością oraz wczesnym i późnym wiązaniem. Jeżeli więc nasz destruktor nie będzie oznaczony jako virtual , to kompilator potraktuje go jako zwyczajną metodę i zastosuje wobec niej technikę wczesnego wiązania. Zasugeruje się po prostu typem zmiennej pZwierzak (którym jest CAnimal* , a więc wskaźnik na obiekt klasy CAnimal ) i wywoła wyłącznie destruktor klasy bazowej CAnimal ! Destruktor ten wprawdzie usunie serce naszego ssaka, ale nie zrobi tego z płucami, bo i nie ma przecież o nich zielonego pojęcia.

Nie dość zatem, że tracimy przez to pamięć przeznaczoną na tenże narząd, to jeszcze pozwalamy, by wokół fruwały nam organy pozbawione właścicieli ;D

To oczywiście tylko obrazowy dowcip, jednak konsekwencje niepełnego zniszczenia obiektów mogą być dużo poważniejsze, szczególnie jeśli ich składniki odwoływały się do siebie nawzajem. Weźmy choćby wspomniane płuca - powinny one przecież dostarczać tlen do serca, a jeżeli samo serce już nie istnieje, no to zaczynają się nieliche problemy.

Rozwiązanie problemu jest rzecz jasna nadzwyczaj proste - wystarczy uczynić destruktor klasy bazowej CAnimal metodą wirtualną:

class CAnimal
{
// (oszczędność jest cnotą, więc znowu pomijamy resztę składowych :D)
public :
virtual ~CAnimal() { delete m_pSerce; }
};

Wtedy też operator delete będzie usuwał obiekt, na który faktycznie wskazuje podany mu wskaźnik. My zaś uchronimy się od perfidnych błędów.

Pamiętaj zatem, aby zawsze umieszczać wirtualny destruktor w klasie bazowej .

1 Tak naprawdę kompilator może w ogóle nie wiedzieć, że CAnimal posiada jakieś klasy pochodne!

Zaczynamy od zera. dosłownie

Deklarując metody opatrzone modyfikatorem virtual , tworzymy grunt pod ich przyszłą, ponowną implementację w klasach dziedziczących. Można też powiedzieć, iż w pewnym sensie zmieniamy charakter zawierającej je klasy: jej rolą nie jest już przede wszystkim tworzenie obiektów, gdyż równie ważne staje się służenie jako baza dla klas pochodnych.

Niekiedy słuszne jest pójście jeszcze dalej, to znaczy całkowite pozbawienie klasy możliwości tworzenia z niej obiektów. Ma to nierzadko rozsądne uzasadnienie i takimi właśnie przypadkami zajmiemy się w tym paragrafie.

Czysto wirtualne metody

Wirtualna funkcja składowa umieszczona w klasie bazowej jest przygotowana na to, aby ustąpić miejsca swej bardziej wyspecjalizowanej wersji, zdefiniowanej w klasie pochodnej. Nie zmienia to jednak faktu, iż musiałaby ona jakoś implementować czynność, której przebiegu często nie sposób ustalić na tym etapie.

Posiadamy dobry przykład, ilustrujący taką właśnie sytuację. Chodzi mianowicie o metodę CAnimal::Oddychaj() . Wewnątrz klasy bazowej, z której mają dopiero dziedziczyć konkretne grupy zwierząt, niemożliwe jest przecież ustalenie uniwersalnego sposobu oddychania. Sensowna implementacja tej metody jest więc w zasadzie niemożliwa.

Sprawia to, iż jest ona wyświenitym kandydatem na czysto wirtualną funkcję składową .

Metody nazywane czysto wirtualnymi (ang. pure virtual ) nie posiadają żadnej implementacji i są przeznaczone wyłącznie do przedefiniowania w klasach pochodnych.

Deklaracja takiej metody ma dość osobliwą postać. Oczywiście z racji nie posiadania żadnego kodu zbędne stają się nawiasy klamrowe wyznaczające jej blok, zatem całość przypomina zwykły prototyp funkcji. Samo oznaczenie, czyniące daną metodę czysto wirtualną, jest jednak raczej niecodzienne:

class CAnimal
{
// (definicja klasy jest skromna z przyczyn oszczędnościowych :))
public :
virtual void Oddychaj() = 0 ;
};

Jest nim występująca na końcu fraza = 0 ; . Kojarzy się ona trochę z domyślną wartością funkcji, ale interpretacja taka upada w obliczu niezwracania przez metodę Oddychaj() żadnego rezultatu. Faktycznie funkcją czysto wirtualną możemy w ten sposób uczynić każdą wirtualną metodę, niezależnie od tego, czy zwraca jakąś wartość i jakiego jest ona typu. Sekwencja = 0 ; jest więc po prostu takim dziwnym oznaczeniem, stosowanym dla tego rodzaju metod. Trzeba się z nim zwyczajnie pogodzić :)

Twórcy C++ wyraźnie nie chcieli wprowadzać tutaj dodatkowego słowa kluczowego, ale w tym przypadku trudno się z nimi zgodzić. Osobiście uważam, że deklaracja w formie na przykład pure virtual void Oddychaj(); byłaby znacznie bardziej przejrzysta.

Po dokonaniu powyższej operacji metoda CAnimal::Oddychaj() staje się zatem czysto wirtualną funkcją składową. W tej postaci określa już tylko samą czynność, bez podawania żadnego algorytmu jej wykonania. Zostanie on ustalony dopiero w klasach dziedziczących od CAnimal .

Można aczkolwiek podać implementację metody czysto wirtualnej, jednak będzie ona mogła być wykorzystywana tylko w kodzie metod klas pochodnych, które ją przedefiniowują, w formie klasa_bazowa :: nazwa_metody ( [ parametry ] ) .

Abstrakcyjne klasy bazowe

Nie widać tego na pierwszy, drugi, ani nawet na dziesiąty rzut oka, ale zadeklarowanie jakiejś metody jako czysto wirtualnej powoduje jeszcze jeden, dodatkowy efekt. Otóż klasa, w której taką funkcję stworzymy, staje się klasą abstrakcyjną .

Klasa abstrakcyjna zawiera przynajmniej jedną czysto wirtualną metodę i z jej powodu nie jest przeznaczona do instancjowania (tworzenia z niej obiektów), a jedynie do wyprowadzania zeń klas pochodnych .

 

Ze względu na wyżej wymienioną definicję czysto wirtualne funkcje składowe określa się niekiedy mianem metod abstrakcyjnych. Nazwa ta jest szczególnie popularna wśród programistów języka Object Pascal.

Takie klasy budują zawsze najwyższe piętra w hierarchiach i są podstawami dla bardziej wyspecjalizowanych typów. W naszym przypadku mamy tylko jedną taką klasę, z której dziedziczą wszystkie inne. Nazywa się CAnimal , jednak dobry zwyczaj programistyczny nakazuje, aby klasy abstrakcyjne miały nazwy zaczynające się od litery I . Różnią się one bowiem znacznie od pozostałych klas. Zatem baza w naszej hierarchii będzie od tej pory zwać się IAnimal .

C++ bardzo dosłownie traktuje regułę, iż klasy abstrakcyjne nie są przeznaczone do instancjowania. Próba utworzenia z nich obiektu zakończy się bowiem błędem; kompilator nie pozwoli na obecność czysto wirtualnej metody w klasie tworzonego obiektu.

Możliwe jest natomiast zadeklarowanie wskaźnika na obiekt takiej klasy i przypisanie mu obiektu klasy potomnej, tak więc poniższy kod będzie jak najbardziej poprawny:

IAnimal* pZwierze = new CBird;
pZwierze->Oddychaj();
delete pZwierze;

Wywołanie metody Oddychaj() jest tu także dozwolone. Wprawdzie w bazowej klasie IAnimal jest ona czysto wirtualna, jednak w CBird , do obiektu której odwołuje się nasz wskaźnik, posiada ona odpowiednią implementację.

Wydawałoby się, że C++ reaguje nieco zbyt alergicznie na próbę utworzenia obiektu klasy abstrakcyjnej - w końcu sama kreacja nie jest niczym niepoprawnym. W ten sposób jednak mamy pewność, że podczas działania programu wszystko będzie działać poprawnie i że omyłkowo nie zostanie wywołana metoda z nieokreśloną implementacją.

Polimorfizm

Gdyby programowanie obiektowe porównać do wysokiego budynku, to u jego fundamentów leżałyby pojęcia "klasy" i "obiekty", środkowe piętra budowałoby "dziedziczenie" oraz "metody wirtualne", zaś u samego szczytu sytuowałby się "polimorfizm". Jest to bowiem największe osiągnięcie tej metody programowania.

Z terminem tym spotykaliśmy się przelotnie już parę razy, ale teraz wreszcie wyjaśnimy sobie wszystko od początku do końca. Zacznijmy choćby od samego słowa: 'polimorfizm' pochodzi od greckiego wyrazu polýmorphos , oznaczającego 'wielokształtny' lub 'wielopostaciowy'. W programowaniu będzie się więc odnosić do takich tworów, które można interpretować na różne sposoby - a więc należących jednocześnie do kilku różnych typów (klas).

Polimorfizm w programowaniu obiektowym oznacza wykorzystanie tego samego kodu do operowania na obiektach przynależnych różnym klasom, dziedziczącym od siebie.

Zjawisko to jest zatem ściśle związane z klasami i dziedziczeniem, aczkolwiek w C++ nie dotyczy ono każdej klasy, a jedynie określonych typów polimorficznych .

Typ polimorficzny to w C++ klasa zawierająca przynajmniej jedną metodę wirtualną .

W praktyce większość klas, do których chcielibyśmy stosować techniki polimorfizmu, spełnia ten warunek. W szczególności tą wymaganą metodą wirtualną może być chociażby destruktor.

Wszystko to brzmi bardzo ładnie, ale trudno nie zadać sobie pytania o praktyczne korzyści związane z wykorzystaniem polimorfizmu. Dlatego też moim celem będzie teraz drobiazgowa odpowiedź na to pytanie - innymi słowy, wreszcie doczekałeś się konkretów ;D

Ogólny kod do szczególnych zastosowań

Zjawisko polimorfizmu pozwala na znaczne uproszczenie większości algorytmów, w których dużą rolę odgrywa zarządzanie wieloma różnymi obiektami. Nie chodzi tu wcale o jakieś skomplikowane operacje sortowania, wyszukiwania, kompresji itp., tylko o często spotykane operacje wykonywania tej samej czynności dla wielu obiektów różnych rodzajów .

Opis ten jest w założeniu dość ogólny, bowiem sposób, w jaki używa się obiektowych technik polimorfizmu jest ściśle związany z konkretnymi programami. Postaram się jednak przytoczyć w miarę klarowne przykłady takich rozwiązań, abyś miał chociaż ogólne pojęcie o tej metodzie programowania i mógł ją stosować we własnych aplikacjach.

Sprowadzanie do bazy

Prosty przypadek wykorzystania polimorfizmu opiera się na elementarnej i rozsądnej zasadzie, którą nie raz już sprawdziliśmy w praktyce. Mianowicie:

Wskaźnik na obiekt klasy bazowej może wskazywać także na obiekt którejkolwiek z jego klas pochodnych.

Bezpośrednie przełożenie tej reguły na konkretne zastosowanie programistyczne jest dość proste. Przypuśćmy więc, że mamy taką oto hierarchię klas:

#include <string>
#include <ctime>
// klasa dowolnego dokumentu
class CDocument
{
protected :
// podstawowe dane dokumentu
std::string m_strAutor; // autor dokumentu
std::string m_strTytul; // tytuł dokumentu
tm m_Data; // data stworzenia
public :
// konstruktory
CDocument()
{ m_strAutor = m_strTytul = "???" ;
time_t Czas = time(NULL); m_Data = *localtime(&Czas); }
CDocument(std::string strTytul)
{ CDocument(); m_strTytul = strTytul; }
CDocument(std::string strAutor, std::string strTytul)
{ CDocument();
m_strAutor = strAutor;
m_strTytul = strTytul; }
//
// metody dostępowe do pól

std::string Autor() const { return m_strAutor; }
std::string Tytul() const { return m_strTytul; }
tm Data() const { return m_Data; }
};
//
// dokument internetowy

class COnlineDocument : public CDocument
{
protected :
std::string m_strURL; // adres internetowy dokumentu
public :
// konstruktory
COnlineDocument(std::string strAutor, std::string strTytul)
{ m_strAutor = strAutor; m_strTytul = strTytul; }
COnlineDocument (std::string strAutor,
std::string strTytul,
std::string strURL)
{ m_strAutor = strAutor;
m_strTytul = strTytul;
m_strURL = strURL; }
//
// metody dostępowe do pól

std::string URL() const { return m_strURL; }
};
// książka
class CBook : public CDocument
{
protected :
std::string m_strISBN; // numer ISBN książki
public :
// konstruktory
CBook(std::string strAutor, std::string strTytul)
{ m_strAutor = strAutor; m_strTytul = strTytul; }
CBook (std::string strAutor,
std::string strTytul,
std::string strISBN)
{ m_strAutor = strAutor;
m_strTytul = strTytul;
m_strISBN = strISBN; }
//
// metody dostępowe do pól

std::string ISBN() const { return m_strISBN; }
};

Z klasy CDocument , reprezentującej dowolny dokument, dziedziczą dwie następne: COnlineDocument , odpowiadająca tekstom dostępnym przez Internet, oraz CBook , opisująca książki.

Napiszmy również odpowiednią funkcję, wyświetlającą podstawowe informacje o podanym dokumencie:

#include <iostream>
void PokazDaneDokumentu(CDocument* pDokument)
{
// wyświetlenie autora
std::cout << "AUTOR: " ;
std::cout << pDokument->Autor() << std::endl;
// pokazanie tytułu dokumentu
// (sekwencja \" wstawia cudzysłów do napisu)

std::cout << "TYTUL: " ;
std::cout << "\"" << pDokument->Tytul() << "\"" << std::endl;
// data utworzenia dokumentu
// (pDokument->Data() zwraca strukturę typu tm, do której pól
// można dostać się tak samo, jak do wszystkich innych - za
// pomocą operatora wyłuskania . (kropki))

std::cout << "DATA : " ;
std::cout << pDokument->Data().tm_mday << "."
<< (pDokument->Data().tm_mon + 1 ) << "."
<< (pDokument->Data().tm_year + 1900 ) << std::endl;
}

Bierze ona jeden parametr, będący zasadniczo wskaźnikiem na obiekt typu CDocument . W jego charakterze może jednak występować także wskazanie na któryś z obiektów potomnych, zatem poniższy kod będzie absolutnie prawidłowy:

COnlineDocument* pTutorial = new COnlineDocument( "Xion" , // autor
"Od zera do gier kodera" , // tytuł
"http://avocado.risp.pl" ); // URL
PokazDaneDokumentu (pTutorial);
delete pTutorial;

 

W pierwszej linijce możnaby równie dobrze użyć typu wskazującego na obiekt CDocument , gdyż wskaźnik pTutorial i tak zostanie potraktowany w ten sposób przy przekazywaniu go do funkcji PokazDaneDokumentu() .

Efektem jego działania powyższego listingu będzie na przykład taki oto widok:


Screen 32. Informacje o dokumencie uzyskane z użyciem prostego polimorfizmu

Brak tu informacji o adresie internetowym dokumentu, ponieważ należy on do składowych specyficznych dla klasy COnlineDocument . Funkcja PokazDaneDokumentu() została natomiast stworzona do pracy z obiektami CDocument , zatem wykorzystuje jedynie informacje zawarte w klasie bazowej. Nie przeszkadza to jednak w przekazaniu jej obiektu klasy pochodnej - w takim przypadku dodatkowe dane zostaną po prostu zignorowane.

To raczej mało satysfakcjonujące rozwiązanie, ale lepsze skutki wymagają już użycia metod wirtualnych. Uczynimy to w kolejnym przykładzie.

Naturalnie, podobny rezultat otrzymalibyśmy podając naszej funkcji obiekt klasy CBook czy też jakiejkolwiek innej dziedziczącej od CDocument . Kod procedury jest więc uniwersalny i może być stosowany do wielu różnych rodzajów obiektów.

Eureka! Na tym przecież polega polimorfizm :)

Możliwe że zauważyłeś, iż żadna z tych przykładowych klas nie jest tutaj typem polimorficznym, a jednak podany wyżej kod działa bez zarzutu. Powodem tego jest jego względna prostota. Dokładniej mówiąc, nie jest konieczne sprawdzanie poprawności typów podczas działania programu, bo wystarczająca jest zwykła kontrola, dokonywana zwyczajowo podczas kompilacji kodu.

Klasy wiedzą same, co należy robić

Z poprzednim przykładem związany jest pewien mankament, nietrudno zresztą zauważalny. Niezależnie od tego, jakie dodatkowe dane o dokumencie zadeklarujemy w klasach pochodnych, nasza funkcja wyświetli tylko i wyłącznie te przewidziane w klasie CDocument . Nie uzyskamy więc nic ponad autora, tytuł oraz datę stworzenia dokumentu.

Trzeba jednak przyznać, że sami niejako jesteśmy sobie winni. Wyodrębniając czynność prezentacji obiektu poza sam obiekt postąpiliśmy niezgodnie z ideą OOPu, która nakazuje łączyć dane i operujący na nich kod.

Zatem przykład z poprzedniego paragrafu to zdecydowanie zły przykład :D

O wiele lepszym rozwiązaniem jest dodanie do klasy CDocument odpowiedniej metody, odpowiedzialnej za czynność wypisywania. A już całkowitym ideałem będzie uczynienie jej funkcją wirtualną - wtedy klasy dziedziczące od CDocument będą mogły ustalić własny sposób prezentacji swoich danych.

Wszystkie te doskonałe pomysły praktycznie realizuje poniższy program przykładowy:

// Polymorphism - wykorzystanie techniki polimorfizmu
// *** documents.h ***

class CDocument
{
// (większość składowych wycięto z powodu zbyt dużej objętości)
public :
virtual void PokazDane();
};
// (reszty klas nieuwzględniono z powodu dziury budżetowej ;D)
// (zaś ich implementacje są w pliku documents.cpp)
// *** main.cpp ***

#include <iostream>
#include <conio.h>
#include "documents.h"
void main()
{
// wskaźnik na obiekty dokumentów
CDocument* pDokument;
// pierwszy dokument - internetowy
std::cout << std::endl << "--- 1. pozycja ---" << std::endl;
pDokument = new COnlineDocument( "Regedit" ,
"Cyfrowe przetwarzanie tekstu" ,
"http://programex.risp.pl/?"
"strona=cyfrowe_przetwarzanie_tekstu"
);
pDokument->PokazDane();
delete pDokument;
// drugi dokument - książka
std::cout << std::endl << "--- 2. pozycja ---" << std::endl;
pDokument = new CBook( "Sam Williams" ,
"W obronie wolnosci" ,
"83-7361-247-5" );
pDokument->PokazDane();
delete pDokument;
getch();
}

Wynikiem jego działania będzie poniższe zestawienie:


Screen 33. Aplikacja prezentująca polimorfizm z wykorzystaniem metod wirtualnych

Zauważmy, że za wyświetlenie obu widniejących na nim pozycji odpowiada wywołanie pozornie tej samej funkcji:

pDokument->PokazDane();

Polimorficzny mechanizm metod wirtualnych sprawia jednak, że zawsze wywoływana jest odpowiednia wersja procedury PokazDane() - odpowiednia dla kolejnych obiektów, na które wskazuje pDokument .

Tutaj mamy wprawdzie tylko dwa takie obiekty, ale nietrudno wyobrazić sobie analogiczne działanie dla większej ich liczby, np.:

CDocument* apDokumenty[ 100 ];
for ( unsigned i = 0 ; i < 100 ; ++i)
apDokumenty[i]->PokazDane();

Poszczególne elementy tablicy apDokumenty mogą wskazywać na obiekty dowolnych klas , dziedziczących od CDocument , a i tak kod wyświetlający ich dane będzie ograniczał się do wywołania zaledwie jednej metody! I to właśnie jest piękne :D

Możliwe zastosowania takiej techniki można mnożyć w nieskończoność, zaś w grach jest po prostu nieoceniona. Pomyślmy tylko, że za pomocą podobnej tablicy i prostej pętli możemy wykonać dowolną czynność na zestawie przeróżnych obiektów. Rysowanie, wyświetlanie, kontrola animacji - wszystko to możemy wykonać poprzez jedną instrukcję! Niezależnie od tego, jak bardzo byłaby rozbudowana hierarchia naszych klas (np. jednostek w grze strategicznej, wrogów w grze RPG, i tak dalej), zastosowanie polimorfizmu z metodami wirtualnymi upraszcza kod większości operacji do podobnie trywialnych konstrukcji jak powyższa.

Od tej pory do nas należy więc tylko zdefiniowanie odpowiedniego modelu klas i ich metod, gdyż zarządzanie poszczególnymi obiektami staje się, jak widać, banalne. Co ważniejsze, zastosowanie technik obiektowych nie tylko upraszcza kod, ale też pozwala na znacznie większą elastyczność.

Pamiętaj, że praktyka czyni mistrza! Poznanie teoretycznych aspektów programowania obiektowego jest wprawdzie niezbędne, ale najwięcej wartościowych umiejętności zdobędziesz podczas samodzielnego projektowania i kodowania programów. Wtedy szybko przekonasz się, że stosowanie technik polimorfizmu jest prawie że intuicyjne - nawet jeśli teraz nie jesteś zbytnio tego pewien.

Typy pod kontrolą

Uniwersalny kod dla wszystkich klas w hierarchii jest bardzo wygodnym rozwiązaniem. Okazjonalnie jednak zdarza się, że trzeba w nim uwzględnić także bardziej szczegółowe przypadki, co oznacza koniecznośc sprawdzania faktycznego typu obiektów, na które wskazują nasze wskaźniki.

Na szczęście C++ oferuje proste mechanizmy, umożliwiające realizację tego zadania.

Operator dynamic_cast

Konwersja wskaźnika do klasy pochodnej na wskaźnik do klasy bazowej jest czynnością dość naturalną, więc przebiega całkowicie automatycznie. Niepotrzebne jest nawet zastosowanie jakiejś formy rzutowania. Nie powinno to wcale dziwić - w końcu na tym polega sama idea dziedziczenia, że obiekt klasy potomnej jest także obiektem przynależnym klasie bazowej.

Inaczej jest z konwersją w odwrotną stronę - ta nie zawsze musi się przecież powieść. C++ powinien więc udostępniać jakiś sposób na sprawdzenie, czy taka zamiana jest możliwa, no i na samo jej przeprowadzanie. Do tych celów służy operator rzutowania dynamic_cast .

Jest to drugi z operatorów rzutowania, jakie mamy okazję poznać. Został on wprowadzony do języka C++ po to, by umożliwić kompleksową obsługę typów polimorficznych w zakresie konwersji "w dół" hierarchii klas. Jego przeznaczenie jest zatem następujące:

Operator dynamic_cast służy do rzutowania wskaźnika do obiektu klasy bazowej na wskaźnik do obiektu klasy pochodnej.

Powiedzieliśmy sobie również, że taka konwersja niekoniecznie musi być możliwa. Rolą omawianego operatora jest więc także sprawdzanie, czy rzeczywiście mamy do czynienia z wskaźnikiem do obiektu potomnego, przechowywanym przez zmienną będącą wskaźnikiem do typu bazowego.

Uff, wszystko to wydaje się bardzo zakręcone, zatem najlepiej będzie, jeżeli przyjrzymy się odpowiednim przykładom. Po raz kolejny posłużymy się przy tym naszą ulubioną systematyką klas zwierząt i napiszemy taką oto funkcję:

#include <stdlib.h> // żeby użyć rand() i srand()
#include <ctime> // żeby użyć time()
IAnimal* StworzLosoweZwierze()
{
// zainicjowanie generatora liczb losowych
srand ( static_cast < unsigned >(time(NULL)));
// wylosowanie liczby i stworzenie obiektu zwierza
switch (rand() % 4 )
{
case 0 : return new CFish;
case 1 : return new CMammal;
case 2 : return new CBird;
case 3 : return new CHomeDog;
default : return NULL;
}
}

Losuje ona liczbę i na jej podstawie tworzy obiekt jednej z czterech, zdefiniowanych jakiś czas temu, klas zwierząt. Następnie zwraca wskaźnik do niego jako wynik swego działania. Rezultat ten jest rzecz jasna typu IAnimal* , aby mógł "pomieścić" odwołania do jakiegokolwiek zwierzęcia, dziedziczącego z klasy bazowej IAnimal .

Powyższa funkcja jest bardzo prostym wariantem tzw. fabryki obiektów (ang. object factory ). Takie fabryki to najczęściej osobne obiekty, które tworzą zależne do siebie byty np. na podstawie stałych wyliczeniowych, przekazywanych swoim metodom. Metody takie mogą więc zwrócić wiele różnych rodzajów obiektów, dlatego deklaruje się je z użyciem wskaźników na klasy bazowe - u nas jest to IAnimal* .

Wywołanie tej funkcji zwraca nam więc dowolne zwierzę i zdawałoby się, że nijak nie potrafimy sprawdzić, do jakiej klasy ono faktycznie należy. Z pomocą przychodzi nam jednak operator dynamic_cast , dzięki któremu możmue spróbować rzutowania otrzymanego wskaźnika na przykład do typu CMammal* :

IAnimal* pZwierze = StworzLosoweZwierze();
CMammal* pSsak = dynamic_cast <CMammal*>(pZwierze);

Taka próba powiedzie się jednak tylko w średnio połowie przypadków (dlaczego? 1 ). Co zatem będzie, jeżeli pZwierze odnosi się do innego rodzaju zwierząt?.

Otóż w takim przypadku otrzymamy prostą informację o błędzie, mianowicie:

dynamic_cast zwróci wskaźnik pusty (o wartości NULL ), jeżeli niemożliwe będzie dokonanie podanego rzutowania.

Aby ją wychwycić potrzebujemy oczywiście dodatkowego warunku, porównującego zmienną pSsak z tą specjalną wartością NULL (będącą zresztą de facto zerem):

if (pSsak != NULL) // sprawdzenie, czy rzutowanie powiodło się
{
// OK - rzeczywiście mamy do czynienia z obiektem klasy CMammal.
// pSsak może być tu użyty tak samo, jak każdy inny wskaźnik
// na obiekt klasy CMammal, na przykład:

pSsak->Biegnij();
}

 

Warunek if (pSsak != NULL) może być zastąpiony przez if (pSsak) . Wówczas kompilator dokona automatycznej zamiany wartości pSsak na logiczną, co da fałsz, jeżeli jest ona równa zeru (czyli NULL ) oraz prawdę w każdej innej sytuacji.

 

Możliwe jest nawet większe skondensowanie kodu. Wystarczy wstawić linijkę z rzutowaniem bezspośrednio do warunku if , tzn. zastosować instrukcję:
if (CMammal* pSsak = dynamic_cast <CMammal*>(pZwierzak))
Pojedynczy znak = jest tutaj umieszczony celowo, gdyż w ten sposób całe przypisanie reprezentuje wynik rzutowania, który zostaje potem niejawnie przyrównany do zera.

Kontrola otrzymanego wyniku rzutowania jest konieczna; jeżeli bowiem spróbowaliśmy zastosować operator wyłuskania -> do pustego wskaźnika, spowodowalibyśmy błąd ochrony pamięci ( access violation ).

Należy więc zawsze sprawdzać, czy rzutowanie dynamic_cast powiodło się, poprzez porównanie otrzymanego wskaźnika z wartością NULL .

I to jest w zasadzie wszystko, co należy wiedzieć o operatorze dynamic_cast :)

Incydentalnie trafiają się sytuacje, w których zastosowanie omawianego operatora wymaga włączenia specjalnej opcji kompilatora, uaktywniającej informacje o typie podczas działania programu. Są to rzadkie przypadki i prawie zawsze dotyczą wielodziedziczenia, niemniej warto wiedzieć, że takie niespodzianki mogą się czasem przytrafić.
Sposób włączenia informacji o typie w czasie działania programu jest opisany w następnym paragrafie.

 

Bliższych szczegółow na temat rzutowania dynamic_cast można doszukać się w dynamic_cast_Operator.htm">MSDN .

typeid i informacje o typie podczas działania programu

Oprócz dynamic_cast - operatora, który pozwala sprawdzić, czy dany wskaźnik do klasy bazowej wskazuje też na obiekt klasy pochodnej - C++ dysponuje nieco bardziej zaawansowanymi konstrukcjami, dostarczającymi wiadomości o typach obiektów i wyrażeń w programie. Są to tak zwane informacje o typie czasu wykonania (ang. Run-Time Type Information ), oznaczane angielskim skrótowcem RTTI .

Znajomość opisywanej tu części RTTI, czyli operatora typeid , generalnie nie jest tak potrzebna, jak umiejętność posługiwania się operatorami rzutowania, ale daje kilka bardzo ciekawych możliwości, najczęściej nieosiągalnych inną drogą. Możesz aczkolwiek tymczasowo pominąć ten paragraf, by wrócić do niego później.

Skorzystanie z RTTI wymaga podjęcia dwóch wstępnych kroków:

  • włączenia odpowiedniej opcji kompilatora:

1.

2.

3.

Screen 34, 35 i 36. Trzy kroki do włączenia RTTI w Visual Studio .NET

W Visual Studio. NET należy w tym celu rozwinąć zakładkę Solution Explorer , kliknąć prawym przyciskiem myszy na nazwę swojego projektu i z menu podręcznego wybrać Properties . W pojawiającym się oknie dialogowym trzeba teraz przejść do strony C/C++|Language i przy opcji Enable Run-Time Type Info ustawić wariant Yes (/GR) .

  • dołączenia do kodu standardowego nagłówka typeinfo , czyli dodania dyrektywy:
    #include <typeinfo>

W zamian za te wysiłki otrzymamy możliwość korzystania z operatora typeid , pobierającego informację o typie podanego mu wyrażenia. Składnia jego użycia jest następująca:

typeid ( wyrażenie ). informacja

Faktycznie instrukcja typeid ( wyrażenie ) zwraca strukturę, należącą do wbudowanego typu std::type_info . Struktura ta opisuje typ wyrażenia i zawiera takie oto składowe:

informacja

opis

name()

Jest to nazwa typu w czytelnej i przyjaznej dla człowieka formie. Możemy ją przechowywać i operować nią tak, jak każdym innym napisem. Przykład:


#include <typeinfo>

#include <iostream>

#include <ctime>


int nX = 10 ; float fY = 3.14 ;

time_t Czas = time(NULL); tm Data = *localtime(&Czas);


std::cout << typeid (nX).name(); // wynik: "int"

std::cout << typeid (fY).name(); // wynik: "float"

std::cout << typeid (Data).name(); // wynik: "struct tm"

raw_name()

Zwraca nazwę typu wewnętrznie używaną przez kompilator. Taka nazwa musi być unikalna, dlatego zawiera różne "dekoracyjne" znaki, jak ? czy @. Nie jest czytelna dla człowieka, ale może ewentualnie służyć w celach porównawczych.

Tabela 11. Informacje dostępne poprzez operator typeid

Oprócz pobierania nazwy typu w postaci ciągu znaków możemy używać operatorów == oraz != do porównywania typów dwóch wyrażeń, na przykład:

unsigned uX;
if ( typeid (uX) == typeid ( unsigned ))
std::cout << "Świetnie, nasz kompilator działa ;D" ;
if ( typeid (uX) != typeid (uX / 0.618 ))
std::cout << "No proszę, tutaj też jest dobrze :)" ;

typeid mógłby więc służyć nam do sprawdzania klasy, do której należy polimorficzny obiekt wskazywany przez wskaźnik. Sprawdźmy zatem, jak by to mogło wyglądać:

IAnimal* pZwierze = new CBird;
std::cout << typeid (pZwierze).name();

Po wykonaniu tego kodu spotka nas raczej przykra niespodzienka - zamiast oczekiwanego rezultatu "class CBird *" otrzymamy "class IAnimal *" ! Wygląda na to, że faktyczny typ obiektu, do którego odwołuje się pZwierze , nie został w ogóle wzięty pod uwagę.

Przypuszczenia te są słuszne. Otóż typeid jest "leniwym" operatorem i zawsze idzie po najmniejszej linii oporu. Typ wyrażenia pZwierze mógł zaś określić nie sięgając nawet do mechanizmów polimorficznych, ponieważ wyraźnie zadeklarowaliśmy go jako IAnimal* . Aby zmusić krnąbrny operator do większego wysiłku, musimy mu podać sam obiekt , a nie wskaźnik na niego, co czynimy w ten sposób:

std::cout << typeid (*pZwierze).name();

O występującym tu operatorze dereferencji - gwiazdce ( * ) powiemy sobie bliżej, gdy przejdziemy do dokładnego omawiania wskaźników jako takich. Na razie zapamiętaj, że przy jego pomocy "wyławiamy" obiekt poprzez wskaźnik do niego.

Naturalnie, teraz powyższy kod zwróci prawidłowy wynik "class CBird" .

Pełny opis operatora typeid znajduje się oczywiście w MSDN .


1 Obiekt klasy CMammal jest tworzony zarówno poprzez new CMammal , jak i new CHomeDog . Klasa CHomeDog dziedziczy przecież po klasie CMammal .

Alternatywne rozwiązania

RTTI jest często zbyt ciężką armatą, wytoczoną przeciw problemowi pobierania informacji o klasie obiektu podczas działania aplikacji. Przy niewielkim nakładzie pracy można samemu wykonać znacznie mniejszy, acz nierzadko wystarczający system.

Po co? Decydującym argumentem może być szybkość. Wbudowane mechanizmy RTTI, jak dynamic_cast i typeid , są dosyć wolne (szczególnie dotyczy to tego pierwszego). Własne, bardziej poręczne rozwiązanie może mieć spory wpływ na wydajność.

Do tego celu mogą posłużyć metody wirtualne oraz odpowiedni typ wyliczeniowy, posiadający listę wartości odpowiadających poszczególnym klasom. W przypadku naszych zwierząt mógłby on wyglądać na przykład tak:

enum ANIMAL { A_BASE, // bazowa klasa IAnimal
A_FISH, // klasa CFish
A_MAMMAL, // klasa CMammal
A_BIRD, // klasa CBird
A_HOMEDOG }; // klasa CHomeDog

Teraz wystarczy tylko zdefiniować proste metody wirtualne, które będą zwracały stałe właściwe swoim klasom:

// (pominąłem pozostałe składowe klas)
class IAnimal
{
public :
virtual ANIMAL Typ() const { return A_BASE; }
};
// bezpośrednie pochodne
class CFish : public IAnimal
{
public :
ANIMAL Typ() const { return A_FISH: }
};
class CMammal : public IAnimal
{
public :
ANIMAL Typ() const { return A_MAMMAL; }
};
class CBird : public IAnimal
{
public :
ANIMAL Typ() const { return A_BIRD; }
};
// pośrednie pochodne
class CHomeDog : public CMammal
{
public :
ANIMAL Typ() const { return A_HOMEDOG; }
};

Po zastosowaniu tego rozwiązania możemy chociażby użyć instrukcji switch , by wykonać kod zależny od typu obiektu:

IAnimal* pZwierzak = StworzLosoweZwierze();
switch (pZwierzak->Typ())
{
case A_FISH: static_cast <CFish*>(pZwierzak)->Plyn(); break ;
case A_BIRD: static_cast <CBird*>(pZwierzak)->Lec(); break ;
case A_MAMMAL: static_cast <CMammal*>(pZwierzak)->Biegnij(); break ;
case A_HOMEDOG: static_cast <CHomeDog*>(pZwierzak)->Szczekaj(); break ;
}

Podobne sprawdzenie, dokonywane przy użyciu dynamic_cast lub typeid , wymagałoby wielopiętrowej instrukcji if . Tutaj wystarczy bardziej naturalny switch , zaś do formalnego rzutowania możemy użyć prostego static_cast , które działa szybciej niż mechanizmy RTTI.

Trzeba jednak pamiętać, że aby bezpiecznie stosować static_cast do rzutowania w dół hierarchii klas, musimy mieć pewność, że taka operacja jest faktycznie wykonalna. Tutaj sprawdzamy rzeczywisty typ obiektu 1 , zatem wszystko jest w porządku, lecz w innych przypadkach należy skorzystać z dynamic_cast .

Systemy identyfikacji i zarządzania typami, podobne do powyższego, są w praktyce używane bardzo często, szczególnie w wielkich projektach. Najbardziej zaawansowane warianty umożliwiają nawet tworzenie obiektów na podstawie nazwy klasy przechowywanej jako napis lub też dynamiczne odtworzenie hierarchii klas podczas działania programu. Trzeba jednak przyznać, iż jest to nierzadko sztuka dla samej sztuki, bez wielkiego praktycznego znaczenia.

" Równowaga przede wszystkim" - pamiętajmy tę sentencję :D

 

***

Gratulacje! Właśnie poznałeś wszystkie teoretyczne założenia programowania obiektowego i ich praktyczną realizację w C++. Wykorzystując zdobytą wiedzę, będziesz mógł efektywnie programować aplikacje z użyciem filozofii OOP.

Słucham? Mówisz, że to wcale nie jest takie proste? Zgadza się, na początku myślenie w kategoriach obiektowych może rzeczywiście sprawiać ci trudności. Pomyślałem więc, że dobrze będzie poświęcić nieco czasu także na zagadnienia związane z samym projektowaniem aplikacji z użyciem poznanych technik. Zajmiemy się tym w nadchodzącym podrozdziale.

1 Sprawdzenie przy użyciu typeid także upoważniałoby nas do stosowania static_cast podczas rzutowania.

| | | |
Copyright © 2006-2013 egrafik.pl | Kontakt | Reklama | Projekty domów
jocker