Ogólnie

 

Tworzymy sobie klasę abstrakcyjną i kilka klas dziedziczących po niej. Teraz tworzymy wskaźnik na klasę abstrakcyjną i po new przypisujemy jej obiekt typu klasy dziedziczacej. Możemy więc wywołać metodę z klasy rodzica, a wywoła się z klasy dziecka. Śliczne!

Polimorfizm

 

Zacznijmy 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.

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.

Prosty przypadek wykorzystania polimorfizmu opiera się na elementarnej i rozsądnej zasadzie. 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
   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:
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:
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ść.