r14.pdf

(367 KB) Pobierz
Szablon dla tlumaczy
Rozdział 14.
Polimorfizm
Z rozdziału 12. dowiedziałeś się, jak pisać funkcje wirtualne w klasach wyprowadzonych. Jest to
jedna z podstawowych umiejętności potrzebnych przy posługiwaniu się polimorfizmem, czyli
możliwością przypisywania — już podczas działania programu — specyficznych obiektów klas
pochodnych do wskaźników wskazujących na obiekty klasy bazowej.
Z tego rozdziału dowiesz się:
czym jest dziedziczenie wielokrotne i jak z niego korzystać,
czym jest dziedziczenie wirtualne,
czym są abstrakcyjne typy danych,
czym są czyste funkcje wirtualne.
Problemy z pojedynczym dziedziczeniem
Przypuśćmy, że od pewnego czasu pracujemy z naszymi klasami zwierząt i że podzieliliśmy
hierarchię klas na ptaki ( Bird ) i ssaki ( Mammal ). Klasa Bird posiada funkcję składową Fly()
(latanie). Klasa Mammal została podzielona na różne rodzaje ssaków, między innymi na klasę
Horse (koń). Klasa Horse posiada funkcje składowe Whinny() (rżenie) oraz Gallop()
(galopowanie).
Nagle okazuje się, że potrzebujemy obiektu pegaza ( Pegasus ): skrzyżowania konia z ptakiem.
Pegasus może latać (metoda Fly() ), ale także może rżeć ( Whinny() ) i galopować ( Gallop() ).
Przy dziedziczeniu pojedynczym okazuje się, że jesteśmy w kropce.
Możemy uczynić z pegaza obiekt klasy Bird , ale wtedy nie będzie mógł rżeć ani galopować.
Możemy zrobić z niego obiekt Horse , ale wtedy nie będzie mógł latać.
Pierwszą próbą rozwiązania tego problemu może być skopiowanie metody Fly() do klasy
Pegasus i wyprowadzenie tej klasy z klasy Horse . Będzie to prawidłowa operacja,
przeprowadzona jednak kosztem posiadania metody Fly() w dwóch miejscach (w klasach Bird i
Pegasus ). Gdy zmienisz ją w jednym miejscu, musisz pamiętać o wprowadzeniu modyfikacji
także w drugim. Oczywiście, programista, który kilka miesięcy czy lat później spróbuje
zmodyfikować taki kod, także musi wiedzieć o obu miejscach.
Wkrótce jednak pojawia się nowy problem. Chcemy stworzyć listę obiektów typu Horse oraz listę
obiektów typu Bird . Chcielibyśmy dodać obiekt klasy Pegasus do dowolnej z tych list, ale
gdyby Pegasus został wyprowadzony z klasy Horse , nie moglibyśmy go dodać do listy obiektów
klasy Bird .
Istnieje kilka rozwiązań tego problemu. Możemy zmienić nazwę metody Gallop() na Move()
(ruch), a następnie przesłonić metodę Move() w klasie Pegasus tak, aby wykonywała pracę
metody Fly() . Następnie przesłonilibyśmy metodę Move() innych koni tak, aby wykonywała
pracę metody Gallop() . Być może pegaz byłby inteligentny na tyle, by galopować na krótkich
dystansach, a latać tylko na dłuższych:
Pegasus::Move(long distance)
{
if (distance > veryFar)
Fly(distance);
else
Gallop(distance);
}
To rozwiązanie posiada jednak pewne ograniczenia. Być może któregoś dnia pegaz zechce latać
na krótkich dystansach lub galopować na dłuższych. Następnym rozwiązaniem mogłoby być
przeniesienie metody Fly() w górę, do klasy Horse , co zostało pokazane na listingu 14.1.
Problem jednak polega na tym, iż zwykłe konie nie potrafią latać, więc w przypadku koni innych
niż pegaz, ta metoda nie będzie nic robić.
Listing 14.1. Gdyby konie umiały latać...
0: // Listing 14.1. Gdyby konie umiały latać...
1: // Przeniesienie metody Fly() do klasy Horse
2:
3: #include <iostream>
4: using namespace std;
5:
6: class Horse
7: {
8: public:
9: void Gallop(){ cout << "Galopuje...\n"; }
10: virtual void Fly() { cout << "Konie nie potrafia latac.\n" ; }
11: private:
12: int itsAge;
13: };
14:
15: class Pegasus : public Horse
16: {
17: public:
18: virtual void Fly() {cout<<"Moge latac! Moge latac! Moge
latac!\n";}
19: };
20:
21: const int NumberHorses = 5;
22: int main()
23: {
24: Horse* Ranch[NumberHorses];
25: Horse* pHorse;
26: int choice,i;
27: for (i=0; i<NumberHorses; i++)
28: {
29: cout << "(1)Horse (2)Pegasus: ";
30: cin >> choice;
31: if (choice == 2)
32: pHorse = new Pegasus;
33: else
34: pHorse = new Horse;
35: Ranch[i] = pHorse;
36: }
37: cout << "\n";
38: for (i=0; i<NumberHorses; i++)
39: {
40: Ranch[i]->Fly();
41: delete Ranch[i];
42: }
43: return 0;
44: }
Wynik
(1)Horse (2)Pegasus: 1
(1)Horse (2)Pegasus: 2
(1)Horse (2)Pegasus: 1
(1)Horse (2)Pegasus: 2
(1)Horse (2)Pegasus: 1
Konie nie potrafia latac.
Moge latac! Moge latac! Moge latac!
Konie nie potrafia latac.
Moge latac! Moge latac! Moge latac!
Konie nie potrafia latac.
Analiza
Ten program oczywiście działa, ale kosztem posiadania przez klasę Horse metody Fly() .
Metoda Fly() dla klasy Horse jest zdefiniowana w linii 10. W rzeczywistej klasie mogłaby po
prostu wyświetlać komunikat błędu lub po cichu zakończyć działanie. W linii 18. klasa Pegasus
przesłania metodę Fly() tak, aby wykonywała właściwą pracę, w tym przypadku polegającą na
wypisywaniu radosnego komunikatu.
Tablica wskaźników do klasy Horse , zadeklarowana w linii 24., służy do zademonstrowania, że
właściwa metoda Fly() zostaje wywołana w zależności od tego, czy został stworzony obiekt klasy
Horse lub klasy Pegasus .
UWAGA Pokazany tutaj przykład został bardzo okrojony, do elementów niezbędych dla
zrozumienia zasad jego działania. Konstruktory, wirtualne destruktory i tak dalej, zostały
usunięte w celu ułatwienia analizy kodu.
Przenoszenie w górę
Przenoszenie pożądanej funkcji w górę hierarchii klas jest powszechnym rozwiązaniem tego typu
problemów; powoduje jednak, że w klasie bazowej występuje wiele funkcji „nadmiarowych”.
Istnieje niebezpieczeństwo, że klasa bazowa stanie się globalną przestrzenią nazw dla wszystkich
funkcji, które mogłyby być użyte w klasach potomnych. Może to znacznie wpłynąć na
efektywność zarządzania typami w C++ i powodować zbytni rozrost i skomplikowanie klas
bazowych.
Chcemy przenieść funkcjonalność w górę hierarchii, ale bez równoczesnego przenoszenia
interfejsu każdej z klas. Oznacza to, że jeśli dwie klasy posiadają wspólną klasę bazową (na
przykład klasy Horse i Bird pochodzą od klasy Animal ) i posiadają wspólną funkcję (zarówno
konie, jak i ptaki odżywiają się), powinniśmy przenieść tę cechę w górę, do klasy bazowej i
stworzyć z niej funkcję wirtualną.
Powinniśmy unikać przy tym przenoszenia interfejsu (tak, jak przeniesienie metody Fly() tam,
gdzie nie powinno jej być) tylko w celu wywoływania danej funkcji w niektórych z klas
wyprowadzonych.
Rzutowanie w dół
Alternatywą dla przedstawionego wcześniej rozwiązania (nie wykluczającą korzystania z
pojedynczego dziedziczenia), jest zatrzymanie metody Fly() wewnątrz klasy Pegasus i
wywoływanie jej tylko wtedy, gdy wskaźnik do obiektu rzeczywiście wskazuje obiekt klasy
Pegasus . Aby sposób ten mógł działać, musimy mieć możliwość zapytania wskaźnika, jaki typ
faktycznie wskazuje. Nazywa się to identyfikacją typów podczas wykonywania programu (RTTI,
Run Time Type Identification). Korzystanie z RTTI stało się oficjalnym elementem języka C++
dopiero od niedawna.
Jeśli kompilator nie obsługuje RTTI, możemy symulować tę obsługę, umieszczając w każdej z
klas metodę zwracającą jedną z wyliczeniowych stałych. Możemy następnie sprawdzać typ
podczas działania programu i wywoływać metodę Fly() tylko wtedy, gdy ta metoda zwróci stałą
dla typu Pegasus .
UWAGA Bądź ostrożny z RTTI. Korzystanie z tego mechanizmu może być oznaką słabości
projektu programu. Zamiast tego użyj funkcji wirtualnych, wzorców lub wielokrotnego
dziedziczenia.
Aby móc wywołać metodę Fly() , musimy dokonać rzutowania wskaźnika, informując
kompilator, że wskazywany obiekt jest obiektem typu Pegasus , a nie obiektem typu Horse .
Nazywa się to rzutowaniem w dół, gdyż obiekt Horse rzutujemy w dół hierarchii, do typu bardziej
wyprowadzonego.
Dziś C++ już oficjalnie, choć dość niechętnie, obsługuje rzutowanie w dół za pomocą nowego
operatora dynamic_cast . Oto sposób jego działania:
Jeśli mamy wskaźnik do klasy bazowej, takiej jak Horse , i przypiszemy mu adres obiektu klasy
wyprowadzonej, takiej jak Pegasus , możemy używać wskaźnika do klasy Horse polimorficznie.
Jeśli chcemy następnie odwołać się do obiektu klasy Pegasus , tworzymy wskaźnik do tej klasy i
w celu dokonania konwersji używamy operatora dynamic_cast .
W czasie działania programu nastąpi sprawdzenie wskaźnika do klasy bazowej. Jeśli konwersja
będzie właściwa, nowy wskaźnik do klasy Pegasus będzie poprawny. Jeśli konwersja będzie
niewłaściwa (nie będzie to wskaźnik do klasy Pegasus ), nowy wskaźnik będzie pusty ( null ).
Ilustruje to listing 14.2.
Listing 14.2. Rzutowanie w dół
0: // Listing 14.2 Użycie operatora dynamic_cast.
1: // Using rtti
2:
3: #include <iostream>
4: using namespace std;
5:
6: enum TYPE { HORSE, PEGASUS };
7:
8: class Horse
Zgłoś jeśli naruszono regulamin